@mmstack/worker 21.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mmstack-worker.mjs","sources":["../../../../packages/worker/src/lib/connect-worker.ts","../../../../packages/worker/src/lib/worker-resource.ts","../../../../packages/worker/src/lib/worker-store.ts","../../../../packages/worker/src/index.ts","../../../../packages/worker/src/mmstack-worker.ts"],"sourcesContent":["import {\n DestroyRef,\n inject,\n Injector,\n PLATFORM_ID,\n runInInjectionContext,\n signal,\n type Signal,\n} from '@angular/core';\nimport {\n closePort,\n deserializeError,\n generateId,\n takeTransferables,\n type HasSchema,\n type SchemaOf,\n type TaskInput,\n type TaskOutput,\n type WorkerEnvelope,\n type WorkerPortLike,\n type WorkerSchema,\n} from '@mmstack/worker/protocol';\n\n/** What the worker advertised in its `ready` handshake. */\nexport type WorkerManifest = {\n readonly hostId: string;\n readonly stores: readonly string[];\n readonly published: readonly string[];\n readonly tasks: readonly string[];\n};\n\n/** Thrown into a task/write promise when it is aborted. `name` is `'AbortError'` (DOM convention). */\nexport class WorkerAbortError extends Error {\n constructor(message = 'The worker task was aborted') {\n super(message);\n this.name = 'AbortError';\n }\n}\n\n/** Thrown into pending tasks/writes when the worker crashes. `name` is `'WorkerCrashedError'`. */\nexport class WorkerCrashedError extends Error {\n constructor(message = 'The worker crashed') {\n super(message);\n this.name = 'WorkerCrashedError';\n }\n}\n\nexport type ConnectWorkerOptions = {\n readonly injector?: Injector;\n /** `'auto'` (default) respawns on crash; `'manual'` surfaces disconnect and stops. */\n readonly restart?: 'auto' | 'manual';\n /** Backoff before respawn attempt `n` (0-based). Default exponential 1s→30s. */\n readonly restartDelay?: (attempt: number) => number;\n};\n\nexport type WorkerRef<M extends WorkerSchema = WorkerSchema> = HasSchema<M> & {\n /** True once the `ready` handshake has completed (and again after an auto-restart). */\n readonly connected: Signal<boolean>;\n /** The worker's advertised manifest, or `null` before the first `ready`. */\n readonly manifest: Signal<WorkerManifest | null>;\n /** Run a named task exposed by the worker host; resolves with its result (typed from the schema). */\n runTask<K extends keyof M['tasks'] & string>(\n task: K,\n input: TaskInput<M, K>,\n opt?: { signal?: AbortSignal; transfer?: Transferable[] },\n ): Promise<TaskOutput<M, K>>;\n destroy(): void;\n\n // ── internal seam for workerStore (phase 4b) — not part of the friendly surface ──\n /** @internal Post an envelope to the worker. */\n _send(msg: WorkerEnvelope, transfer?: Transferable[]): void;\n /** @internal Receive every non-task envelope (store traffic). Returns an unsubscribe. */\n _subscribe(handler: (msg: WorkerEnvelope) => void): () => void;\n /** @internal Run `fn` on every (re)connection — used to re-subscribe stores after a restart. */\n _onReady(fn: () => void): () => void;\n /** @internal Run `fn` when the connection drops (crash) — replicas reject pending writes here. */\n _onDisconnect(fn: () => void): () => void;\n /** @internal This client's identity on the transport. */\n readonly clientId: string;\n};\n\n/**\n * Connects the main thread to a worker over a {@link WorkerPortLike} the caller provides via `spawn`\n * — typically `() => new Worker(new URL('./my.worker', import.meta.url), { type: 'module' })` (the\n * URL literal must live in APP code so the bundler emits the worker chunk). Performs the hello/ready\n * handshake, exposes `connected`/`manifest`, routes named-task runs, and (via internal seams)\n * carries the store-replication traffic for {@link workerStore}. On crash it respawns with backoff\n * when `restart: 'auto'`.\n */\nexport function connectWorker<H = WorkerSchema>(\n spawn: () => WorkerPortLike,\n opt?: ConnectWorkerOptions,\n): WorkerRef<SchemaOf<H>> {\n const injector = opt?.injector ?? inject(Injector);\n return runInInjectionContext(injector, () =>\n build(spawn, opt),\n ) as unknown as WorkerRef<SchemaOf<H>>;\n}\n\nfunction build(\n spawn: () => WorkerPortLike,\n opt?: ConnectWorkerOptions,\n): WorkerRef {\n const isServer = inject(PLATFORM_ID) === 'server';\n const clientId = generateId();\n const connected = signal(false);\n const manifest = signal<WorkerManifest | null>(null);\n\n const taskPending = new Map<number, { resolve: (v: any) => void; reject: (e: unknown) => void }>();\n const storeHandlers = new Set<(msg: WorkerEnvelope) => void>();\n const readyHooks = new Set<() => void>();\n const disconnectHooks = new Set<() => void>();\n const restart = opt?.restart ?? 'auto';\n const restartDelay = opt?.restartDelay ?? ((n: number) => Math.min(30_000, 1000 * 2 ** n));\n let attempt = 0;\n let runIdSeq = 0;\n let destroyed = false;\n let crashed = false; // between a crash and the next successful handshake\n let port: WorkerPortLike = null as unknown as WorkerPortLike;\n\n const send = (msg: WorkerEnvelope, transfer?: Transferable[]): void => {\n if (port) port.postMessage(msg, transfer); // no-op on the server / between crash and respawn\n };\n\n const onMessage = (msg: WorkerEnvelope): void => {\n switch (msg.type) {\n case 'ready':\n attempt = 0; // a successful handshake resets the backoff\n crashed = false;\n manifest.set({\n hostId: msg.hostId,\n stores: msg.stores,\n published: msg.published,\n tasks: msg.tasks,\n });\n connected.set(true);\n for (const hook of readyHooks) hook();\n return;\n case 'task:ok': {\n const p = taskPending.get(msg.runId);\n if (p) {\n taskPending.delete(msg.runId);\n p.resolve(msg.value);\n }\n return;\n }\n case 'task:error': {\n const p = taskPending.get(msg.runId);\n if (p) {\n taskPending.delete(msg.runId);\n p.reject(deserializeError(msg.error));\n }\n return;\n }\n case 'task:aborted':\n taskPending.delete(msg.runId); // caller already rejected at abort\n return;\n default:\n // store:* traffic → replicas\n for (const h of storeHandlers) h(msg);\n }\n };\n\n const handleCrash = (): void => {\n if (destroyed) return;\n crashed = true;\n connected.set(false);\n // in-flight tasks can't survive a crash; store replicas hold their value and re-hydrate on reconnect\n for (const [, p] of taskPending) p.reject(new WorkerCrashedError());\n taskPending.clear();\n for (const fn of disconnectHooks) fn();\n if (restart === 'manual') return;\n const delay = restartDelay(attempt++);\n setTimeout(() => {\n if (!destroyed) openPort();\n }, delay);\n };\n\n const openPort = (): void => {\n // terminate the previous worker before respawning, so a transient/manual restart never orphans\n // a live thread (a genuine crash already killed it; terminate() is then a harmless no-op)\n if (port) closePort(port);\n port = spawn();\n port.onmessage = (ev) => onMessage(ev.data as WorkerEnvelope);\n // duck-typed crash detection via property setters (NOT addEventListener — that perturbs a node\n // MessagePort's onmessage delivery). A real `Worker` has onerror; onmessageerror where present.\n const evt = port as unknown as {\n onerror?: ((e?: unknown) => void) | null;\n onmessageerror?: ((e?: unknown) => void) | null;\n };\n if ('onerror' in evt) evt.onerror = handleCrash;\n if ('onmessageerror' in evt) evt.onmessageerror = handleCrash;\n send({ type: 'hello', proto: 1, clientId });\n };\n\n // no workers on the server — connected stays false, runTask rejects, replicas render their default\n if (!isServer) openPort();\n\n inject(DestroyRef).onDestroy(() => teardown());\n\n const teardown = (): void => {\n destroyed = true;\n connected.set(false);\n for (const [, p] of taskPending) p.reject(new WorkerAbortError('worker connection destroyed'));\n taskPending.clear();\n // same contract as a crash: anything pending on the connection (replica writes) settles NOW\n // rather than dangling — a destroyed port can never deliver the echo that would resolve them\n for (const fn of disconnectHooks) fn();\n if (port) closePort(port);\n };\n\n return {\n connected,\n manifest,\n clientId,\n runTask(\n task: string,\n input: unknown,\n o?: { signal?: AbortSignal; transfer?: Transferable[] },\n ): Promise<unknown> {\n if (isServer) return Promise.reject(new WorkerCrashedError('no worker on the server'));\n if (destroyed) return Promise.reject(new WorkerAbortError('worker connection destroyed'));\n // between a crash and the respawned handshake the port is dead — a message posted into it\n // vanishes and the promise would never settle. Reject honestly; the caller retries/reloads.\n // (Before the FIRST handshake this is false: a starting worker buffers messages, so we send.)\n if (crashed) return Promise.reject(new WorkerCrashedError('worker is restarting'));\n if (o?.signal?.aborted) return Promise.reject(new WorkerAbortError());\n const runId = ++runIdSeq;\n return new Promise<unknown>((resolve, reject) => {\n taskPending.set(runId, { resolve, reject });\n send(\n { type: 'task:run', runId, task, input },\n o?.transfer ?? takeTransferables(input),\n );\n o?.signal?.addEventListener(\n 'abort',\n () => {\n if (!taskPending.has(runId)) return;\n taskPending.delete(runId);\n send({ type: 'task:abort', runId });\n reject(new WorkerAbortError());\n },\n { once: true },\n );\n });\n },\n destroy: teardown,\n _send: send,\n _subscribe(handler) {\n storeHandlers.add(handler);\n return () => storeHandlers.delete(handler);\n },\n _onReady(fn) {\n readyHooks.add(fn);\n return () => readyHooks.delete(fn);\n },\n _onDisconnect(fn) {\n disconnectHooks.add(fn);\n return () => disconnectHooks.delete(fn);\n },\n };\n}\n","import {\n computed,\n DestroyRef,\n effect,\n inject,\n Injector,\n PLATFORM_ID,\n runInInjectionContext,\n signal,\n untracked,\n type ResourceStatus,\n type Signal,\n type ValueEqualityFn,\n} from '@angular/core';\nimport { injectTransitionScope } from '@mmstack/primitives';\nimport { WorkerAbortError, type WorkerRef } from './connect-worker';\n\n/** Returned from a paused `params` fn to HOLD (keep the current value/status, run nothing). */\nexport const PAUSED: unique symbol = Symbol('@mmstack/worker:PAUSED');\n\nexport type WorkerRequestContext = { readonly paused: typeof PAUSED };\n\n/**\n * Reactive input producer. Reads signals to derive the task input; return `undefined` to disable\n * (idle, no run), or `ctx.paused` to hold. Re-runs when the returned input changes (by `Object.is`).\n */\nexport type WorkerParamsFn<TInput> = (\n ctx: WorkerRequestContext,\n) => TInput | undefined | void | typeof PAUSED;\n\nexport type WorkerResourceOptions<TResult> = {\n readonly injector?: Injector;\n /** Equality for the result value — a run resolving equal to the held value emits no notification. */\n readonly equal?: ValueEqualityFn<TResult>;\n readonly defaultValue?: TResult;\n /**\n * Hold the previous value through a re-run (status `'reloading'`) instead of clearing it. Default\n * TRUE — a `workerResource` is an async derivation, not a fetch; flashing empty mid-recompute is\n * rarely wanted.\n */\n readonly keepPrevious?: boolean;\n /** Register into the nearest transition scope: `'indicator'` drives pending, `'suspend'` also gates first paint. */\n readonly register?: false | 'indicator' | 'suspend';\n /** The connected worker whose host exposes the task. */\n readonly worker: WorkerRef;\n /** The name of the task to run (as declared in `createWorkerHost({ tasks })`). */\n readonly task: string;\n};\n\nexport type WorkerResourceRef<T> = {\n /** The latest successfully-computed value (held through re-runs per `keepPrevious`). */\n readonly value: Signal<T | undefined>;\n readonly status: Signal<ResourceStatus>;\n readonly error: Signal<unknown>;\n readonly isLoading: Signal<boolean>;\n hasValue(): boolean;\n /** Re-run with the current input, bypassing input de-duplication. */\n reload(): void;\n /** Cancel the in-flight run; the value is KEPT and status becomes `'local'`. */\n abort(): void;\n destroy(): void;\n};\n\n/**\n * Runs a named task on a connected worker, exposed with the standard resource surface so heavy\n * compute makes the UI *pending*, not *frozen*. `params` reactively derives the input; latest-wins\n * (a changed input supersedes and aborts the in-flight run); the result surfaces as\n * `value`/`status`/`error`, and — with `register` — participates in transition scopes. No-ops on the\n * server.\n */\nexport function workerResource<TInput, TResult>(\n params: WorkerParamsFn<TInput>,\n options: WorkerResourceOptions<TResult>,\n): WorkerResourceRef<TResult> {\n const injector = options.injector ?? inject(Injector);\n return runInInjectionContext(injector, () => build<TInput, TResult>(params, options));\n}\n\nfunction build<TInput, TResult>(\n params: WorkerParamsFn<TInput>,\n options: WorkerResourceOptions<TResult>,\n): WorkerResourceRef<TResult> {\n const isServer = inject(PLATFORM_ID) === 'server';\n const keepPrevious = options.keepPrevious ?? true;\n const userEqual = options.equal;\n const { worker, task } = options;\n\n const runTask = (input: TInput, signal: AbortSignal): Promise<TResult> =>\n worker.runTask(task, input, { signal }) as Promise<TResult>;\n\n const value = signal<TResult | undefined>(options.defaultValue, {\n equal: userEqual\n ? (a, b) => (a === undefined || b === undefined ? a === b : userEqual(a, b))\n : undefined,\n });\n const status = signal<ResourceStatus>('idle');\n const error = signal<unknown>(undefined);\n const isLoading = computed(() => {\n const s = status();\n return s === 'loading' || s === 'reloading';\n });\n const hasValue = () => value() !== undefined;\n\n let epoch = 0;\n let inFlight: AbortController | null = null;\n let lastInput: TInput | undefined;\n let hasRun = false;\n\n const start = (input: TInput): void => {\n const myEpoch = ++epoch;\n inFlight?.abort();\n const ac = new AbortController();\n inFlight = ac;\n lastInput = input;\n hasRun = true;\n status.set(keepPrevious && value() !== undefined ? 'reloading' : 'loading');\n error.set(undefined);\n\n runTask(input, ac.signal).then(\n (result) => {\n if (inFlight === ac) inFlight = null;\n if (myEpoch !== epoch) return; // superseded — discard\n value.set(result);\n status.set('resolved');\n },\n (err) => {\n if (inFlight === ac) inFlight = null;\n if (myEpoch !== epoch) return;\n if (err instanceof WorkerAbortError || (err as { name?: string })?.name === 'AbortError')\n return;\n error.set(err);\n status.set('error');\n },\n );\n };\n\n const input = computed(() => params({ paused: PAUSED }));\n\n const ref = effect(() => {\n const i = input();\n if (isServer || i === PAUSED) return; // server: never run · paused: hold\n if (i === undefined) return; // disabled — keep current value/status\n // dedup: a resume to the same input (e.g. after a pause) does not re-run\n if (Object.is(i, lastInput) && hasRun && untracked(status) !== 'error') return;\n untracked(() => start(i as TInput));\n });\n\n inject(DestroyRef).onDestroy(() => {\n epoch++;\n inFlight?.abort();\n });\n\n const self: WorkerResourceRef<TResult> = {\n value,\n status,\n error,\n isLoading,\n hasValue,\n reload: () => {\n const i = untracked(input);\n if (isServer || i === PAUSED || i === undefined) return;\n start(i as TInput);\n },\n abort: () => {\n if (!inFlight) return; // nothing in flight (a scope's abortPending must not disturb a settled value)\n epoch++;\n inFlight.abort();\n inFlight = null;\n status.set('local'); // value kept\n },\n destroy: () => {\n epoch++;\n inFlight?.abort();\n ref.destroy();\n },\n };\n\n if (options.register) {\n const scope = injectTransitionScope();\n scope.add(self, { suspends: options.register === 'suspend' });\n inject(DestroyRef).onDestroy(() => scope.remove(self));\n }\n\n return self;\n}\n","import {\n computed,\n DestroyRef,\n inject,\n Injector,\n runInInjectionContext,\n signal,\n untracked,\n type ResourceStatus,\n type Signal,\n type WritableSignal,\n} from '@angular/core';\nimport {\n applyOps,\n diffOps,\n injectTransitionScope,\n toStore,\n type SignalStore,\n type WritableSignalStore,\n} from '@mmstack/primitives';\nimport {\n deserializeError,\n type IsWritableKey,\n type StoreKeys,\n type StoreValueOf,\n type WorkerEnvelope,\n type WorkerSchema,\n} from '@mmstack/worker/protocol';\nimport {\n WorkerAbortError,\n WorkerCrashedError,\n type WorkerRef,\n} from './connect-worker';\n\nexport type WorkerStoreOptions<T> = {\n readonly injector?: Injector;\n /** Value the replica holds before the first snapshot arrives. */\n readonly defaultValue?: T;\n /** Register into the nearest transition scope while hydrating/reloading. */\n readonly register?: false | 'indicator' | 'suspend';\n};\n\n/** The write path — present only on OWNED (writable) subtrees; absent on published (read-only) ones. */\nexport type WorkerStoreWrite<T> = {\n /**\n * Route a write to the OWNER: run `recipe` against a writable draft of the current replica, diff\n * it to ops, ship them. Resolves once the owner's authoritative batch carrying this write has been\n * applied to THIS replica (honest async — the value is not applied locally first; for optimistic\n * UI, fork the replica and reconcile on resolve). Rejects if the owner reports a write error.\n */\n write(recipe: (draft: WritableSignalStore<T>) => void): Promise<void>;\n};\n\nexport type WorkerStoreRef<T, W extends boolean = true> = {\n /** The live, READ-ONLY replica — deep per-leaf reads, mirrored from the worker-owned subtree. */\n readonly store: SignalStore<T>;\n /** The replica root value (undefined before hydration). */\n readonly value: Signal<T | undefined>;\n readonly status: Signal<ResourceStatus>;\n readonly error: Signal<unknown>;\n readonly isLoading: Signal<boolean>;\n /** The underlying worker connection's liveness. */\n readonly connected: Signal<boolean>;\n hasValue(): boolean;\n destroy(): void;\n} & (W extends true ? WorkerStoreWrite<T> : object);\n\n/**\n * A live READ-ONLY replica of a store subtree OWNED by the worker. Subscribes over the\n * {@link WorkerRef}, hydrates from the owner's snapshot, then applies each authoritative op batch\n * into a main-thread `store` — one `set` per batch, so a batch of N ops is a single notification\n * wave. Satisfies `ResourceLike`/`UseSource`, so it participates in transition scopes and nests in\n * `latest()`/`use()`. Writes are not local: they route to the owner (added in the next step).\n */\n// manifest-driven: key constrained to the worker's stores/published, value inferred, and `write()`\n// present only for OWNED keys\nexport function workerStore<M extends WorkerSchema, K extends StoreKeys<M>>(\n worker: WorkerRef<M>,\n key: K,\n opt?: WorkerStoreOptions<StoreValueOf<M, K> & object>,\n): WorkerStoreRef<StoreValueOf<M, K> & object, IsWritableKey<M, K>>;\n// explicit-value: for an untyped connection, or to override the inferred type\nexport function workerStore<T extends object>(\n worker: WorkerRef,\n key: string,\n opt?: WorkerStoreOptions<T>,\n): WorkerStoreRef<T, true>;\nexport function workerStore(\n worker: WorkerRef,\n key: string,\n opt?: WorkerStoreOptions<any>,\n): WorkerStoreRef<any, boolean> {\n const injector = opt?.injector ?? inject(Injector);\n return runInInjectionContext(injector, () => build(worker, key, opt));\n}\n\nfunction build<T extends object>(\n worker: WorkerRef,\n key: string,\n opt?: WorkerStoreOptions<T>,\n): WorkerStoreRef<T, true> {\n const injector = inject(Injector); // captured for use in write() (called outside injection context)\n const root = signal<T | undefined>(opt?.defaultValue);\n // shape-adaptive: the store reflects `root` even as it goes undefined → object on first snapshot\n const replica = toStore(root as unknown as WritableSignal<T>, {\n injector,\n }) as SignalStore<T>;\n\n const status = signal<ResourceStatus>('loading');\n const error = signal<unknown>(undefined);\n const isLoading = computed(() => {\n const s = status();\n return s === 'loading' || s === 'reloading';\n });\n let hasSnapshot = false;\n let resyncing = false; // awaiting a fresh snapshot after a detected gap\n let lastVersion = 0;\n let writeSeq = 0;\n const writePending = new Map<\n number,\n { resolve: () => void; reject: (e: unknown) => void; version: number | null }\n >();\n\n const hasValue = () => hasSnapshot;\n\n // resolve any write whose acked authoritative version has now been applied to this replica\n const settleWrites = () => {\n for (const [id, p] of writePending) {\n if (p.version !== null && lastVersion >= p.version) {\n writePending.delete(id);\n p.resolve();\n }\n }\n };\n\n const onStoreMessage = (msg: WorkerEnvelope): void => {\n if (!('store' in msg) || msg.store !== key) return;\n switch (msg.type) {\n case 'store:snapshot': {\n root.set(msg.value as T);\n lastVersion = msg.version;\n hasSnapshot = true;\n resyncing = false; // a fresh snapshot ends any resync\n status.set('resolved');\n error.set(undefined);\n settleWrites();\n return;\n }\n case 'store:ops': {\n if (!hasSnapshot || resyncing) return; // pre-hydration, or dropping until a fresh snapshot\n if (msg.batch.version <= lastVersion) return; // stale/duplicate (at-least-once transport)\n if (msg.batch.version > lastVersion + 1) {\n // GAP: a batch was lost — re-hydrate from a fresh snapshot rather than apply out of order\n resync();\n return;\n }\n root.set(applyOps(untracked(root) as T, msg.batch.ops));\n lastVersion = msg.batch.version;\n settleWrites();\n return;\n }\n case 'store:write:ack': {\n const p = writePending.get(msg.writeId);\n if (!p) return;\n if (lastVersion >= msg.version) {\n writePending.delete(msg.writeId);\n p.resolve(); // the authoritative batch already landed (FIFO: ops precede the ack)\n } else {\n p.version = msg.version; // resolve once a batch reaches this version\n }\n return;\n }\n case 'store:write:error': {\n const p = writePending.get(msg.writeId);\n if (p) {\n writePending.delete(msg.writeId);\n p.reject(deserializeError(msg.error));\n }\n return;\n }\n case 'store:status': {\n // rung 3: a published derivation's remote status → this replica's status, so an in-flight\n // worker computation shows as pending on the main thread (holding the last value)\n switch (msg.status) {\n case 'error':\n error.set(msg.error ? deserializeError(msg.error) : new Error('remote computation error'));\n status.set('error');\n return;\n case 'loading':\n case 'reloading':\n error.set(undefined);\n status.set(hasSnapshot ? 'reloading' : 'loading'); // hold the value while recomputing\n return;\n case 'resolved':\n error.set(undefined);\n if (hasSnapshot) status.set('resolved');\n return;\n default:\n return;\n }\n }\n }\n };\n\n const subscribe = (): void => {\n if (hasSnapshot) status.set('reloading'); // holding stale while the fresh snapshot lands\n worker._send({ type: 'store:subscribe', store: key, clientId: worker.clientId });\n };\n\n // a lost batch left a version gap — hold the stale value and re-subscribe for a fresh snapshot\n const resync = (): void => {\n resyncing = true;\n status.set('reloading');\n worker._send({ type: 'store:subscribe', store: key, clientId: worker.clientId });\n };\n\n const unsub = worker._subscribe(onStoreMessage);\n // subscribe on each `ready` — the first connection and every auto-restart re-hydration; if the\n // connection is ALREADY up (created post-handshake), `_onReady` won't fire, so subscribe now\n const offReady = worker._onReady(subscribe);\n if (untracked(worker.connected)) subscribe();\n // a crash can't be delivered — pending writes reject (the caller holds the value + recipe to retry)\n const offDisconnect = worker._onDisconnect(() => {\n for (const [, p] of writePending) p.reject(new WorkerCrashedError());\n writePending.clear();\n });\n\n const self: WorkerStoreRef<T> = {\n store: replica,\n value: root.asReadonly(),\n status,\n error,\n isLoading,\n connected: worker.connected,\n hasValue,\n write: (recipe) => {\n const base = untracked(root);\n if (base === undefined)\n return Promise.reject(\n new Error('[@mmstack/worker] cannot write before the replica has hydrated'),\n );\n // hydrated but disconnected = the worker crashed (or is restarting) — a write posted now\n // vanishes into a dead port and its echo can never arrive. Reject; the caller retries.\n if (!untracked(worker.connected))\n return Promise.reject(new WorkerCrashedError('worker is not connected'));\n // fork-diff: a scratch store seeded with the CURRENT value (same ref → copy-on-write keeps\n // untouched subtrees shared), so diffOps against the base is O(changed paths)\n const scratch = signal(base);\n const scratchStore = toStore(scratch as unknown as WritableSignal<T>, {\n injector,\n }) as unknown as WritableSignalStore<T>;\n recipe(scratchStore);\n const ops = diffOps(base, untracked(scratch));\n if (!ops.length) return Promise.resolve();\n\n const writeId = ++writeSeq;\n return new Promise<void>((resolve, reject) => {\n writePending.set(writeId, { resolve, reject, version: null });\n worker._send({\n type: 'store:write',\n store: key,\n writeId,\n clientId: worker.clientId,\n ops,\n });\n });\n },\n destroy: () => {\n unsub();\n offReady();\n offDisconnect();\n // acks can no longer be received — settle any in-flight write instead of dangling it\n for (const [, p] of writePending)\n p.reject(new WorkerAbortError('worker store destroyed'));\n writePending.clear();\n worker._send({ type: 'store:unsubscribe', store: key, clientId: worker.clientId });\n },\n };\n\n inject(DestroyRef).onDestroy(() => self.destroy());\n\n if (opt?.register) {\n const scope = injectTransitionScope();\n scope.add(self, { suspends: opt.register === 'suspend' });\n inject(DestroyRef).onDestroy(() => scope.remove(self));\n }\n\n return self;\n}\n","// shared wire types, re-exported so main-thread consumers import only from '@mmstack/worker'\nexport * from '@mmstack/worker/protocol';\n\nexport {\n connectWorker,\n WorkerAbortError,\n WorkerCrashedError,\n type ConnectWorkerOptions,\n type WorkerManifest,\n type WorkerRef,\n} from './lib/connect-worker';\nexport {\n PAUSED,\n workerResource,\n type WorkerParamsFn,\n type WorkerRequestContext,\n type WorkerResourceOptions,\n type WorkerResourceRef,\n} from './lib/worker-resource';\nexport {\n workerStore,\n type WorkerStoreOptions,\n type WorkerStoreRef,\n} from './lib/worker-store';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":["build"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@mmstack/worker",
3
+ "version": "21.0.0",
4
+ "keywords": [
5
+ "angular",
6
+ "signals",
7
+ "web-worker",
8
+ "worker",
9
+ "multithreading",
10
+ "concurrency"
11
+ ],
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/mihajm/mmstack.git",
16
+ "directory": "packages/worker"
17
+ },
18
+ "homepage": "https://github.com/mihajm/mmstack/blob/master/packages/worker",
19
+ "peerDependencies": {
20
+ "@angular/core": ">=21 <22",
21
+ "@angular/common": ">=21 <22",
22
+ "@mmstack/primitives": ">=21.5.1 <22"
23
+ },
24
+ "sideEffects": false,
25
+ "module": "fesm2022/mmstack-worker.mjs",
26
+ "typings": "types/mmstack-worker.d.ts",
27
+ "exports": {
28
+ "./package.json": {
29
+ "default": "./package.json"
30
+ },
31
+ ".": {
32
+ "types": "./types/mmstack-worker.d.ts",
33
+ "default": "./fesm2022/mmstack-worker.mjs"
34
+ },
35
+ "./host": {
36
+ "types": "./types/mmstack-worker-host.d.ts",
37
+ "default": "./fesm2022/mmstack-worker-host.mjs"
38
+ },
39
+ "./protocol": {
40
+ "types": "./types/mmstack-worker-protocol.d.ts",
41
+ "default": "./fesm2022/mmstack-worker-protocol.mjs"
42
+ }
43
+ },
44
+ "type": "module",
45
+ "dependencies": {
46
+ "tslib": "^2.3.0"
47
+ }
48
+ }
@@ -0,0 +1,109 @@
1
+ import { WorkerPortLike, WorkerSchema, HasSchema, SignalValueOf } from '@mmstack/worker/protocol';
2
+ export * from '@mmstack/worker/protocol';
3
+ import { WritableSignal, Signal, ResourceStatus } from '@angular/core';
4
+ import { OpLogDriver, toStoreOptions } from '@mmstack/primitives';
5
+
6
+ /** A named unit of compute a worker exposes (rung 1). `ctx.signal` aborts when the caller cancels. */
7
+ type WorkerTaskHandler<I = any, O = any> = (input: I, ctx: {
8
+ readonly signal: AbortSignal;
9
+ }) => O | Promise<O>;
10
+ /** A published derivation: a plain signal, or a status-bearing one (a `latest()`) for pending propagation. */
11
+ type PublishedSource<T> = Signal<T> | (Signal<T | undefined> & {
12
+ readonly status: Signal<ResourceStatus>;
13
+ });
14
+ type CreateWorkerHostOptions = {
15
+ /**
16
+ * Store subtrees this worker OWNS — the single writer. Keys are the wire identifiers. Typed as
17
+ * the plain writable-signal interface (any copy-on-write `WritableSignal<object>` qualifies, per
18
+ * `opLog`); a concrete `store<T>` satisfies it without the mapped-store variance friction that
19
+ * `WritableSignalStore<any>` would introduce.
20
+ */
21
+ readonly stores?: Record<string, WritableSignal<any>>;
22
+ /**
23
+ * Read-only DERIVED subtrees the worker PUBLISHES (rung 3) — a `Signal<T>` (e.g. a `computed` or
24
+ * `projection`) or a status-bearing async derivation (a `latest()`). Main-thread `workerStore`s
25
+ * mirror them like owned stores but cannot write; a status-bearing entry also propagates its
26
+ * pending/error to the replica, so an in-flight worker computation shows as pending on the main
27
+ * thread.
28
+ */
29
+ readonly published?: Record<string, PublishedSource<any>>;
30
+ /** Named tasks callable from the main thread (rung 1). */
31
+ readonly tasks?: Record<string, WorkerTaskHandler>;
32
+ /**
33
+ * Transport to serve on. Defaults to the worker global scope (`self`) when running in a real
34
+ * Worker, so `my.worker.ts` never touches `self`. Pass an explicit port for tests (a
35
+ * `MessageChannel` port) or a SharedWorker fan-in.
36
+ */
37
+ readonly port?: WorkerPortLike;
38
+ };
39
+ type WorkerHost<M extends WorkerSchema = WorkerSchema> = HasSchema<M> & {
40
+ /** This host's transport identity — the `origin` of every store batch it emits. */
41
+ readonly hostId: string;
42
+ /** Serve an additional transport (multi-client / tests). Returns a disconnect handle. */
43
+ connect(port: WorkerPortLike): () => void;
44
+ /**
45
+ * Synchronously emit any pending owned-store changes to subscribers NOW, instead of waiting for
46
+ * the microtask driver. Makes the mirror deterministic (tests call it before asserting) and is
47
+ * the honest settle point before applying a routed write (phase 4). No-op when nothing is pending.
48
+ */
49
+ flush(): void;
50
+ /** Stop every owned opLog and drop all connections. */
51
+ dispose(): void;
52
+ };
53
+ /**
54
+ * The worker-side runtime: owns store subtrees, hosts tasks, and mirrors owned state to every
55
+ * connected client as op batches. Plain functions + signals — NO Angular DI (there is none in a
56
+ * worker); each owned store is observed by a real {@link opLog} driven off the microtask queue.
57
+ *
58
+ * ```ts
59
+ * // my.worker.ts
60
+ * const todos = store<Todo[]>([], workerStoreContext());
61
+ * createWorkerHost({ stores: { todos } }); // serves `self` by default
62
+ * ```
63
+ */
64
+ declare function createWorkerHost<S extends Record<string, WritableSignal<any>> = Record<string, never>, P extends Record<string, PublishedSource<any>> = Record<string, never>, T extends Record<string, WorkerTaskHandler> = Record<string, never>>(options: {
65
+ readonly stores?: S;
66
+ readonly published?: P;
67
+ readonly tasks?: T;
68
+ readonly port?: WorkerPortLike;
69
+ }): WorkerHost<{
70
+ readonly stores: {
71
+ [K in keyof S]: SignalValueOf<S[K]>;
72
+ };
73
+ readonly published: {
74
+ [K in keyof P]: SignalValueOf<P[K]>;
75
+ };
76
+ readonly tasks: T;
77
+ }>;
78
+
79
+ /**
80
+ * An injector-free {@link OpLogDriver} for `@mmstack/primitives` `opLog`. Schedules the emission
81
+ * reaction on the microtask queue via `createWatch` (Angular's renderer-independent watch
82
+ * primitive) instead of an `effect`, so an opLog can run where there is no Angular injector: the
83
+ * worker side of the worker-graph, whose store has no application tick driving it.
84
+ *
85
+ * This lives in `@mmstack/worker/host` rather than in primitives on purpose. `createWatch` is a
86
+ * lower-tier signals-primitives export; keeping it out of `@mmstack/primitives` lets that package
87
+ * back-port to older Angular majors that may not export it. The worker package tracks a newer
88
+ * Angular where `createWatch` is available.
89
+ *
90
+ * Batching is per-microtask rather than per-app-tick; the worker protocol orders by
91
+ * `(origin, version)`, never tick alignment, so that is safe.
92
+ */
93
+ declare function microtaskOpLogDriver(): OpLogDriver;
94
+
95
+ /**
96
+ * The one shared store context for this worker — pass it to every `store`/`toStore` you create in
97
+ * the worker so they share proxy identity and cleanup, exactly as `providedIn: 'root'` shares one
98
+ * cache across an app's stores.
99
+ *
100
+ * ```ts
101
+ * // my.worker.ts
102
+ * const todos = store<Todo[]>([], workerStoreContext());
103
+ * const filter = store({ q: '' }, workerStoreContext()); // same context
104
+ * ```
105
+ */
106
+ declare function workerStoreContext(): toStoreOptions;
107
+
108
+ export { createWorkerHost, microtaskOpLogDriver, workerStoreContext };
109
+ export type { CreateWorkerHostOptions, WorkerHost, WorkerTaskHandler };
@@ -0,0 +1,184 @@
1
+ import { OpBatch, StoreOp } from '@mmstack/primitives';
2
+
3
+ /**
4
+ * A structured-clone-safe rendering of an `Error` for cross-thread propagation. A worker throw
5
+ * can't cross `postMessage` as an `Error` (methods/prototype are lost), so tasks and store writes
6
+ * serialize failures into this shape and the receiving side rebuilds a real `Error`.
7
+ */
8
+ type SerializedError = {
9
+ name: string;
10
+ message: string;
11
+ stack?: string;
12
+ cause?: SerializedError;
13
+ };
14
+ /** Renders any thrown value (Error or otherwise) into a {@link SerializedError}, chaining `cause`. */
15
+ declare function serializeError(err: unknown): SerializedError;
16
+ /** Rebuilds a real `Error` (name/message/stack/cause chain preserved) from a {@link SerializedError}. */
17
+ declare function deserializeError(e: SerializedError): Error;
18
+
19
+ /** Wire-protocol version, negotiated in the hello/ready handshake. Bump on breaking envelope change. */
20
+ declare const PROTO_VERSION = 1;
21
+ type ProtoVersion = typeof PROTO_VERSION;
22
+ /** The status a remote (worker-owned) computation reports across the boundary — the rung-3 seam. */
23
+ type RemoteStatus = 'idle' | 'loading' | 'reloading' | 'resolved' | 'error';
24
+ /**
25
+ * Every message exchanged over a {@link WorkerPortLike}. One discriminated union shared by the
26
+ * main-thread client and the worker host (and, later, `@mmstack/mesh`), so the two sides can never
27
+ * drift. Grouped: lifecycle · tasks (rung 1) · stores (rung 2) · remote status (rung 3).
28
+ */
29
+ type WorkerEnvelope = {
30
+ type: 'hello';
31
+ proto: ProtoVersion;
32
+ clientId: string;
33
+ } | {
34
+ type: 'ready';
35
+ proto: ProtoVersion;
36
+ hostId: string;
37
+ stores: readonly string[];
38
+ published: readonly string[];
39
+ tasks: readonly string[];
40
+ } | {
41
+ type: 'fatal';
42
+ error: SerializedError;
43
+ } | {
44
+ type: 'task:run';
45
+ runId: number;
46
+ task: string;
47
+ input: unknown;
48
+ } | {
49
+ type: 'task:abort';
50
+ runId: number;
51
+ } | {
52
+ type: 'task:ok';
53
+ runId: number;
54
+ value: unknown;
55
+ } | {
56
+ type: 'task:error';
57
+ runId: number;
58
+ error: SerializedError;
59
+ } | {
60
+ type: 'task:aborted';
61
+ runId: number;
62
+ } | {
63
+ type: 'store:subscribe';
64
+ store: string;
65
+ clientId: string;
66
+ } | {
67
+ type: 'store:snapshot';
68
+ store: string;
69
+ version: number;
70
+ value: unknown;
71
+ } | {
72
+ type: 'store:ops';
73
+ store: string;
74
+ batch: OpBatch;
75
+ } | {
76
+ type: 'store:write';
77
+ store: string;
78
+ writeId: number;
79
+ clientId: string;
80
+ ops: readonly StoreOp[];
81
+ } | {
82
+ type: 'store:write:ack';
83
+ store: string;
84
+ writeId: number;
85
+ version: number;
86
+ } | {
87
+ type: 'store:write:error';
88
+ store: string;
89
+ writeId: number;
90
+ error: SerializedError;
91
+ } | {
92
+ type: 'store:unsubscribe';
93
+ store: string;
94
+ clientId: string;
95
+ } | {
96
+ type: 'store:status';
97
+ store: string;
98
+ status: RemoteStatus;
99
+ error?: SerializedError;
100
+ };
101
+ /** Narrows a raw message to a specific envelope variant by its `type`. */
102
+ type EnvelopeOf<K extends WorkerEnvelope['type']> = Extract<WorkerEnvelope, {
103
+ type: K;
104
+ }>;
105
+
106
+ /** A short unique id for host/client identity on a shared transport. */
107
+ declare function generateId(): string;
108
+
109
+ /**
110
+ * The compile-time SHAPE of a worker host — its owned stores, published derivations, and tasks
111
+ * (key → value/signature). It is a phantom: carried through `typeof host` for main-thread type
112
+ * inference, never present at runtime. `workerStore`/`runTask` read it to constrain keys, infer
113
+ * value types, and expose `write()` only on OWNED (writable) subtrees.
114
+ */
115
+ type WorkerSchema = {
116
+ readonly stores: Record<string, unknown>;
117
+ readonly published: Record<string, unknown>;
118
+ readonly tasks: Record<string, (...args: any[]) => any>;
119
+ };
120
+ /** An empty schema — the default for an untyped connection (keys are `string`, values `unknown`). */
121
+ type EmptyWorkerSchema = {
122
+ readonly stores: Record<string, never>;
123
+ readonly published: Record<string, never>;
124
+ readonly tasks: Record<string, never>;
125
+ };
126
+ declare const WORKER_SCHEMA: unique symbol;
127
+ /** Phantom-tags a type with its {@link WorkerSchema} `M` so `SchemaOf` can recover it. */
128
+ type HasSchema<M extends WorkerSchema> = {
129
+ readonly [WORKER_SCHEMA]?: M;
130
+ };
131
+ /**
132
+ * Recovers the {@link WorkerSchema} carried by a `WorkerHost`/`WorkerRef` (via {@link HasSchema}),
133
+ * or passes a raw schema through — so `connectWorker<typeof host>()` and `connectWorker<MySchema>()`
134
+ * both work. Falls back to the loose schema otherwise.
135
+ */
136
+ type SchemaOf<H> = H extends HasSchema<infer M> ? M : H extends WorkerSchema ? H : WorkerSchema;
137
+ /** The input type of a schema's task `K`. */
138
+ type TaskInput<M extends WorkerSchema, K extends keyof M['tasks']> = Parameters<M['tasks'][K]>[0];
139
+ /** The resolved output type of a schema's task `K`. */
140
+ type TaskOutput<M extends WorkerSchema, K extends keyof M['tasks']> = Awaited<ReturnType<M['tasks'][K]>>;
141
+ /** Keys addressable by `workerStore` — owned OR published. */
142
+ type StoreKeys<M extends WorkerSchema> = (keyof M['stores'] & string) | (keyof M['published'] & string);
143
+ /** The value type behind a store/published key. */
144
+ type StoreValueOf<M extends WorkerSchema, K extends string> = K extends keyof M['stores'] ? M['stores'][K] : K extends keyof M['published'] ? M['published'][K] : unknown;
145
+ /** True when a key is an OWNED (writable) subtree — used to gate `write()`. */
146
+ type IsWritableKey<M extends WorkerSchema, K extends string> = K extends keyof M['stores'] ? true : false;
147
+ /** Extracts the value type a signal/store holds (for building a schema from host options). */
148
+ type SignalValueOf<S> = S extends {
149
+ (): infer V;
150
+ } ? V : never;
151
+
152
+ /**
153
+ * The minimal structural transport the worker protocol rides on — the intersection of a `Worker`,
154
+ * a `MessagePort`, and a `BroadcastChannel`. Modeled on `@mmstack/resource`'s `MutationSyncChannel`:
155
+ * anything with `postMessage` + assignable `onmessage` qualifies, so a `MessageChannel` port-pair
156
+ * works in tests with zero adapters and a future `@mmstack/mesh` can slot a WS/RTC channel behind
157
+ * the same type.
158
+ */
159
+ type WorkerPortLike = {
160
+ postMessage(message: unknown, transfer?: Transferable[]): void;
161
+ onmessage: ((ev: any) => void) | null;
162
+ /** `Worker` has `terminate()`, `MessagePort`/`BroadcastChannel` have `close()` — either, both optional. */
163
+ terminate?(): void;
164
+ close?(): void;
165
+ };
166
+ /** Closes a port by whichever teardown method it exposes. */
167
+ declare function closePort(port: WorkerPortLike): void;
168
+ /**
169
+ * Marks `value` so that, when it is sent over a {@link WorkerPortLike}, the listed `Transferable`s
170
+ * (e.g. an `ArrayBuffer`) are moved rather than structure-cloned — zero-copy, but detached at the
171
+ * sender. Comlink-idiom. v1 honors this on task input/output only; store traffic always clones (a
172
+ * moved buffer inside a replicated tree would detach the owner's copy).
173
+ *
174
+ * ```ts
175
+ * const buf = new Float64Array(1e6);
176
+ * workerResource(() => transfer({ buf }, [buf.buffer]), { worker, task: 'process' });
177
+ * ```
178
+ */
179
+ declare function transfer<T extends object>(value: T, transferables: Transferable[]): T;
180
+ /** @internal Reads back the Transferables registered on a value via {@link transfer}, if any. */
181
+ declare function takeTransferables(value: unknown): Transferable[] | undefined;
182
+
183
+ export { PROTO_VERSION, closePort, deserializeError, generateId, serializeError, takeTransferables, transfer };
184
+ export type { EmptyWorkerSchema, EnvelopeOf, HasSchema, IsWritableKey, ProtoVersion, RemoteStatus, SchemaOf, SerializedError, SignalValueOf, StoreKeys, StoreValueOf, TaskInput, TaskOutput, WorkerEnvelope, WorkerPortLike, WorkerSchema };
@@ -0,0 +1,151 @@
1
+ import { WorkerSchema, HasSchema, TaskInput, TaskOutput, WorkerEnvelope, WorkerPortLike, SchemaOf, StoreKeys, StoreValueOf, IsWritableKey } from '@mmstack/worker/protocol';
2
+ export * from '@mmstack/worker/protocol';
3
+ import { Injector, Signal, ValueEqualityFn, ResourceStatus } from '@angular/core';
4
+ import { SignalStore, WritableSignalStore } from '@mmstack/primitives';
5
+
6
+ /** What the worker advertised in its `ready` handshake. */
7
+ type WorkerManifest = {
8
+ readonly hostId: string;
9
+ readonly stores: readonly string[];
10
+ readonly published: readonly string[];
11
+ readonly tasks: readonly string[];
12
+ };
13
+ /** Thrown into a task/write promise when it is aborted. `name` is `'AbortError'` (DOM convention). */
14
+ declare class WorkerAbortError extends Error {
15
+ constructor(message?: string);
16
+ }
17
+ /** Thrown into pending tasks/writes when the worker crashes. `name` is `'WorkerCrashedError'`. */
18
+ declare class WorkerCrashedError extends Error {
19
+ constructor(message?: string);
20
+ }
21
+ type ConnectWorkerOptions = {
22
+ readonly injector?: Injector;
23
+ /** `'auto'` (default) respawns on crash; `'manual'` surfaces disconnect and stops. */
24
+ readonly restart?: 'auto' | 'manual';
25
+ /** Backoff before respawn attempt `n` (0-based). Default exponential 1s→30s. */
26
+ readonly restartDelay?: (attempt: number) => number;
27
+ };
28
+ type WorkerRef<M extends WorkerSchema = WorkerSchema> = HasSchema<M> & {
29
+ /** True once the `ready` handshake has completed (and again after an auto-restart). */
30
+ readonly connected: Signal<boolean>;
31
+ /** The worker's advertised manifest, or `null` before the first `ready`. */
32
+ readonly manifest: Signal<WorkerManifest | null>;
33
+ /** Run a named task exposed by the worker host; resolves with its result (typed from the schema). */
34
+ runTask<K extends keyof M['tasks'] & string>(task: K, input: TaskInput<M, K>, opt?: {
35
+ signal?: AbortSignal;
36
+ transfer?: Transferable[];
37
+ }): Promise<TaskOutput<M, K>>;
38
+ destroy(): void;
39
+ /** @internal Post an envelope to the worker. */
40
+ _send(msg: WorkerEnvelope, transfer?: Transferable[]): void;
41
+ /** @internal Receive every non-task envelope (store traffic). Returns an unsubscribe. */
42
+ _subscribe(handler: (msg: WorkerEnvelope) => void): () => void;
43
+ /** @internal Run `fn` on every (re)connection — used to re-subscribe stores after a restart. */
44
+ _onReady(fn: () => void): () => void;
45
+ /** @internal Run `fn` when the connection drops (crash) — replicas reject pending writes here. */
46
+ _onDisconnect(fn: () => void): () => void;
47
+ /** @internal This client's identity on the transport. */
48
+ readonly clientId: string;
49
+ };
50
+ /**
51
+ * Connects the main thread to a worker over a {@link WorkerPortLike} the caller provides via `spawn`
52
+ * — typically `() => new Worker(new URL('./my.worker', import.meta.url), { type: 'module' })` (the
53
+ * URL literal must live in APP code so the bundler emits the worker chunk). Performs the hello/ready
54
+ * handshake, exposes `connected`/`manifest`, routes named-task runs, and (via internal seams)
55
+ * carries the store-replication traffic for {@link workerStore}. On crash it respawns with backoff
56
+ * when `restart: 'auto'`.
57
+ */
58
+ declare function connectWorker<H = WorkerSchema>(spawn: () => WorkerPortLike, opt?: ConnectWorkerOptions): WorkerRef<SchemaOf<H>>;
59
+
60
+ /** Returned from a paused `params` fn to HOLD (keep the current value/status, run nothing). */
61
+ declare const PAUSED: unique symbol;
62
+ type WorkerRequestContext = {
63
+ readonly paused: typeof PAUSED;
64
+ };
65
+ /**
66
+ * Reactive input producer. Reads signals to derive the task input; return `undefined` to disable
67
+ * (idle, no run), or `ctx.paused` to hold. Re-runs when the returned input changes (by `Object.is`).
68
+ */
69
+ type WorkerParamsFn<TInput> = (ctx: WorkerRequestContext) => TInput | undefined | void | typeof PAUSED;
70
+ type WorkerResourceOptions<TResult> = {
71
+ readonly injector?: Injector;
72
+ /** Equality for the result value — a run resolving equal to the held value emits no notification. */
73
+ readonly equal?: ValueEqualityFn<TResult>;
74
+ readonly defaultValue?: TResult;
75
+ /**
76
+ * Hold the previous value through a re-run (status `'reloading'`) instead of clearing it. Default
77
+ * TRUE — a `workerResource` is an async derivation, not a fetch; flashing empty mid-recompute is
78
+ * rarely wanted.
79
+ */
80
+ readonly keepPrevious?: boolean;
81
+ /** Register into the nearest transition scope: `'indicator'` drives pending, `'suspend'` also gates first paint. */
82
+ readonly register?: false | 'indicator' | 'suspend';
83
+ /** The connected worker whose host exposes the task. */
84
+ readonly worker: WorkerRef;
85
+ /** The name of the task to run (as declared in `createWorkerHost({ tasks })`). */
86
+ readonly task: string;
87
+ };
88
+ type WorkerResourceRef<T> = {
89
+ /** The latest successfully-computed value (held through re-runs per `keepPrevious`). */
90
+ readonly value: Signal<T | undefined>;
91
+ readonly status: Signal<ResourceStatus>;
92
+ readonly error: Signal<unknown>;
93
+ readonly isLoading: Signal<boolean>;
94
+ hasValue(): boolean;
95
+ /** Re-run with the current input, bypassing input de-duplication. */
96
+ reload(): void;
97
+ /** Cancel the in-flight run; the value is KEPT and status becomes `'local'`. */
98
+ abort(): void;
99
+ destroy(): void;
100
+ };
101
+ /**
102
+ * Runs a named task on a connected worker, exposed with the standard resource surface so heavy
103
+ * compute makes the UI *pending*, not *frozen*. `params` reactively derives the input; latest-wins
104
+ * (a changed input supersedes and aborts the in-flight run); the result surfaces as
105
+ * `value`/`status`/`error`, and — with `register` — participates in transition scopes. No-ops on the
106
+ * server.
107
+ */
108
+ declare function workerResource<TInput, TResult>(params: WorkerParamsFn<TInput>, options: WorkerResourceOptions<TResult>): WorkerResourceRef<TResult>;
109
+
110
+ type WorkerStoreOptions<T> = {
111
+ readonly injector?: Injector;
112
+ /** Value the replica holds before the first snapshot arrives. */
113
+ readonly defaultValue?: T;
114
+ /** Register into the nearest transition scope while hydrating/reloading. */
115
+ readonly register?: false | 'indicator' | 'suspend';
116
+ };
117
+ /** The write path — present only on OWNED (writable) subtrees; absent on published (read-only) ones. */
118
+ type WorkerStoreWrite<T> = {
119
+ /**
120
+ * Route a write to the OWNER: run `recipe` against a writable draft of the current replica, diff
121
+ * it to ops, ship them. Resolves once the owner's authoritative batch carrying this write has been
122
+ * applied to THIS replica (honest async — the value is not applied locally first; for optimistic
123
+ * UI, fork the replica and reconcile on resolve). Rejects if the owner reports a write error.
124
+ */
125
+ write(recipe: (draft: WritableSignalStore<T>) => void): Promise<void>;
126
+ };
127
+ type WorkerStoreRef<T, W extends boolean = true> = {
128
+ /** The live, READ-ONLY replica — deep per-leaf reads, mirrored from the worker-owned subtree. */
129
+ readonly store: SignalStore<T>;
130
+ /** The replica root value (undefined before hydration). */
131
+ readonly value: Signal<T | undefined>;
132
+ readonly status: Signal<ResourceStatus>;
133
+ readonly error: Signal<unknown>;
134
+ readonly isLoading: Signal<boolean>;
135
+ /** The underlying worker connection's liveness. */
136
+ readonly connected: Signal<boolean>;
137
+ hasValue(): boolean;
138
+ destroy(): void;
139
+ } & (W extends true ? WorkerStoreWrite<T> : object);
140
+ /**
141
+ * A live READ-ONLY replica of a store subtree OWNED by the worker. Subscribes over the
142
+ * {@link WorkerRef}, hydrates from the owner's snapshot, then applies each authoritative op batch
143
+ * into a main-thread `store` — one `set` per batch, so a batch of N ops is a single notification
144
+ * wave. Satisfies `ResourceLike`/`UseSource`, so it participates in transition scopes and nests in
145
+ * `latest()`/`use()`. Writes are not local: they route to the owner (added in the next step).
146
+ */
147
+ declare function workerStore<M extends WorkerSchema, K extends StoreKeys<M>>(worker: WorkerRef<M>, key: K, opt?: WorkerStoreOptions<StoreValueOf<M, K> & object>): WorkerStoreRef<StoreValueOf<M, K> & object, IsWritableKey<M, K>>;
148
+ declare function workerStore<T extends object>(worker: WorkerRef, key: string, opt?: WorkerStoreOptions<T>): WorkerStoreRef<T, true>;
149
+
150
+ export { PAUSED, WorkerAbortError, WorkerCrashedError, connectWorker, workerResource, workerStore };
151
+ export type { ConnectWorkerOptions, WorkerManifest, WorkerParamsFn, WorkerRef, WorkerRequestContext, WorkerResourceOptions, WorkerResourceRef, WorkerStoreOptions, WorkerStoreRef };