Interfaces
ConnectRemoteSandboxOptions
Properties
LazyRemoteSandbox
remoteSandbox’s return type: the branded handle plus ready for
eager checkers. ready kicks off the connection when first accessed and
settles with the same fail-fast errors connectRemoteSandbox throws
(no serve discovered / no browser tab connected).
Extends
Properties
| Property | Modifier | Type | Description |
|---|---|---|---|
[REMOTE_SANDBOX] | readonly | true | - |
admin | readonly | SandboxAdmin | Admin-plane access (rule-bypass reads). Identity-agnostic by design — admin reads aren’t gated on auth, so they live on the sandbox, not on a context. See SandboxAdmin. |
auth | readonly | RemoteAuthAdmin | Admin auth user CRUD. |
channel | readonly | RemoteSandboxChannel | The raw worker op/sub relay channel (narrowed to the wire payload types). |
currentUser | public | { token?: Record<string, unknown>; uid: string; } | Current authenticated user across the sandbox. Mutated by pyric/auth’s signInAnonymously / signInWithEmailAndPassword / signOut / sandbox.setUser. Read per-call by service factories (e.g. a future getFirestore(sandbox) overload) so they see auth state changes without re-binding handles. Defaults to null (anonymous / signed out). Independent of withAuth({uid}) — withAuth still produces a frozen SandboxContext that carries its own identity for the runner’s test code (the existing pattern: explicit identity per service call). currentUser exists for the pyric/auth mirror, where consumer app code drives identity through a stateful Auth handle rather than naming it per call. |
currentUser.token? | public | Record<string, unknown> | - |
currentUser.uid | public | string | - |
ready | readonly | Promise<void> | - |
rtdb | readonly | RemoteRtdb | RTDB conveniences (admin lens pinned). |
serveUrl | readonly | string | Base URL of the pyric dev this handle is attached to (used in error guidance: “open |
storage | readonly | RemoteStorage | Storage conveniences (admin lens pinned; 8 MiB per-op byte cap). |
Methods
clearPersistence()
clearPersistence(): Promise<void>;
Wipe the persisted blob for this sandbox’s key. In-memory state
is left intact — call reset() if you want both. Useful for
“sign out and forget” flows.
No-op when persistence is not enabled.
Returns
Promise<void>
Inherited from
RemoteSandbox.clearPersistence
close()
close(): void;
Close the connection. In-flight ops reject; subscriptions stop.
Returns
void
Inherited from
dispose()
dispose(): void;
Tear down listener registries on this sandbox’s environment without
replacing it. Use this when you’re about to discard the sandbox
itself (e.g. runner.reseed() builds a fresh sandbox rather than
calling reset()) and want to drop callback references on the
outgoing instance defensively. Idempotent. Does not touch data.
Returns
void
Inherited from
enablePersistence()
enablePersistence(options: SandboxPersistenceOptions): Promise<void>;
Persist the sandbox’s data to a backend and restore it on next
enablePersistence call. The default 'indexedDB' backend turns
the sandbox into the host page’s local Firestore — writes flush
automatically and a fresh initializeSandbox() rehydrates from
the prior session.
Restoration happens before the promise resolves; awaiting this call is sufficient to guarantee in-memory state matches the persisted blob.
Idempotent across the same key — calling twice in one process
is a no-op on the second call. Different keys are rejected as an
error (a sandbox can persist to at most one backend at a time).
Listener semantics: every write event the sandbox emits triggers
a debounced flush (default 250ms). Browser hosts additionally
flush on beforeunload so a page navigation doesn’t lose the
tail of the debounce window.
See SandboxPersistenceOptions for backend selection and tuning.
Parameters
| Parameter | Type |
|---|---|
options | SandboxPersistenceOptions |
Returns
Promise<void>
Inherited from
RemoteSandbox.enablePersistence
enableTabSync()
enableTabSync(options?: TabSyncOptions): () => void;
Enable cross-tab realtime sync via BroadcastChannel. A write in
this tab will propagate to every OTHER tab of the same origin that
also called enableTabSync, causing their onSnapshot listeners to
re-evaluate — restoring production’s cross-client realtime behavior.
Opt-in, OFF by default. Firestore only (RTDB is a follow-on).
Returns a disable function. Calling it removes the onEvent
subscription, the channel message listener, and closes the channel
(when it was created internally). After disable, no further propagation
occurs in either direction.
Multi-writer note: concurrent writes from two tabs to the same doc produce last-write-wins divergence — there is no conflict resolution. The intended model is one active writer (one user, one tab) with observers in other tabs; this covers the overwhelming majority of local development scenarios.
Parameters
| Parameter | Type |
|---|---|
options? | TabSyncOptions |
Returns
(): void;
Returns
void
See
TabSyncOptions for channel injection (tests) and originId.
Example
// In every tab that should participate in realtime:
const sandbox = initializeSandbox();
const disableSync = sandbox.enableTabSync();
// Later, to stop syncing:
disableSync();
Inherited from
flush()
flush(): Promise<void>;
Force a snapshot to the configured persistence backend right now. Useful before a manual navigation, or in tests that need deterministic ordering against the debounce window. Resolves once the write hits the backend.
Throws if persistence is not enabled.
Returns
Promise<void>
Inherited from
history()
history(): SandboxEvent[];
Every SandboxEvent this sandbox has emitted since init or
the last reset(). Returns a defensive copy.
Use this for replay: hand the array to replay(events, rules)
from pyric/sandbox and the engine re-issues every
captured write against a fresh sandbox.
Unlike onEvent (live stream from the moment of subscribe),
history() returns every event the sandbox has seen — useful
for consumers that attach late (e.g., loading a saved session
before subscribing) or that need a snapshot at a particular moment.
reset() and dispose() each append a closing session_boundary
event; reset() then clears the history. Consumers that took a
snapshot before reset retain the boundary in their copy.
Returns
Inherited from
loadSnapshot()
loadSnapshot(data: SandboxSnapshot): void;
CLOBBER-restore the sandbox’s entire state from a prior snapshot:
reset() (clears firestore + the signed-in session), then rebuild firestore
from data and restore each registered service. This is a TOTAL replace —
documents absent from data do NOT survive — and is the counterpart to
snapshot. It is what makes “transfer (clobber) one instance’s data
into another” and named-branch switching possible.
Fires a session_boundary (reset phase), re-evaluates live listeners against
the loaded state, and the next persistence flush writes the loaded state.
Services present in data but not currently registered are skipped (a
snapshot taken via snapshot always includes every registered
service, so this only affects cross-instance imports from a sandbox that had
a service this one lacks).
Parameters
| Parameter | Type |
|---|---|
data | SandboxSnapshot |
Returns
void
Inherited from
onCurrentUserChanged()
onCurrentUserChanged(cb: (user: {
token?: Record<string, unknown>;
uid: string;
}) => void): () => void;
Subscribe to currentUser changes. Fires on every mutation —
sign-in, sign-out, user swap. Does NOT fire on subscribe.
Survives reset() and dispose() only as a no-op: a disposed
sandbox emits nothing further; a reset sandbox clears
currentUser to null (and fires the change) before swapping
the env.
Returns an unsubscribe function. Listener errors are swallowed — subscribers are observational, the sandbox does not propagate their errors.
Parameters
| Parameter | Type |
|---|---|
cb | (user: { token?: Record<string, unknown>; uid: string; }) => void |
Returns
(): void;
Returns
void
Inherited from
RemoteSandbox.onCurrentUserChanged
onEvent()
onEvent(cb: (event: SandboxEvent) => void): () => void;
Subscribe to every event the sandbox emits — see SandboxEvent
for the discriminated-union shape. One subscription covers
request/denial/snapshot-error/listener-lifecycle/session-boundary;
filter on event.kind to recover individual streams.
Replaces the prior three-channel surface (onRequest / onDenial
/ onSnapshotError) — see issue #307. Filter cookbook:
- All denials:
event.kind === 'request' && event.result === 'deny' - Stream errors:
event.kind === 'listener_errored' - Per-op traffic:
event.kind === 'request'
Survives sandbox.reset() — the subscription is held on the
sandbox, not on the underlying environment. A session_boundary
event with phase: 'reset' fires before the env swap so consumers
can segment their stream.
Returns an unsubscribe function. Listener errors are swallowed so a faulty subscriber can’t change rule semantics or hide other events. Both synchronous throws and rejected Promises from async callbacks are silently discarded — subscribers are observational, the sandbox doesn’t await them and doesn’t propagate their errors.
Parameters
| Parameter | Type |
|---|---|
cb | (event: SandboxEvent) => void |
Returns
(): void;
Returns
void
Inherited from
registerPersistableService()
registerPersistableService(name: string, hooks: PersistableService): () => void;
Register a service (auth, storage, …) as a persistence participant.
The sandbox calls hooks.snapshot() on every flush and
hooks.restore(data) on restore. If hooks.subscribe is provided,
the persistence controller subscribes and schedules a debounced
flush on each change — so auth-user edits flush promptly, not only
on the next Firestore write.
Returns an unregister function — call it if the service is torn
down before the sandbox is disposed (uncommon in practice; the
sandbox’s dispose() clears the registry anyway).
Throws failed-precondition when a service with the same name is
already registered — the auth package registers 'auth' once when
getAuth(sandbox) first creates a backend, so accidental double-
registration is a caller bug, not a no-op.
Advanced / internal API. Service packages (auth, storage) call this when they first attach to a sandbox. Consumer app code should not need to call this directly.
Parameters
| Parameter | Type |
|---|---|
name | string |
hooks | PersistableService |
Returns
(): void;
Returns
void
Inherited from
RemoteSandbox.registerPersistableService
reset()
reset(): void;
Reset the underlying environment to a fresh state — wipes data, rules, and any service-specific configuration.
Snapshot listeners attached to the OLD environment are dropped at
the swap — they can’t survive because their target docs have been
wiped. onEvent subscribers DO survive — the registry lives on
the sandbox, and a session_boundary event with phase: 'reset'
fires before the swap so subscribers know the rollover happened.
Existing SandboxContexts continue to work — their sandbox
reference is stable; subsequent operations resolve to the new env.
Returns
void
Inherited from
resetAll()
resetAll(): Promise<{
errors: string[];
}>;
Reset the WHOLE sandbox: reset (Firestore env + signed-in
session), then clear every registered persistable service that
provides a PersistableService.reset hook — auth users, the
RTDB tree, storage objects. This is the one sandbox-owned “wipe
everything” path: because it iterates the service registry, a new
service that registers with a reset hook is cleared automatically,
and a consumer (Pyric Studio’s reset) cannot forget one.
Service resets may be async (storage clears IndexedDB stores); the
returned promise resolves when every service has finished clearing.
A service whose reset throws is isolated (others still clear) and
REPORTED in the returned errors (as name: message) — a reset that
leaves data behind must never look successful to the caller.
Returns
Promise<{
errors: string[];
}>
Inherited from
runWithProvenance()?
optional runWithProvenance<T>(provenance: EventProvenance, fn: () => T): T;
Run fn with ambient EventProvenance defaults: every event
emitted SYNCHRONOUSLY during fn that doesn’t already carry a
provenance field (on the event itself or via an explicit per-emit
override) is stamped with these values instead of the global
defaults. This is the mechanical “who issued this op” seam the
serve worker uses to tag Studio-issued ops (actor: { kind: 'studio' }) and to stamp the auth lens an op ran under
(authLens) — declared by the caller that issues the op, never
inferred from the op’s shape.
SYNCHRONOUS WINDOW: the ambient values apply only until fn
returns (for an async fn, its synchronous prefix — which covers
the local environment’s rules eval + event emission, since those
run before the op’s promise is handed back). Work an op DEFERS
(snapshot-listener deliveries and re-evals drain on a microtask,
off-stack) is intentionally OUTSIDE the window: a listener re-eval
belongs to the listener’s owner, not to whoever’s write triggered
it. Nested calls stack — the innermost window wins per field, and
each window restores the previous one on exit (including on throw).
OPTIONAL because remote sandbox proxies can’t provide an ambient
emit window (events are emitted in the worker they front). Callers
spell sandbox.runWithProvenance?.(prov, fn) ?? fn().
Type Parameters
| Type Parameter |
|---|
T |
Parameters
| Parameter | Type |
|---|---|
provenance | EventProvenance |
fn | () => T |
Returns
T
Inherited from
RemoteSandbox.runWithProvenance
snapshot()
snapshot(): SandboxSnapshot;
Capture a snapshot of every service’s state. For v1 with only
Firestore, the return value carries a firestore key mapping doc
paths to data. Future services will add their own keys.
Returns
Inherited from
withAuth()
withAuth(auth: {
token?: Record<string, unknown>;
uid: string;
}): SandboxContext;
Derive a context bound to this sandbox under the given auth identity. Operations through services attached to the returned context evaluate rules under that identity. Many contexts can coexist for one sandbox; data is shared.
null is anonymous; an AuthState object names the user (and
optional custom claims). Passing undefined is a deliberate
error — say withAuth(null) for anonymous so the call site is
unambiguous.
Parameters
| Parameter | Type |
|---|---|
auth | { token?: Record<string, unknown>; uid: string; } |
auth.token? | Record<string, unknown> |
auth.uid | string |
Returns
Example
const sandbox = initializeSandbox();
const dbAlice = getFirestore(sandbox.withAuth({ uid: 'alice' }));
const dbAnon = getFirestore(sandbox.withAuth(null));
Inherited from
RemoteAuthAdmin
Admin auth user-CRUD passthrough (never lensed — auth ops operate the
worker’s user pool directly, mirroring pyric/auth’s sandbox ops).
Methods
clearUsers()
clearUsers(): Promise<void>;
Returns
Promise<void>
createUser()
createUser(request: CreateUserRequest): Promise<AuthUserRecord>;
Parameters
| Parameter | Type |
|---|---|
request | CreateUserRequest |
Returns
Promise<AuthUserRecord>
deleteUser()
deleteUser(uid: string): Promise<void>;
Parameters
| Parameter | Type |
|---|---|
uid | string |
Returns
Promise<void>
listUsers()
listUsers(): Promise<AuthUserRecord[]>;
Returns
Promise<AuthUserRecord[]>
updateUser()
updateUser(uid: string, request: UpdateUserRequest): Promise<AuthUserRecord>;
Parameters
| Parameter | Type |
|---|---|
uid | string |
request | UpdateUserRequest |
Returns
Promise<AuthUserRecord>
RemoteRtdb
Thin RTDB conveniences over the channel. Every call pins
actAs: { mode: 'admin' } — firebase-admin’s rules-bypass semantics,
matching what pyric-admin’s database backend needs. Use the raw
channel for lensed (rules-evaluated) access.
Methods
get()
get(path: string): Promise<unknown>;
Read the value at path (null when absent).
Parameters
| Parameter | Type |
|---|---|
path | string |
Returns
Promise<unknown>
onValue()
onValue(
path: string,
callback: (snapshot: RemoteRtdbSnapshot) => void,
onError?: (err: Error & {
code: string;
}) => void): () => void;
Subscribe to the value at path (initial snapshot + every change).
Parameters
| Parameter | Type |
|---|---|
path | string |
callback | (snapshot: RemoteRtdbSnapshot) => void |
onError? | (err: Error & { code: string; }) => void |
Returns
(): void;
Returns
void
push()
push(path: string, value?: unknown): Promise<{
key: string;
path: string;
}>;
Push value under a CLIENT-minted 20-char push id (the worker-protocol
contract: rtdb.push carries the key, so .key is known synchronously
on the pyric-admin side). Resolves with the minted key + full path.
Parameters
| Parameter | Type |
|---|---|
path | string |
value? | unknown |
Returns
Promise<{
key: string;
path: string;
}>
remove()
remove(path: string): Promise<void>;
Parameters
| Parameter | Type |
|---|---|
path | string |
Returns
Promise<void>
set()
set(path: string, value: unknown): Promise<void>;
Parameters
| Parameter | Type |
|---|---|
path | string |
value | unknown |
Returns
Promise<void>
update()
update(path: string, values: Record<string, unknown>): Promise<void>;
Parameters
| Parameter | Type |
|---|---|
path | string |
values | Record<string, unknown> |
Returns
Promise<void>
RemoteRtdbSnapshot
Wire shape of an RTDB snapshot as the worker host serializes it.
Properties
RemoteSandbox
The Node-side remote sandbox handle. Extends pyric/sandbox’s branded
RemoteSandboxBase — structurally a full Sandbox, so it can be
passed to pyric-admin/app’s initializeApp({ sandbox }), whose RTDB and
Auth backends dispatch on the brand and route through channel.
Sandbox members that are genuinely sync-only (admin, snapshot(),
history(), onEvent, currentUser, …) cannot be mirrored over the
wire in slice 1 and throw a remediating unimplemented error naming
what to do instead. Implemented members: withAuth (pure local pair
construction) and dispose (aliases close).
Extends
Extended by
Properties
| Property | Modifier | Type | Description | Overrides |
|---|---|---|---|---|
[REMOTE_SANDBOX] | readonly | true | - | - |
admin | readonly | SandboxAdmin | Admin-plane access (rule-bypass reads). Identity-agnostic by design — admin reads aren’t gated on auth, so they live on the sandbox, not on a context. See SandboxAdmin. | - |
auth | readonly | RemoteAuthAdmin | Admin auth user CRUD. | - |
channel | readonly | RemoteSandboxChannel | The raw worker op/sub relay channel (narrowed to the wire payload types). | RemoteSandbox.channel |
currentUser | public | { token?: Record<string, unknown>; uid: string; } | Current authenticated user across the sandbox. Mutated by pyric/auth’s signInAnonymously / signInWithEmailAndPassword / signOut / sandbox.setUser. Read per-call by service factories (e.g. a future getFirestore(sandbox) overload) so they see auth state changes without re-binding handles. Defaults to null (anonymous / signed out). Independent of withAuth({uid}) — withAuth still produces a frozen SandboxContext that carries its own identity for the runner’s test code (the existing pattern: explicit identity per service call). currentUser exists for the pyric/auth mirror, where consumer app code drives identity through a stateful Auth handle rather than naming it per call. | - |
currentUser.token? | public | Record<string, unknown> | - | - |
currentUser.uid | public | string | - | - |
rtdb | readonly | RemoteRtdb | RTDB conveniences (admin lens pinned). | - |
serveUrl | readonly | string | Base URL of the pyric dev this handle is attached to (used in error guidance: “open | - |
storage | readonly | RemoteStorage | Storage conveniences (admin lens pinned; 8 MiB per-op byte cap). | - |
Methods
clearPersistence()
clearPersistence(): Promise<void>;
Wipe the persisted blob for this sandbox’s key. In-memory state
is left intact — call reset() if you want both. Useful for
“sign out and forget” flows.
No-op when persistence is not enabled.
Returns
Promise<void>
Inherited from
RemoteSandbox.clearPersistence
close()
close(): void;
Close the connection. In-flight ops reject; subscriptions stop.
Returns
void
dispose()
dispose(): void;
Tear down listener registries on this sandbox’s environment without
replacing it. Use this when you’re about to discard the sandbox
itself (e.g. runner.reseed() builds a fresh sandbox rather than
calling reset()) and want to drop callback references on the
outgoing instance defensively. Idempotent. Does not touch data.
Returns
void
Inherited from
enablePersistence()
enablePersistence(options: SandboxPersistenceOptions): Promise<void>;
Persist the sandbox’s data to a backend and restore it on next
enablePersistence call. The default 'indexedDB' backend turns
the sandbox into the host page’s local Firestore — writes flush
automatically and a fresh initializeSandbox() rehydrates from
the prior session.
Restoration happens before the promise resolves; awaiting this call is sufficient to guarantee in-memory state matches the persisted blob.
Idempotent across the same key — calling twice in one process
is a no-op on the second call. Different keys are rejected as an
error (a sandbox can persist to at most one backend at a time).
Listener semantics: every write event the sandbox emits triggers
a debounced flush (default 250ms). Browser hosts additionally
flush on beforeunload so a page navigation doesn’t lose the
tail of the debounce window.
See SandboxPersistenceOptions for backend selection and tuning.
Parameters
| Parameter | Type |
|---|---|
options | SandboxPersistenceOptions |
Returns
Promise<void>
Inherited from
RemoteSandbox.enablePersistence
enableTabSync()
enableTabSync(options?: TabSyncOptions): () => void;
Enable cross-tab realtime sync via BroadcastChannel. A write in
this tab will propagate to every OTHER tab of the same origin that
also called enableTabSync, causing their onSnapshot listeners to
re-evaluate — restoring production’s cross-client realtime behavior.
Opt-in, OFF by default. Firestore only (RTDB is a follow-on).
Returns a disable function. Calling it removes the onEvent
subscription, the channel message listener, and closes the channel
(when it was created internally). After disable, no further propagation
occurs in either direction.
Multi-writer note: concurrent writes from two tabs to the same doc produce last-write-wins divergence — there is no conflict resolution. The intended model is one active writer (one user, one tab) with observers in other tabs; this covers the overwhelming majority of local development scenarios.
Parameters
| Parameter | Type |
|---|---|
options? | TabSyncOptions |
Returns
(): void;
Returns
void
See
TabSyncOptions for channel injection (tests) and originId.
Example
// In every tab that should participate in realtime:
const sandbox = initializeSandbox();
const disableSync = sandbox.enableTabSync();
// Later, to stop syncing:
disableSync();
Inherited from
flush()
flush(): Promise<void>;
Force a snapshot to the configured persistence backend right now. Useful before a manual navigation, or in tests that need deterministic ordering against the debounce window. Resolves once the write hits the backend.
Throws if persistence is not enabled.
Returns
Promise<void>
Inherited from
history()
history(): SandboxEvent[];
Every SandboxEvent this sandbox has emitted since init or
the last reset(). Returns a defensive copy.
Use this for replay: hand the array to replay(events, rules)
from pyric/sandbox and the engine re-issues every
captured write against a fresh sandbox.
Unlike onEvent (live stream from the moment of subscribe),
history() returns every event the sandbox has seen — useful
for consumers that attach late (e.g., loading a saved session
before subscribing) or that need a snapshot at a particular moment.
reset() and dispose() each append a closing session_boundary
event; reset() then clears the history. Consumers that took a
snapshot before reset retain the boundary in their copy.
Returns
Inherited from
loadSnapshot()
loadSnapshot(data: SandboxSnapshot): void;
CLOBBER-restore the sandbox’s entire state from a prior snapshot:
reset() (clears firestore + the signed-in session), then rebuild firestore
from data and restore each registered service. This is a TOTAL replace —
documents absent from data do NOT survive — and is the counterpart to
snapshot. It is what makes “transfer (clobber) one instance’s data
into another” and named-branch switching possible.
Fires a session_boundary (reset phase), re-evaluates live listeners against
the loaded state, and the next persistence flush writes the loaded state.
Services present in data but not currently registered are skipped (a
snapshot taken via snapshot always includes every registered
service, so this only affects cross-instance imports from a sandbox that had
a service this one lacks).
Parameters
| Parameter | Type |
|---|---|
data | SandboxSnapshot |
Returns
void
Inherited from
onCurrentUserChanged()
onCurrentUserChanged(cb: (user: {
token?: Record<string, unknown>;
uid: string;
}) => void): () => void;
Subscribe to currentUser changes. Fires on every mutation —
sign-in, sign-out, user swap. Does NOT fire on subscribe.
Survives reset() and dispose() only as a no-op: a disposed
sandbox emits nothing further; a reset sandbox clears
currentUser to null (and fires the change) before swapping
the env.
Returns an unsubscribe function. Listener errors are swallowed — subscribers are observational, the sandbox does not propagate their errors.
Parameters
| Parameter | Type |
|---|---|
cb | (user: { token?: Record<string, unknown>; uid: string; }) => void |
Returns
(): void;
Returns
void
Inherited from
RemoteSandbox.onCurrentUserChanged
onEvent()
onEvent(cb: (event: SandboxEvent) => void): () => void;
Subscribe to every event the sandbox emits — see SandboxEvent
for the discriminated-union shape. One subscription covers
request/denial/snapshot-error/listener-lifecycle/session-boundary;
filter on event.kind to recover individual streams.
Replaces the prior three-channel surface (onRequest / onDenial
/ onSnapshotError) — see issue #307. Filter cookbook:
- All denials:
event.kind === 'request' && event.result === 'deny' - Stream errors:
event.kind === 'listener_errored' - Per-op traffic:
event.kind === 'request'
Survives sandbox.reset() — the subscription is held on the
sandbox, not on the underlying environment. A session_boundary
event with phase: 'reset' fires before the env swap so consumers
can segment their stream.
Returns an unsubscribe function. Listener errors are swallowed so a faulty subscriber can’t change rule semantics or hide other events. Both synchronous throws and rejected Promises from async callbacks are silently discarded — subscribers are observational, the sandbox doesn’t await them and doesn’t propagate their errors.
Parameters
| Parameter | Type |
|---|---|
cb | (event: SandboxEvent) => void |
Returns
(): void;
Returns
void
Inherited from
registerPersistableService()
registerPersistableService(name: string, hooks: PersistableService): () => void;
Register a service (auth, storage, …) as a persistence participant.
The sandbox calls hooks.snapshot() on every flush and
hooks.restore(data) on restore. If hooks.subscribe is provided,
the persistence controller subscribes and schedules a debounced
flush on each change — so auth-user edits flush promptly, not only
on the next Firestore write.
Returns an unregister function — call it if the service is torn
down before the sandbox is disposed (uncommon in practice; the
sandbox’s dispose() clears the registry anyway).
Throws failed-precondition when a service with the same name is
already registered — the auth package registers 'auth' once when
getAuth(sandbox) first creates a backend, so accidental double-
registration is a caller bug, not a no-op.
Advanced / internal API. Service packages (auth, storage) call this when they first attach to a sandbox. Consumer app code should not need to call this directly.
Parameters
| Parameter | Type |
|---|---|
name | string |
hooks | PersistableService |
Returns
(): void;
Returns
void
Inherited from
RemoteSandbox.registerPersistableService
reset()
reset(): void;
Reset the underlying environment to a fresh state — wipes data, rules, and any service-specific configuration.
Snapshot listeners attached to the OLD environment are dropped at
the swap — they can’t survive because their target docs have been
wiped. onEvent subscribers DO survive — the registry lives on
the sandbox, and a session_boundary event with phase: 'reset'
fires before the swap so subscribers know the rollover happened.
Existing SandboxContexts continue to work — their sandbox
reference is stable; subsequent operations resolve to the new env.
Returns
void
Inherited from
resetAll()
resetAll(): Promise<{
errors: string[];
}>;
Reset the WHOLE sandbox: reset (Firestore env + signed-in
session), then clear every registered persistable service that
provides a PersistableService.reset hook — auth users, the
RTDB tree, storage objects. This is the one sandbox-owned “wipe
everything” path: because it iterates the service registry, a new
service that registers with a reset hook is cleared automatically,
and a consumer (Pyric Studio’s reset) cannot forget one.
Service resets may be async (storage clears IndexedDB stores); the
returned promise resolves when every service has finished clearing.
A service whose reset throws is isolated (others still clear) and
REPORTED in the returned errors (as name: message) — a reset that
leaves data behind must never look successful to the caller.
Returns
Promise<{
errors: string[];
}>
Inherited from
runWithProvenance()?
optional runWithProvenance<T>(provenance: EventProvenance, fn: () => T): T;
Run fn with ambient EventProvenance defaults: every event
emitted SYNCHRONOUSLY during fn that doesn’t already carry a
provenance field (on the event itself or via an explicit per-emit
override) is stamped with these values instead of the global
defaults. This is the mechanical “who issued this op” seam the
serve worker uses to tag Studio-issued ops (actor: { kind: 'studio' }) and to stamp the auth lens an op ran under
(authLens) — declared by the caller that issues the op, never
inferred from the op’s shape.
SYNCHRONOUS WINDOW: the ambient values apply only until fn
returns (for an async fn, its synchronous prefix — which covers
the local environment’s rules eval + event emission, since those
run before the op’s promise is handed back). Work an op DEFERS
(snapshot-listener deliveries and re-evals drain on a microtask,
off-stack) is intentionally OUTSIDE the window: a listener re-eval
belongs to the listener’s owner, not to whoever’s write triggered
it. Nested calls stack — the innermost window wins per field, and
each window restores the previous one on exit (including on throw).
OPTIONAL because remote sandbox proxies can’t provide an ambient
emit window (events are emitted in the worker they front). Callers
spell sandbox.runWithProvenance?.(prov, fn) ?? fn().
Type Parameters
| Type Parameter |
|---|
T |
Parameters
| Parameter | Type |
|---|---|
provenance | EventProvenance |
fn | () => T |
Returns
T
Inherited from
RemoteSandbox.runWithProvenance
snapshot()
snapshot(): SandboxSnapshot;
Capture a snapshot of every service’s state. For v1 with only
Firestore, the return value carries a firestore key mapping doc
paths to data. Future services will add their own keys.
Returns
Inherited from
withAuth()
withAuth(auth: {
token?: Record<string, unknown>;
uid: string;
}): SandboxContext;
Derive a context bound to this sandbox under the given auth identity. Operations through services attached to the returned context evaluate rules under that identity. Many contexts can coexist for one sandbox; data is shared.
null is anonymous; an AuthState object names the user (and
optional custom claims). Passing undefined is a deliberate
error — say withAuth(null) for anonymous so the call site is
unambiguous.
Parameters
| Parameter | Type |
|---|---|
auth | { token?: Record<string, unknown>; uid: string; } |
auth.token? | Record<string, unknown> |
auth.uid | string |
Returns
Example
const sandbox = initializeSandbox();
const dbAlice = getFirestore(sandbox.withAuth({ uid: 'alice' }));
const dbAnon = getFirestore(sandbox.withAuth(null));
Inherited from
RemoteSandboxChannel
The raw relay channel: any worker-protocol op or snap-delivering
subscription, verbatim. The typed conveniences below are built on this;
checkpoint 2’s pyric-admin remote-dispatch arm consumes it directly.
Methods
op()
op(op: WorkerOpPayload): Promise<unknown>;
Dispatch one worker op. Resolves with the worker’s res.value;
rejects with an Error carrying .code. NOTE: callers choose their own
actAs lens — nothing is pinned here.
Parameters
| Parameter | Type |
|---|---|
op | WorkerOpPayload |
Returns
Promise<unknown>
subscribe()
subscribe(
sub: WorkerSubPayload,
onSnap: (value: unknown) => void,
onError?: (err: Error & {
code: string;
}) => void): () => void;
Register a worker subscription. onSnap receives each snap value;
an establishment failure (the worker host’s { __error } snap) routes
to onError instead. Returns the unsubscribe function.
Parameters
| Parameter | Type |
|---|---|
sub | WorkerSubPayload |
onSnap | (value: unknown) => void |
onError? | (err: Error & { code: string; }) => void |
Returns
(): void;
Returns
void
RemoteSandboxCore
Properties
| Property | Type | Description |
|---|---|---|
channel | RemoteSandboxChannel | - |
ready | Promise<void> | Resolves on attach-ack; rejects when no browser tab is connected. |
Methods
dispose()
dispose(reason?: string): void;
Fail everything in flight (transport closed). Idempotent.
Parameters
| Parameter | Type |
|---|---|
reason? | string |
Returns
void
handleMessage()
handleMessage(msg: BridgeMessage): void;
Feed one parsed message from the transport into the core.
Parameters
| Parameter | Type |
|---|---|
msg | BridgeMessage |
Returns
void
start()
start(): void;
Send the attach handshake. ready settles on the ack.
Returns
void
RemoteStorage
Thin Storage conveniences over the channel — the byte-carrying base64 ops
plus browse/metadata. Every call pins actAs: { mode: 'admin' }
(firebase-admin’s rules-bypass semantics, matching RemoteRtdb);
use the raw channel for lensed (rules-evaluated) access. Bytes are
capped at 8 MiB raw (MAX_STORAGE_OP_BYTES) on both ends —
streaming transfers are not supported on the sandbox backend.
Methods
deleteObject()
deleteObject(path: string): Promise<void>;
Delete the object at path. Idempotent (missing = no-op).
Parameters
| Parameter | Type |
|---|---|
path | string |
Returns
Promise<void>
exists()
exists(path: string): Promise<boolean>;
Does an object exist at path? (getMetadata with not-found → false.)
Parameters
| Parameter | Type |
|---|---|
path | string |
Returns
Promise<boolean>
getBytes()
getBytes(path: string): Promise<Buffer<ArrayBufferLike>>;
Download the object’s bytes. Rejects storage/object-not-found when
absent, payload-too-large when over the op cap.
Parameters
| Parameter | Type |
|---|---|
path | string |
Returns
Promise<Buffer<ArrayBufferLike>>
getMetadata()
getMetadata(path: string): Promise<FullMetadata>;
Read the object’s FullMetadata.
Parameters
| Parameter | Type |
|---|---|
path | string |
Returns
Promise<FullMetadata>
listAll()
listAll(path: string): Promise<{
items: {
fullPath: string;
name: string;
}[];
prefixes: {
fullPath: string;
name: string;
}[];
}>;
Enumerate immediate child items + prefixes under path.
Parameters
| Parameter | Type |
|---|---|
path | string |
Returns
Promise<{
items: {
fullPath: string;
name: string;
}[];
prefixes: {
fullPath: string;
name: string;
}[];
}>
putBytes()
putBytes(
path: string,
data: Uint8Array,
options?: {
contentType?: string;
metadata?: Record<string, unknown>;
}): Promise<FullMetadata>;
Upload data at path (replaces any existing object). Resolves with
the stored object’s FullMetadata.
Parameters
| Parameter | Type |
|---|---|
path | string |
data | Uint8Array |
options? | { contentType?: string; metadata?: Record<string, unknown>; } |
options.contentType? | string |
options.metadata? | Record<string, unknown> |
Returns
Promise<FullMetadata>
RemoteTransport
Minimal transport the core writes to. connectRemoteSandbox adapts a
ws socket; tests inject an in-process pipe to a ConsumerSession.
Methods
ref()?
optional ref(): void;
OPTIONAL event-loop hold hooks (exit-hang fix). The WS adapter unrefs
its socket once connected so an IDLE remote client never pins the Node
event loop (a finished script exits); the core calls ref() when work
becomes outstanding (first pending op / live subscription) and
unref() when the last one settles, so in-flight delivery keeps the
process alive. Pure in-process transports (tests) may omit both.
Returns
void
send()
send(msg: BridgeMessage): void;
Parameters
| Parameter | Type |
|---|---|
msg | BridgeMessage |
Returns
void
unref()?
optional unref(): void;
Returns
void
Functions
buildRemoteAuthAdmin()
function buildRemoteAuthAdmin(channel: RemoteSandboxChannel): RemoteAuthAdmin;
Parameters
| Parameter | Type |
|---|---|
channel | RemoteSandboxChannel |
Returns
buildRemoteRtdb()
function buildRemoteRtdb(channel: RemoteSandboxChannel): RemoteRtdb;
Parameters
| Parameter | Type |
|---|---|
channel | RemoteSandboxChannel |
Returns
buildRemoteStorage()
function buildRemoteStorage(channel: RemoteSandboxChannel): RemoteStorage;
Parameters
| Parameter | Type |
|---|---|
channel | RemoteSandboxChannel |
Returns
connectRemoteSandbox()
function connectRemoteSandbox(options?: ConnectRemoteSandboxOptions): Promise<RemoteSandbox>;
Discover the running pyric dev --bridge, attach to its bridge WS as a
worker-relay CONSUMER (never a peer — attaching cannot kick the browser
tab out of last-connection-wins), and return the typed remote handle.
Fails fast when no serve is discoverable or no browser tab is connected — there is deliberately no headless fallback (see module doc).
Parameters
| Parameter | Type |
|---|---|
options? | ConnectRemoteSandboxOptions |
Returns
Promise<RemoteSandbox>
createLazyRemoteSandbox()
function createLazyRemoteSandbox(connect: () => Promise<RemoteSandbox>, options?: {
url?: string;
}): LazyRemoteSandbox;
The lazy wrapper with the connect function injected — the test seam
(tests inject a fake connect; production injects connectRemoteSandbox).
Parameters
| Parameter | Type |
|---|---|
connect | () => Promise<RemoteSandbox> |
options? | { url?: string; } |
options.url? | string |
Returns
createRemoteSandboxCore()
function createRemoteSandboxCore(transport: RemoteTransport, opts: {
opTimeoutMs?: number;
serveUrl: string;
}): RemoteSandboxCore;
Transport-agnostic client core: correlation ids/subIds are minted HERE
(this leg’s id space; the bridge re-mints for the peer leg), pending ops
carry a Node-side timeout above the bridge’s 30s, and { __error } snap
values are routed to the subscription’s error handler.
Parameters
| Parameter | Type |
|---|---|
transport | RemoteTransport |
opts | { opTimeoutMs?: number; serveUrl: string; } |
opts.opTimeoutMs? | number |
opts.serveUrl | string |
Returns
createRemoteSandboxHandle()
function createRemoteSandboxHandle(opts: {
channel: RemoteSandboxChannel;
close: () => void;
serveUrl: string;
}): RemoteSandbox;
Build the branded remote sandbox handle over an established channel.
Split out of connectRemoteSandbox so the in-process test harness
(fake ports + createConsumerSession, no WS) constructs the EXACT handle
production hands to pyric-admin.
The handle satisfies Sandbox structurally:
withAuth/disposeare real (pure local construction / teardown).- Everything whose contract is sync-only or worker-owned (
admin,snapshot,loadSnapshot,history,onEvent,reset,currentUser,onCurrentUserChanged, tab sync, persistence) throws a remediatingunimplementederror. NotablyonEvent: the unified event stream (target: 'events') is not relayable until slice 2’s bounded backpressure lands, and no remote dispatch arm may depend on it — a throw (not a silent no-op) keeps a subscriber from believing it is observing events that will never arrive.
Parameters
| Parameter | Type |
|---|---|
opts | { channel: RemoteSandboxChannel; close: () => void; serveUrl: string; } |
opts.channel | RemoteSandboxChannel |
opts.close | () => void |
opts.serveUrl | string |
Returns
remoteSandbox()
function remoteSandbox(options?: ConnectRemoteSandboxOptions): LazyRemoteSandbox;
Synchronous construction, lazy connection — the ambient-init seam.
@pyric/cli/register installs this behind the
Symbol.for('pyric.remote.sandboxFactory') global so pyric-admin’s bare
initializeApp() can mint a full branded handle without awaiting anything.
The wire connection (discovery → WS attach) happens on the FIRST op (or
ready access), so the existing fail-fast — “no browser tab is connected —
open connectRemoteSandbox (eager) is unchanged.
Parameters
| Parameter | Type |
|---|---|
options? | ConnectRemoteSandboxOptions |