@thomasfosterau/legation-browser 0.0.0-stage → 0.1.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.
- package/CHANGELOG.md +842 -0
- package/LICENSE +21 -0
- package/README.md +323 -2
- package/dist/index.d.ts +656 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1109 -0
- package/dist/index.js.map +1 -0
- package/dist/rolldown-runtime-D8jQpDG7.js +12 -0
- package/package.json +57 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["make","BroadcastMirrorChannel.make","EngineHost","run","MirrorProtocol","BroadcastMirrorChannel.make","Mirror","EngineHost","listen","Marker","BackgroundSync.identityUnchecked","BackgroundSync.register"],"sources":["../src/internal/IndexedDbKeyValue.ts","../src/IndexedDbClientStore.ts","../src/BroadcastMirrorChannel.ts","../src/ServiceWorkerHost.ts","../src/ElectedHost.ts","../src/CacheApiHttpCache.ts","../src/HttpCacheInvalidation.ts","../src/BrowserConnectivity.ts","../src/BackgroundSync.ts","../src/PushPoke.ts"],"sourcesContent":["/**\n * The platform-compatible IndexedDB key-value seam under {@link IndexedDbClientStore} (#393). Not part of\n * the package's public surface: the store and the tests import it from here, and nothing re-exports it.\n *\n * It opens the platform's database without a version, keeps one connection per layer, and gives the\n * store a `KeyValueStore` whose records the platform's `BrowserKeyValueStore` reads back, plus the\n * `AtomicKeyValue` batch over the same entries store.\n */\n\nimport { IndexedDb } from \"@effect/platform-browser\";\nimport { StoreError } from \"@thomasfosterau/legation\";\nimport { AtomicKeyValue } from \"@thomasfosterau/legation/internal\";\nimport * as Arr from \"effect/Array\";\nimport * as Effect from \"effect/Effect\";\nimport * as Layer from \"effect/Layer\";\nimport * as P from \"effect/Predicate\";\nimport * as Result from \"effect/Result\";\nimport { KeyValueStore } from \"effect/persistence\";\n\n/**\n * The database and object store this module reads and writes: the database\n * platform-browser's `BrowserKeyValueStore.layerIndexedDb` (4.0.2) also uses,\n * by default `effect_key_value_store`, with an `entries` store keyed by `key`\n * whose records are `{ key, value }`. Both are written here in the same layout,\n * so the store and the atomic batch read one database.\n *\n * The database is opened without a version (#393). The platform's layer opens\n * at version 1, which fails with a `VersionError` on a database a later version\n * created, and that failure kills the layer. Opening at whatever version exists\n * keeps the layer alive. A database without the `entries` store cannot be fixed\n * from here (creating it needs a version upgrade), so its reads and its batch\n * fail with an error that names it.\n */\nconst PLATFORM_DATABASE = \"effect_key_value_store\";\nconst ENTRIES_STORE = \"entries\";\n\n/**\n * What every call on a store whose database another connection upgraded fails with (#393). The\n * connection is closed by then, so the call cannot run; the message says why.\n */\nconst UPGRADED_MESSAGE =\n \"the IndexedDB database was upgraded by another connection; reload to continue\";\n\n/** One open connection to the platform database, and whether another connection has closed it. */\ninterface Connection {\n readonly db: IDBDatabase;\n /** Set when another connection asks for a newer version and this one closes itself. */\n upgraded: boolean;\n}\n\n/**\n * Open the platform's database at the version it has, creating the entries\n * store only when the database is new. The connection closes itself when\n * another connection asks for a newer version, rather than blocking that\n * connection's upgrade (`onversionchange`); after that, its calls fail with\n * {@link UPGRADED_MESSAGE}.\n */\nconst openDatabase = (\n database: string\n): Effect.Effect<Connection, StoreError, IndexedDb.IndexedDb> =>\n Effect.gen(function* () {\n const idb = (yield* IndexedDb.IndexedDb).indexedDB;\n return yield* Effect.callback<Connection, StoreError>((resume) => {\n const fail = (cause: unknown) =>\n resume(\n Effect.fail(new StoreError({ message: \"Failed to open the IndexedDB store\", cause }))\n );\n const opened = Result.try(() => idb.open(database));\n if (Result.isFailure(opened)) {\n fail(opened.failure);\n return;\n }\n const request = opened.success;\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(ENTRIES_STORE)) {\n db.createObjectStore(ENTRIES_STORE, { keyPath: \"key\" });\n }\n };\n request.onsuccess = () => {\n const connection: Connection = { db: request.result, upgraded: false };\n connection.db.onversionchange = () => {\n connection.upgraded = true;\n connection.db.close();\n };\n resume(Effect.succeed(connection));\n };\n request.onerror = () => fail(request.error);\n });\n });\n\n/**\n * Abort `transaction`, ignoring the `InvalidStateError` an already-finished\n * transaction throws: there is nothing left to abort then.\n */\nconst abortQuietly = (transaction: IDBTransaction): void => {\n Result.try(() => transaction.abort());\n};\n\n/** A clear error for a database that lacks the entries store (see the module note above). */\nconst missingEntriesStore = (db: IDBDatabase, method: string, key?: string) =>\n new KeyValueStore.KeyValueStoreError({\n method,\n message: `IndexedDB database \"${db.name}\" has no \"${ENTRIES_STORE}\" object store`,\n ...(key === undefined ? {} : { key }),\n });\n\n/**\n * One request in its own transaction, resolved when the transaction completes,\n * so a write is durable before the caller continues. A failed or aborted\n * transaction fails the call.\n */\nconst runRequest = <A>(\n connection: Connection,\n mode: IDBTransactionMode,\n failure: { readonly method: string; readonly message: string; readonly key?: string },\n evaluate: (store: IDBObjectStore) => IDBRequest<A>\n): Effect.Effect<A, KeyValueStore.KeyValueStoreError> =>\n Effect.callback<A, KeyValueStore.KeyValueStoreError>((resume) => {\n let settled = false;\n let result: A | undefined;\n const finish = (effect: Effect.Effect<A, KeyValueStore.KeyValueStoreError>) => {\n if (settled) return;\n settled = true;\n resume(effect);\n };\n const fail = (cause: unknown) =>\n finish(\n Effect.fail(\n new KeyValueStore.KeyValueStoreError({\n ...failure,\n message: connection.upgraded ? UPGRADED_MESSAGE : failure.message,\n cause,\n })\n )\n );\n if (connection.upgraded) {\n finish(\n Effect.fail(\n new KeyValueStore.KeyValueStoreError({\n method: failure.method,\n message: UPGRADED_MESSAGE,\n ...(failure.key === undefined ? {} : { key: failure.key }),\n })\n )\n );\n return;\n }\n const db = connection.db;\n if (!db.objectStoreNames.contains(ENTRIES_STORE)) {\n finish(Effect.fail(missingEntriesStore(db, failure.method, failure.key)));\n return;\n }\n const began = Result.try(() => db.transaction(ENTRIES_STORE, mode));\n if (Result.isFailure(began)) {\n fail(began.failure);\n return;\n }\n const transaction = began.success;\n transaction.oncomplete = () => finish(Effect.succeed(result as A));\n transaction.onerror = () => fail(transaction.error);\n transaction.onabort = () => fail(transaction.error);\n const issued = Result.try(() => {\n const request = evaluate(transaction.objectStore(ENTRIES_STORE));\n request.onsuccess = () => {\n result = request.result;\n };\n request.onerror = () => fail(request.error);\n });\n if (Result.isFailure(issued)) {\n fail(issued.failure);\n abortQuietly(transaction);\n }\n return Effect.sync(() => {\n if (settled) return;\n settled = true;\n abortQuietly(transaction);\n });\n });\n\n/**\n * The platform's `KeyValueStore` semantics over the database {@link openDatabase}\n * opened: a string value reads back as a string, a `Uint8Array` as bytes, and\n * every call is its own transaction, as the platform's store makes them.\n */\nconst keyValueOver = (connection: Connection): KeyValueStore.KeyValueStore => {\n /** A stored record: the `{ key, value }` the platform writes. */\n type StoredRecord = { readonly key: string; readonly value: unknown } | undefined;\n return KeyValueStore.make({\n get: (key) =>\n runRequest<StoredRecord>(\n connection,\n \"readonly\",\n { method: \"get\", message: \"Failed to get value from backing store\", key },\n (store) => store.get(key)\n ).pipe(Effect.map((found) => (P.isString(found?.value) ? found.value : undefined))),\n getUint8Array: (key) =>\n runRequest<StoredRecord>(\n connection,\n \"readonly\",\n { method: \"getUint8Array\", message: \"Failed to get value from backing store\", key },\n (store) => store.get(key)\n ).pipe(Effect.map((found) => (found?.value instanceof Uint8Array ? found.value : undefined))),\n has: (key) =>\n runRequest(\n connection,\n \"readonly\",\n { method: \"has\", message: \"Failed to check key in backing store\", key },\n (store) => store.count(key)\n ).pipe(Effect.map((count) => count > 0)),\n set: (key, value) =>\n runRequest(\n connection,\n \"readwrite\",\n { method: \"set\", message: \"Failed to set value in backing store\", key },\n (store) => store.put({ key, value })\n ).pipe(Effect.asVoid),\n remove: (key) =>\n runRequest(\n connection,\n \"readwrite\",\n { method: \"remove\", message: \"Failed to remove value from backing store\", key },\n (store) => store.delete(key)\n ).pipe(Effect.asVoid),\n clear: runRequest(\n connection,\n \"readwrite\",\n { method: \"clear\", message: \"Failed to clear backing store\" },\n (store) => store.clear()\n ).pipe(Effect.asVoid),\n size: runRequest(\n connection,\n \"readonly\",\n { method: \"size\", message: \"Failed to get backing store size\" },\n (store) => store.count()\n ),\n keys: runRequest(\n connection,\n \"readonly\",\n { method: \"keys\", message: \"Failed to list keys in backing store\" },\n (store) => store.getAllKeys()\n ).pipe(Effect.map((keys) => keys.filter(P.isString))),\n });\n};\n\n/**\n * Commit `ops` in one `readwrite` transaction on the entries store: IndexedDB\n * commits a transaction whole or not at all, so a failed or interrupted batch\n * leaves the store as it was.\n */\nconst commitOps = (\n connection: Connection,\n ops: ReadonlyArray<AtomicKeyValue.KeyValueOp>\n): Effect.Effect<void, StoreError> =>\n Effect.callback<void, StoreError>((resume) => {\n let settled = false;\n const finish = (effect: Effect.Effect<void, StoreError>) => {\n if (settled) return;\n settled = true;\n resume(effect);\n };\n const fail = (cause: unknown) =>\n finish(\n Effect.fail(\n new StoreError({\n message: connection.upgraded\n ? UPGRADED_MESSAGE\n : \"Failed to commit an atomic batch to IndexedDB\",\n cause,\n })\n )\n );\n if (connection.upgraded) {\n finish(Effect.fail(new StoreError({ message: UPGRADED_MESSAGE })));\n return;\n }\n const db = connection.db;\n if (!db.objectStoreNames.contains(ENTRIES_STORE)) {\n finish(\n Effect.fail(\n new StoreError({\n message: `Cannot commit an atomic batch: IndexedDB database \"${db.name}\" has no \"${ENTRIES_STORE}\" object store`,\n })\n )\n );\n return;\n }\n const began = Result.try(() => db.transaction(ENTRIES_STORE, \"readwrite\"));\n if (Result.isFailure(began)) {\n fail(began.failure);\n return;\n }\n const transaction = began.success;\n const abort = () => abortQuietly(transaction);\n transaction.oncomplete = () => finish(Effect.void);\n transaction.onerror = () => fail(transaction.error);\n transaction.onabort = () => fail(transaction.error);\n const issued = Result.try(() => {\n const entries = transaction.objectStore(ENTRIES_STORE);\n // Every request is issued synchronously here: IndexedDB commits a transaction once control\n // returns to the event loop with no request pending, so nothing may await between them.\n Arr.forEach(ops, (op) => {\n if (op._tag === \"Set\") entries.put({ key: op.key, value: op.value });\n else entries.delete(op.key);\n });\n });\n if (Result.isFailure(issued)) {\n fail(issued.failure);\n abort();\n }\n // Interrupted before the commit point: abort, so none of the batch lands.\n return Effect.sync(() => {\n if (!settled) {\n settled = true;\n abort();\n }\n });\n });\n\n/**\n * The platform `KeyValueStore` over IndexedDB, opened without a version (see\n * the module note above). Keys are the full keys the store holds.\n */\nexport const keyValueLayer = (\n database: string | undefined\n): Layer.Layer<KeyValueStore.KeyValueStore, never, IndexedDb.IndexedDb> =>\n Layer.effect(KeyValueStore.KeyValueStore)(\n Effect.gen(function* () {\n const connection = yield* Effect.acquireRelease(\n openDatabase(database ?? PLATFORM_DATABASE),\n (opened) => Effect.sync(() => opened.db.close())\n ).pipe(Effect.orDie);\n return keyValueOver(connection);\n })\n );\n\n/**\n * Every string entry in the entries store, read in one `readonly` transaction (#392): a single\n * request, so the records are one state of the database and a batch another connection commits is\n * either wholly in them or wholly absent. The platform store writes only strings for the keys this\n * layer reads, so other values are left out.\n */\nconst readEntries = (\n connection: Connection\n): Effect.Effect<ReadonlyArray<AtomicKeyValue.KeyValueEntry>, StoreError> =>\n runRequest<Array<{ readonly key: string; readonly value: unknown }>>(\n connection,\n \"readonly\",\n { method: \"getAll\", message: \"Failed to read the IndexedDB store\" },\n (store) => store.getAll()\n ).pipe(\n Effect.map((records) =>\n records.flatMap((record) =>\n P.isString(record.value) ? [{ key: record.key, value: record.value }] : []\n )\n ),\n Effect.mapError((error) => new StoreError({ message: error.message, cause: error }))\n );\n\n/**\n * The atomic batch over the same IndexedDB database as the `KeyValueStore`\n * (#393), with its one-transaction read of the entries (#392). Keys arrive in\n * full, namespace prefix included, as the store holds them, so the batch needs\n * no knowledge of the prefix.\n */\nexport const atomicKeyValueLayer = (\n database: string | undefined\n): Layer.Layer<AtomicKeyValue.AtomicKeyValue, never, IndexedDb.IndexedDb> =>\n Layer.effect(AtomicKeyValue.AtomicKeyValue)(\n Effect.gen(function* () {\n const connection = yield* Effect.acquireRelease(\n openDatabase(database ?? PLATFORM_DATABASE),\n (opened) => Effect.sync(() => opened.db.close())\n ).pipe(Effect.orDie);\n return AtomicKeyValue.AtomicKeyValue.of({\n apply: (ops) => (Arr.isReadonlyArrayEmpty(ops) ? Effect.void : commitOps(connection, ops)),\n readAll: readEntries(connection),\n });\n })\n );\n","/**\n * IndexedDbClientStore\n *\n * The durable browser ClientStore (ADR-0010): core's KeyValueStore-backed\n * store over platform-browser's IndexedDB KeyValueStore. Every engine\n * state transition lands in IndexedDB, so a killed service worker (or\n * closed tab in main-thread hosting) rehydrates losslessly.\n *\n * A durable replica on a browser is a durable replica on a **device**, which\n * is where the identity boundary earns its keep (#212, CONTEXT.md): a\n * multi-tenant app on a shared device syncs one user's private rows\n * legitimately — the server's auth filter said they could have them — and\n * then the next user opens the app to the freshest thing on the machine.\n * There are two ways to hold that line, and both are here:\n *\n * - {@link layerWindow} with an `identity`: **one replica, reset on change.**\n * The store stamps the fingerprint and compares it on open; a mismatch\n * quarantines the unpushed journal under the superseded fingerprint and\n * drops rows and cookie before anything can read them.\n * - {@link layerNamespaced}: **one replica per identity.** The fingerprint\n * derives the key prefix, so two principals never share a keyspace and\n * nothing needs resetting.\n *\n * A version change from another connection (a newer app build upgrading the\n * database, or a deletion) closes this store's connections, since they would\n * otherwise block it. From then on every call fails with an error that says\n * the database was upgraded by another connection and asks for a reload. The\n * store never reopens itself, so a reload is the way back.\n *\n * Which to choose: namespace when the device has a small number of regular\n * users who switch back and forth (each keeps a warm replica, so switching\n * back costs no re-pull, at the price of holding both on disk); reset when\n * sign-ins are one-off or unbounded — a kiosk, a shared library machine —\n * where accumulating a replica per visitor is the worse trade.\n */\n\nimport { IndexedDb } from \"@effect/platform-browser\";\nimport { ClientStore, Identity } from \"@thomasfosterau/legation\";\nimport * as Layer from \"effect/Layer\";\nimport { atomicKeyValueLayer, keyValueLayer } from \"./internal/IndexedDbKeyValue.js\";\n\n/** Options for the IndexedDB-backed `ClientStore` layers. */\nexport interface IndexedDbClientStoreOptions {\n /** IndexedDB database name. Defaults to platform-browser's default. */\n readonly database?: string;\n /** Key namespace inside the store. Defaults to core's `\"legation/\"`. */\n readonly prefix?: string;\n /**\n * The identity this replica belongs to — an opaque, app-supplied\n * fingerprint, **never PII and never a credential** (see\n * {@link Identity.Fingerprint}). Supplying it turns the identity boundary\n * on: the store compares it with its stamp on open and resets the replica\n * on a mismatch, quarantining unpushed writes under the superseded\n * fingerprint. Unset leaves the boundary off.\n */\n readonly identity?: Identity.Fingerprint;\n}\n\n/** The store options that reach core, absent keys left absent. */\nconst keyValueOptions = (\n options: IndexedDbClientStoreOptions | undefined\n): ClientStore.KeyValueOptions => ({\n ...(options?.prefix === undefined ? {} : { prefix: options.prefix }),\n ...(options?.identity === undefined ? {} : { identity: options.identity }),\n});\n\n/**\n * ClientStore over IndexedDB. Requires the `IndexedDb` primitives service;\n * use {@link layerWindow} where browser globals are available, or provide\n * `IndexedDb` yourself (tests use fake-indexeddb).\n *\n * A pull and a push acknowledgement commit as one IndexedDB transaction (#393),\n * so a durable pull writes its rows once and writes no release record.\n */\nexport const layer = (\n options?: IndexedDbClientStoreOptions\n): Layer.Layer<ClientStore.ClientStore, never, IndexedDb.IndexedDb> =>\n ClientStore.layerKeyValue(keyValueOptions(options)).pipe(\n Layer.provide(\n Layer.merge(keyValueLayer(options?.database), atomicKeyValueLayer(options?.database))\n )\n );\n\n/** Self-contained layer using the browser's global `indexedDB`. */\nexport const layerWindow = (\n options?: IndexedDbClientStoreOptions\n): Layer.Layer<ClientStore.ClientStore> =>\n layer(options).pipe(Layer.provide(IndexedDb.layerWindow));\n\n/**\n * The key prefix {@link layerNamespaced} derives for a fingerprint:\n * core's `\"legation/\"` namespace with a percent-encoded identity segment\n * under it, so two principals' replicas cannot collide and neither can\n * collide with an un-namespaced one.\n */\nexport const namespacedPrefix = (identity: Identity.Fingerprint): string =>\n `legation/identity/${Identity.keySegment(identity)}/`;\n\n/**\n * The namespacing alternative to resetting (#212): one replica per identity\n * in a single IndexedDB database, keyed by fingerprint.\n *\n * A principal's rows are unreachable from another principal's store because\n * they are under a different key prefix — no comparison, no reset, and a\n * user who switches back finds their replica still warm. The fingerprint is\n * still stamped, so a store reached through this layer keeps the same\n * guarantee if it is ever opened un-namespaced.\n *\n * The cost is disk: every identity that has ever signed in on this device\n * keeps a replica until something clears it. Prefer {@link layerWindow} with\n * an `identity` where sign-ins are unbounded.\n *\n * Pass `database` to isolate at the database level instead of the key level\n * — a heavier boundary (a whole IndexedDB database per identity, deletable\n * in one call) for apps that want it.\n */\nexport const layerNamespaced = (\n identity: Identity.Fingerprint,\n options?: Omit<IndexedDbClientStoreOptions, \"identity\" | \"prefix\">\n): Layer.Layer<ClientStore.ClientStore> =>\n layerWindow({\n ...(options?.database === undefined ? {} : { database: options.database }),\n prefix: namespacedPrefix(identity),\n identity,\n });\n","/**\n * BroadcastMirrorChannel\n *\n * The browser binding of the `MirrorChannel` seam: schema-defined mirror\n * protocol messages over a `BroadcastChannel` name. Pair one in the engine\n * host with one per client context (tab, window, worker) — BroadcastChannel\n * semantics mean a side never receives its own posts.\n */\nimport { TransportError } from \"@thomasfosterau/legation\";\nimport { MirrorChannel } from \"@thomasfosterau/legation/internal\";\nimport * as Effect from \"effect/Effect\";\nimport * as Queue from \"effect/Queue\";\nimport * as Scope from \"effect/Scope\";\n\n/** A {@link MirrorChannel.MirrorChannel} over a `BroadcastChannel` name. */\nexport const make = (\n name: string\n): Effect.Effect<MirrorChannel.MirrorChannel, never, Scope.Scope> =>\n Effect.gen(function* () {\n const channel = yield* Effect.acquireRelease(\n Effect.sync(() => new BroadcastChannel(name)),\n (ch) => Effect.sync(() => ch.close())\n );\n return {\n post: (message) =>\n Effect.try({\n // oxlint-disable-next-line unicorn/require-post-message-target-origin -- BroadcastChannel.postMessage takes no targetOrigin\n try: () => channel.postMessage(message),\n catch: (cause) => new TransportError({ message: `BroadcastChannel post failed`, cause }),\n }),\n subscribe: Effect.gen(function* () {\n const queue = yield* Queue.unbounded<unknown>();\n yield* Effect.acquireRelease(\n Effect.sync(() => {\n const listener = (event: MessageEvent) => {\n Queue.offerUnsafe(queue, event.data);\n };\n channel.addEventListener(\"message\", listener as EventListener);\n return listener;\n }),\n (listener) =>\n Effect.sync(() => channel.removeEventListener(\"message\", listener as EventListener))\n );\n return { take: Queue.take(queue).pipe(Effect.orDie) };\n }),\n };\n });\n","/**\n * ServiceWorkerHost\n *\n * The recommended engine host (ADR-0010): runs the ClientEngine inside the\n * per-origin service-worker singleton — engine sync loop + mirror\n * serving over a BroadcastChannel. Durable state belongs in the\n * IndexedDbClientStore layer, so a killed worker rehydrates losslessly.\n *\n * The engine stays host-agnostic: the same `run` works on the main thread\n * (the documented degraded mode) since it only needs BroadcastChannel.\n *\n * Background Sync is composed in, opt-in (#213): install\n * `BackgroundSync.listen({ identity })` at the top level of the worker script and pass\n * the listener as `backgroundSync`, and the host answers the library's `sync`\n * tag by flushing the journal through `ClientEngine.push` — so a write\n * journaled offline lands after its tab has closed — and registers the tag\n * when the journal cannot drain now (a mutation journaled offline, or a push\n * that failed terminally; never an online write, #246). Periodic Background Sync stays app code\n * (a `periodicsync` listener calling `ClientEngine.pull`); see\n * `BackgroundSync` for why.\n *\n * To evict HTTP-cache entries as collection changes land, fork\n * `HttpCacheInvalidation.invalidateOnChanges` alongside `run` in the same\n * scope — kept out of the host so it stays an opt-in composition:\n *\n * ```ts\n * yield* ServiceWorkerHost.run();\n * yield* HttpCacheInvalidation.invalidateOnChanges({\n * store,\n * changes: HttpCacheInvalidation.collectionChanges(engine.engineEvents),\n * });\n * ```\n *\n * A worker that also answers JSON:API `GET`s from the replica (the façade,\n * ADR-0029 — `JsonApiFacade` in `@thomasfosterau/legation-jsonapi`) needs what the host\n * knows: {@link start} serves exactly as {@link run} does and returns the\n * {@link Host} — the engine and the registrations the host holds on the\n * mirrors' behalf, the ledger the façade's coverage check scans — then\n * leaves the caller to keep the scope open.\n */\n\nimport type { Client, Collection, Node } from \"@thomasfosterau/legation\";\nimport { ClientEngine, type Coverage, EngineHost } from \"@thomasfosterau/legation/internal\";\nimport * as Effect from \"effect/Effect\";\nimport * as Scope from \"effect/Scope\";\nimport type * as BackgroundSync from \"./BackgroundSync.js\";\nimport type * as PushPoke from \"./PushPoke.js\";\nimport * as BroadcastMirrorChannel from \"./BroadcastMirrorChannel.js\";\n\n/** Options for {@link start} and {@link run}. */\nexport interface ServiceWorkerHostOptions {\n /** BroadcastChannel name shared with the mirrors it serves. */\n readonly channelName?: string;\n /**\n * The collection registry mirrors' `related` registrations resolve against\n * (forwarded to {@link EngineHost.run}). Pass it whenever mirrors use\n * relational reads.\n */\n readonly registry?: Collection.CollectionRegistry;\n /**\n * The node graph and URL-keyed reads for `SubscribeUrl` registrations\n * (#159): a page with no `Client` posts `{ _tag: \"SubscribeUrl\",\n * mirrorId, url }` on the channel and the host pins the URL's windows\n * under that id — forwarded to {@link EngineHost.run}. Build `urls` with\n * `JsonApiQuery.urlReads(nodes)` from `@thomasfosterau/legation-jsonapi`.\n */\n readonly nodes?: Node.NodeRegistry;\n readonly urls?: Client.UrlReads;\n /**\n * The worker's Background Sync listener (#213), from\n * `BackgroundSync.listen({ identity })` at the top level of the worker script. The host\n * attaches its engine for as long as it serves: the listener's `sync`\n * events — including one that woke the worker before the host had booted —\n * flush the journal through `push`, and the tag is registered when the\n * journal cannot drain now (offline, or a terminally failed push). Unset, no listener is served and `sync` events are not flushed.\n */\n readonly backgroundSync?: BackgroundSync.Listener;\n /**\n * Web Push as a poke carrier (#214): install `PushPoke.listen({ identity })`\n * at the top level of the worker script and pass it here. The host attaches\n * its engine, so a recognised push registers the Background Sync (or pulls,\n * with `action: \"pull\"`) once the replica's identity checks out. Unset, the\n * library's push is not answered.\n */\n readonly pushPoke?: PushPoke.Listener;\n}\n\n/** The default channel name; Mirror setups should use the same one. */\nexport const defaultChannelName = \"legation\";\n\n/** The serving host: the engine it serves and what it holds for the mirrors. */\nexport interface Host {\n /** The engine the host serves — the replica a façade reads (`ClientView.fromEngine(engine)`). */\n readonly engine: ClientEngine.ClientEngine;\n /**\n * The registrations the host holds on its mirrors' behalf at this moment\n * (`EngineHost.Host.registrations`): the ledger a façade's coverage check\n * scans, `Coverage.coverageOf(nodes, view.query, host.registrations)`.\n */\n readonly registrations: Effect.Effect<ReadonlyArray<Coverage.Entry>>;\n}\n\n/**\n * Start the host: serve mirrors over the broadcast channel and start the\n * engine's sync loop (pull on poke, push+pull on reconnect), then return\n * the {@link Host}. Both the host and `runSync` fork their fibers into the\n * surrounding scope, so the caller must keep that scope open for as long\n * as it serves (`Effect.never`, as {@link run} does); closing it stops\n * serving.\n */\nexport const start = (\n options: ServiceWorkerHostOptions = {}\n): Effect.Effect<Host, never, ClientEngine.ClientEngine | Scope.Scope> =>\n Effect.gen(function* () {\n const {\n channelName = defaultChannelName,\n registry,\n nodes,\n urls,\n backgroundSync,\n pushPoke,\n } = options;\n const engine = yield* ClientEngine.ClientEngine;\n const channel = yield* BroadcastMirrorChannel.make(channelName);\n const host = yield* EngineHost.run({\n channel,\n engine,\n ...(registry === undefined ? {} : { registry }),\n ...(nodes === undefined ? {} : { nodes }),\n ...(urls === undefined ? {} : { urls }),\n });\n yield* engine.runSync.pipe(Effect.ignore);\n if (backgroundSync !== undefined) yield* backgroundSync.serve(engine);\n if (pushPoke !== undefined) yield* pushPoke.serve(engine);\n return { engine, registrations: host.registrations };\n });\n\n/**\n * Run the host: {@link start}, then serve forever. NEVER returns — both the\n * host and `runSync` fork their fibers into the surrounding scope, so\n * returning would let a caller's `Effect.scoped` tear the whole host down\n * immediately. Interrupt it / close the scope to stop serving.\n */\nexport const run = (\n options: ServiceWorkerHostOptions = {}\n): Effect.Effect<never, never, ClientEngine.ClientEngine | Scope.Scope> =>\n start(options).pipe(Effect.andThen(Effect.never));\n","/**\n * ElectedHost\n *\n * Built-in multi-tab leader election for the no-service-worker degraded\n * topology (#119): exactly one tab runs the full host (ADR-0010's\n * main-thread path — `ClientEngine` + durable store + transports +\n * `BroadcastMirrorChannel` serving), every other tab mirrors it, and a\n * dying leader's replacement resumes the SAME client identity against the\n * SAME durable store — same journal, same rows, same cookie, so the CVR\n * conversation picks up where it left off (ADR-0010).\n *\n * Election is the Web Locks API (`navigator.locks`), held for the tab's\n * whole lifetime once won — there is no voluntary step-down. A tab that\n * loses (or is still waiting) mirrors over the same `BroadcastChannel` name\n * the elected tab hosts on; when its turn comes, the browser grants the\n * lock and this module boots the host in place, so the app's `Client`\n * never needs to be rebuilt — {@link run} hands back one `Client` whose\n * underlying view swaps itself from mirror to host, and a `role` stream\n * purely for observability (devtools, a debug badge).\n *\n * Environments without `navigator.locks` (some embedded webviews) get a\n * documented fallback, not a polyfill: this module logs a warning and\n * becomes the host immediately, which is only correct for a single tab —\n * see the package README's topology section.\n */\nimport type { Collection, Mutation, Node, Query, Transport } from \"@thomasfosterau/legation\";\nimport { Client, ClientStore, TransportError } from \"@thomasfosterau/legation\";\nimport {\n ClientEngine,\n ClientView,\n EngineHost,\n Mirror,\n MirrorProtocol,\n Subscriptions,\n Subset,\n} from \"@thomasfosterau/legation/internal\";\nimport * as Effect from \"effect/Effect\";\nimport * as Exit from \"effect/Exit\";\nimport * as Layer from \"effect/Layer\";\nimport * as Ref from \"effect/Ref\";\nimport * as Scope from \"effect/Scope\";\nimport * as Stream from \"effect/Stream\";\nimport * as SubscriptionRef from \"effect/SubscriptionRef\";\nimport * as BroadcastMirrorChannel from \"./BroadcastMirrorChannel.js\";\nimport * as ServiceWorkerHost from \"./ServiceWorkerHost.js\";\n\n/** Whether this tab is currently serving the engine or mirroring another tab's. */\nexport type Role = \"host\" | \"mirror\";\n\n/** Options for {@link run}. */\nexport interface ElectedHostOptions {\n /** The Web Lock name AND the sync-protocol client group — one election per group. */\n readonly clientGroupId: string;\n readonly nodes: Node.NodeRegistry;\n readonly mutations: Mutation.MutatorRegistry;\n /**\n * `ClientStore` (durable — pair {@link IndexedDbClientStore}) and\n * `ClientTransport`, composed as a Layer so winning the election is what\n * triggers connecting: nothing here opens a socket or an IndexedDB\n * transaction until this tab is actually elected, keeping a losing (or\n * still-waiting) tab's footprint to the mirror alone.\n */\n readonly dependencies: Layer.Layer<ClientStore.ClientStore | Transport.ClientTransport>;\n /**\n * The engine's client identity while this tab hosts. Defaults to\n * `host-${clientGroupId}` — deterministic and shared by construction, so\n * whichever tab wins resumes the SAME journal/rows/cookie regardless of\n * which physical tab or random id it happens to be (a fresh random id\n * per boot would fork the mutation-id sequence and orphan whatever the\n * old host had already journaled).\n */\n readonly hostClientId?: string;\n /** BroadcastChannel name shared with every tab in this group. Defaults to {@link ServiceWorkerHost.defaultChannelName}. */\n readonly channelName?: string;\n /** Forwarded to {@link EngineHost.run} for mirrors' `related` registrations. */\n readonly registry?: Collection.CollectionRegistry;\n /**\n * URL-keyed reads (#159): lets the returned Client `subscribe(url)`, and\n * — with `nodes` — lets the hosting tab answer `SubscribeUrl` from pages\n * without a Client (forwarded to {@link EngineHost.run}).\n */\n readonly urls?: Client.UrlReads;\n readonly queries?: Query.QueryRegistry;\n readonly syncLoop?: ClientEngine.SyncLoopOptions;\n /** Override for tests/non-browser injection. Defaults to `navigator.locks`. */\n readonly locks?: LockManager;\n}\n\n/** What {@link run} returns: one stable Client for the tab, and a stream of its host/mirror role. */\nexport interface ElectedHost {\n /**\n * One stable Client for the tab's whole lifetime: reads and writes go\n * through the mirror until this tab is elected, and through the owned\n * engine from that point on — the switch is transparent to callers (a\n * live `observe`/`related` re-hydrates from the new tier; explicit\n * `subscribe` pins are replayed onto it).\n */\n readonly client: Client.Client;\n /** Purely observational — devtools/debug UI. Never drives application logic. */\n readonly role: Stream.Stream<Role>;\n}\n\n/** Resolve the ambient `navigator.locks`, `undefined` where it (or `navigator`) doesn't exist. */\nconst globalLocks = (): LockManager | undefined => globalThis.navigator?.locks;\n\n/**\n * Wait to be granted `name` (Web Lock, exclusive mode), then hold it until\n * this effect's scope closes. While waiting, interruption aborts the\n * pending request (`LockManager.request`'s own `signal` option) instead of\n * leaving it queued forever.\n */\nconst electLeader = (locks: LockManager, name: string): Effect.Effect<void, never, Scope.Scope> =>\n Effect.acquireRelease(\n Effect.callback<() => void>((resume, signal) => {\n locks\n .request(\n name,\n { mode: \"exclusive\", signal },\n () =>\n new Promise<void>((resolveHeld) => {\n resume(Effect.succeed(() => resolveHeld()));\n })\n )\n .catch(() => undefined); // interrupted before ever winning: nothing to resume\n }),\n (release) => Effect.sync(release),\n { interruptible: true }\n ).pipe(Effect.asVoid);\n\n/**\n * A `ClientView` over a `SubscriptionRef` of the currently-active view:\n * one-shot calls (`query`/`mutate`/…) always hit whatever is current;\n * `observe`/`observeNamed`/`related` re-subscribe themselves against the\n * new view the moment it changes (`Stream.switchMap`, tearing the old\n * subscription down first — a mirror's own `unsubscribe`/interrupt path);\n * explicit pins (`subscribe`/`subscribeQuery`) have no ongoing stream to\n * redo that for them, so this keeps its own ledger and replays it onto\n * every newly-active view.\n */\nconst switchingView = (\n viewRef: SubscriptionRef.SubscriptionRef<ClientView.ClientView>\n): Effect.Effect<ClientView.ClientView, never, Scope.Scope> =>\n Effect.gen(function* () {\n const pinnedSubsets = yield* Ref.make(new Map<string, Subset.Subset>());\n const pinnedNamed = yield* Ref.make(\n new Map<string, { readonly name: string; readonly args: unknown }>()\n );\n\n // Skip the CURRENT value (already active) and replay pins onto every\n // view this switches to AFTER that.\n yield* Effect.forkScoped(\n Stream.runForEach(Stream.drop(SubscriptionRef.changes(viewRef), 1), (view) =>\n Effect.gen(function* () {\n const subsets = yield* Ref.get(pinnedSubsets);\n const named = yield* Ref.get(pinnedNamed);\n yield* Effect.forEach(\n subsets.values(),\n (subset) => view.subscribe(subset).pipe(Effect.ignore),\n {\n discard: true,\n }\n );\n yield* Effect.forEach(\n named.values(),\n (entry) => view.subscribeQuery(entry.name, entry.args).pipe(Effect.ignore),\n { discard: true }\n );\n })\n ).pipe(Effect.ignore)\n );\n\n const current = SubscriptionRef.get(viewRef);\n const withCurrent = <A, E>(f: (view: ClientView.ClientView) => Effect.Effect<A, E>) =>\n Effect.flatMap(current, f);\n\n return {\n query: (subset) => withCurrent((view) => view.query(subset)),\n observe: (subset) =>\n SubscriptionRef.changes(viewRef).pipe(Stream.switchMap((view) => view.observe(subset))),\n observeChanges: (subset, changeOptions) =>\n SubscriptionRef.changes(viewRef).pipe(\n Stream.switchMap((view) => view.observeChanges(subset, changeOptions))\n ),\n subscribe: (subset) =>\n Ref.update(pinnedSubsets, (map) => new Map(map).set(Subset.hash(subset), subset)).pipe(\n Effect.andThen(withCurrent((view) => view.subscribe(subset)))\n ),\n unsubscribe: (subset) =>\n Ref.update(pinnedSubsets, (map) => {\n const next = new Map(map);\n next.delete(Subset.hash(subset));\n return next;\n }).pipe(Effect.andThen(withCurrent((view) => view.unsubscribe(subset)))),\n subscribeQuery: (name, args) =>\n Ref.update(pinnedNamed, (map) =>\n new Map(map).set(MirrorProtocol.namedKey(name, args), { name, args })\n ).pipe(Effect.andThen(withCurrent((view) => view.subscribeQuery(name, args)))),\n unsubscribeQuery: (name, args) =>\n Ref.update(pinnedNamed, (map) => {\n const next = new Map(map);\n next.delete(MirrorProtocol.namedKey(name, args));\n return next;\n }).pipe(Effect.andThen(withCurrent((view) => view.unsubscribeQuery(name, args)))),\n queryNamed: (name, args) => withCurrent((view) => view.queryNamed(name, args)),\n pulled: (key) => withCurrent((view) => view.pulled(key)),\n observeNamed: (name, args) =>\n SubscriptionRef.changes(viewRef).pipe(\n Stream.switchMap((view) => view.observeNamed(name, args))\n ),\n related: (registry, collectionName, subset, edges) =>\n Effect.succeed(\n SubscriptionRef.changes(viewRef).pipe(\n Stream.switchMap((view) =>\n Stream.unwrap(view.related(registry, collectionName, subset, edges))\n )\n )\n ),\n // A held edge registration has no stream to redo the switch for it:\n // re-pin on every view this switches to (the current one first —\n // `changes` replays it), releasing the previous tier's hold.\n pinEdges: (registry, collectionName, subset, edges) =>\n Effect.gen(function* () {\n const scope = yield* Effect.scope;\n const held = yield* Ref.make<Scope.Closeable | undefined>(undefined);\n yield* Effect.forkIn(\n Stream.runForEach(SubscriptionRef.changes(viewRef), (view) =>\n Effect.gen(function* () {\n const previous = yield* Ref.getAndSet(held, undefined);\n if (previous !== undefined) yield* Scope.close(previous, Exit.void);\n const child = yield* Scope.fork(scope);\n yield* view\n .pinEdges(registry, collectionName, subset, edges)\n .pipe(Scope.provide(child));\n yield* Ref.set(held, child);\n })\n ).pipe(Effect.ignore),\n scope\n );\n }),\n read: (subsets) =>\n withCurrent((view) =>\n view.read === undefined\n ? Effect.fail(\n new TransportError({\n message: \"ElectedHost: no fallthrough read available on this tier\",\n })\n )\n : view.read(subsets)\n ),\n mutate: (mutator, args) => withCurrent((view) => view.mutate(mutator, args)),\n mutateWithAck: (mutator, args) => withCurrent((view) => view.mutateWithAck(mutator, args)),\n mutationStates: SubscriptionRef.changes(viewRef).pipe(\n Stream.switchMap((view) => view.mutationStates)\n ),\n };\n });\n\n/**\n * Elect this tab into hosting `clientGroupId`'s engine — full main-thread\n * host (ADR-0010) while the lock is held, a mirror over the same channel\n * otherwise — and hand back one `Client` that works unchanged across the\n * transition, plus a `role` stream for observability.\n */\nexport const run = (options: ElectedHostOptions): Effect.Effect<ElectedHost, never, Scope.Scope> =>\n Effect.gen(function* () {\n const channelName = options.channelName ?? ServiceWorkerHost.defaultChannelName;\n const locks = options.locks ?? globalLocks();\n\n const mirrorScope = yield* Scope.make();\n const mirrorChannel = yield* BroadcastMirrorChannel.make(channelName).pipe(\n Scope.provide(mirrorScope)\n );\n const mirror = yield* Mirror.make({ channel: mirrorChannel, mutators: options.mutations }).pipe(\n Scope.provide(mirrorScope)\n );\n\n const viewRef = yield* SubscriptionRef.make<ClientView.ClientView>(mirror);\n const roleRef = yield* SubscriptionRef.make<Role>(\"mirror\");\n\n const becomeHost = Effect.gen(function* () {\n const hostClientId = options.hostClientId ?? `host-${options.clientGroupId}`;\n // One counted voice for both fronts over this engine — the host\n // serving other tabs and this tab's own view — so a mirror releasing\n // a window never drops one this tab's Client still pins.\n const engine = Subscriptions.counted(\n yield* ClientEngine.make({\n clientId: hostClientId,\n clientGroupId: options.clientGroupId,\n mutators: options.mutations,\n ...(options.queries === undefined ? {} : { queries: options.queries }),\n ...(options.syncLoop === undefined ? {} : { syncLoop: options.syncLoop }),\n }).pipe(Effect.provide(options.dependencies))\n );\n const hostChannel = yield* BroadcastMirrorChannel.make(channelName);\n yield* EngineHost.run({\n channel: hostChannel,\n engine,\n nodes: options.nodes,\n ...(options.registry === undefined ? {} : { registry: options.registry }),\n ...(options.urls === undefined ? {} : { urls: options.urls }),\n });\n yield* Effect.forkScoped(engine.runSync.pipe(Effect.ignore));\n yield* SubscriptionRef.set(viewRef, ClientView.fromEngine(engine));\n yield* SubscriptionRef.set(roleRef, \"host\");\n // The mirror is no longer reachable through the switching view (and,\n // being the same tab, could never mirror its own broadcast anyway) —\n // release its heartbeat/channel rather than leave it running inert.\n yield* Scope.close(mirrorScope, Exit.void);\n });\n\n if (locks === undefined) {\n yield* Effect.logWarning(\n \"ElectedHost: navigator.locks is unavailable — becoming host immediately. \" +\n \"This is only correct for a single tab; see the README's topology docs.\"\n );\n yield* becomeHost;\n } else {\n // A failure here (e.g. the durable store or transport failing to\n // construct) leaves this tab mirroring forever rather than crashing\n // the fiber silently — logged so it's visible, not asserted against,\n // since there's nothing more useful to do with it from here.\n yield* Effect.forkScoped(\n electLeader(locks, options.clientGroupId).pipe(\n Effect.andThen(becomeHost),\n Effect.tapCause((cause) => Effect.logError(\"ElectedHost: failed to become host\", cause)),\n Effect.ignore\n )\n );\n }\n\n const view = yield* switchingView(viewRef);\n const client = Client.fromView(\n options.nodes,\n view,\n options.urls === undefined ? {} : { urls: options.urls }\n );\n return { client, role: SubscriptionRef.changes(roleRef) };\n });\n","/**\n * CacheApiHttpCache\n *\n * The browser implementation of core's HTTP cache module (ADR-0008 narrows\n * ADR-0004's stale-marking semantics to exactly this module): a\n * ResponseCacheStore over the Cache API, plus the stale-while-revalidate\n * fetch handler service workers wire into their fetch events.\n *\n * Cache metadata (freshness window, tags) travels in a response header on\n * the stored copy, so entries are self-describing and survive across\n * service-worker restarts. Structural `CacheLike` typing keeps the module\n * testable without browser globals.\n */\n\nimport { StoreError, TransportError } from \"@thomasfosterau/legation\";\nimport { HttpCache } from \"@thomasfosterau/legation/internal\";\nimport * as Clock from \"effect/Clock\";\nimport * as Duration from \"effect/Duration\";\nimport * as Effect from \"effect/Effect\";\nimport * as Option from \"effect/Option\";\n\n/** The subset of the Cache API this store uses. */\nexport interface CacheLike {\n readonly match: (request: Request) => Promise<Response | undefined>;\n readonly put: (request: Request, response: Response) => Promise<void>;\n readonly delete: (request: Request) => Promise<boolean>;\n readonly keys: () => Promise<ReadonlyArray<Request>>;\n}\n\n/** The slice of the Cache API's `CacheStorage` that {@link open} uses (`caches` in a browser). */\nexport interface CacheStorageLike {\n readonly open: (name: string) => Promise<CacheLike>;\n}\n\n/** Header carrying the serialized CachedResponseMeta on stored copies. */\nexport const metaHeader = \"x-legation-cache-meta\";\n\nconst storeError = (message: string) => (cause: unknown) => new StoreError({ message, cause });\n\nconst tryCache = <A>(message: string, run: () => Promise<A>): Effect.Effect<A, StoreError> =>\n Effect.tryPromise({ try: run, catch: storeError(message) });\n\n/** Decode a stored meta header; `None` when it is malformed (foreign or corrupt). */\nconst parseMeta = Option.liftThrowable(\n (raw: string) => JSON.parse(decodeURIComponent(raw)) as HttpCache.CachedResponseMeta\n);\n\nconst readMeta = (response: Response): HttpCache.CachedResponseMeta | undefined => {\n const raw = response.headers.get(metaHeader);\n if (raw === null) return undefined;\n return Option.getOrUndefined(parseMeta(raw));\n};\n\n/** ResponseCacheStore over one named Cache API cache. */\nexport const make = (cache: CacheLike): HttpCache.ResponseCacheStore => ({\n match: (request) =>\n tryCache(`Cache match failed for ${request.url}`, () => cache.match(request)).pipe(\n Effect.map((response) => {\n if (response === undefined) return undefined;\n const meta = readMeta(response);\n // Entries without our metadata are foreign; treat as a miss.\n return meta === undefined ? undefined : { response, meta };\n })\n ),\n put: (request, response, meta) =>\n tryCache(`Cache put failed for ${request.url}`, async () => {\n const headers = new Headers(response.headers);\n headers.set(metaHeader, encodeURIComponent(JSON.stringify(meta)));\n const stored = new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n await cache.put(request, stored);\n }),\n delete: (request) =>\n Effect.asVoid(tryCache(`Cache delete failed for ${request.url}`, () => cache.delete(request))),\n invalidateTags: (tags) =>\n Effect.gen(function* () {\n const requests = yield* tryCache(\"Cache enumeration failed\", () => cache.keys());\n yield* Effect.forEach(\n requests,\n (request) =>\n Effect.gen(function* () {\n const response = yield* tryCache(`Cache match failed for ${request.url}`, () =>\n cache.match(request)\n );\n const meta = response === undefined ? undefined : readMeta(response);\n if (meta !== undefined && meta.tags.some((tag) => tags.includes(tag))) {\n yield* tryCache(`Cache delete failed for ${request.url}`, () =>\n cache.delete(request)\n );\n }\n }),\n { concurrency: 1, discard: true }\n );\n }),\n});\n\n/** Open a named cache (defaults to the global `caches`). */\nexport const open = (\n name: string,\n cacheStorage?: CacheStorageLike\n): Effect.Effect<HttpCache.ResponseCacheStore, StoreError> =>\n tryCache(`Failed to open cache \"${name}\"`, () =>\n (cacheStorage ?? (globalThis.caches as CacheStorageLike)).open(name)\n ).pipe(Effect.map(make));\n\n// ─── Fetch handler ───────────────────────────────────────────────────────────\n\n/** Options for {@link fetchHandler}. */\nexport interface FetchHandlerOptions {\n readonly store: HttpCache.ResponseCacheStore;\n /** Per-request policy; `undefined` bypasses the cache entirely. */\n readonly policy: (request: Request) => HttpCache.HttpCachePolicy | undefined;\n /** The network. Defaults to the global `fetch`. */\n readonly fetch?: (request: Request) => Promise<Response>;\n}\n\n/** What {@link fetchHandler} answers a request with. */\nexport interface FetchHandlerResult {\n readonly response: Response;\n /**\n * Present when a stale entry was served: refreshes the cache in the\n * background. Service workers pass it to `event.waitUntil`.\n */\n readonly revalidate?: Effect.Effect<void, StoreError | TransportError> | undefined;\n}\n\n/**\n * Build the stale-while-revalidate handler: fresh entries are served from\n * cache, stale-but-unexpired entries are served immediately with a\n * background revalidation, and expired or missing entries reload from the\n * network (caching ok GET responses).\n */\nexport const fetchHandler = (options: FetchHandlerOptions) => {\n const network = (request: Request) =>\n Effect.tryPromise({\n try: () => (options.fetch ?? globalThis.fetch)(request),\n catch: (cause) => new TransportError({ message: `Fetch failed for ${request.url}`, cause }),\n });\n\n const fetchAndStore = (\n request: Request,\n policy: HttpCache.HttpCachePolicy\n ): Effect.Effect<Response, StoreError | TransportError> =>\n Effect.gen(function* () {\n const response = yield* network(request);\n if (!response.ok) return response;\n const now = yield* Clock.currentTimeMillis;\n const timeToLive = policy.timeToLive;\n const meta: HttpCache.CachedResponseMeta = {\n url: request.url,\n storedAt: now,\n staleAt: now + Duration.toMillis(policy.staleTime),\n expiresAt: timeToLive === undefined ? undefined : now + Duration.toMillis(timeToLive),\n tags: policy.tags ?? [],\n };\n // Store a clone; hand the original back to the caller.\n yield* options.store.put(request, response.clone(), meta);\n return response;\n });\n\n return (request: Request): Effect.Effect<FetchHandlerResult, StoreError | TransportError> =>\n Effect.gen(function* () {\n const policy = request.method === \"GET\" ? options.policy(request) : undefined;\n if (policy === undefined) {\n return { response: yield* network(request), revalidate: undefined };\n }\n const cached = yield* options.store.match(request);\n const now = yield* Clock.currentTimeMillis;\n if (cached !== undefined) {\n const { meta, response } = cached;\n if (now < meta.staleAt) {\n return { response, revalidate: undefined };\n }\n if (meta.expiresAt === undefined || now < meta.expiresAt) {\n return {\n response,\n revalidate: Effect.asVoid(fetchAndStore(request, policy)),\n };\n }\n }\n return { response: yield* fetchAndStore(request, policy), revalidate: undefined };\n });\n};\n","/**\n * HttpCacheInvalidation\n *\n * Drives HTTP-cache tag invalidation from collection and row changes (issue\n * #49, extended per-row by #207): as synced changes land, evict the cached\n * responses tagged for the collections and individual rows that moved. This\n * is the concrete wiring behind the collection-driven invalidation core's\n * `HttpCache` module promises — the missing link between a pull arriving and\n * the stale-while-revalidate cache noticing.\n *\n * Two tag granularities share one convention (see CONTEXT.md's \"Tag\" entry;\n * #206's edge/KV invalidation sink reuses this same spelling):\n *\n * - Coarse: `collection:{name}` (the default {@link defaultTagFor}) — every\n * response enumerating (part of) a collection carries it.\n * - Fine: `collection:{name}:{id}` (the default {@link defaultFineTagFor}) —\n * a response carries one per row it actually contains, up to\n * {@link FINE_TAG_ROW_BUDGET} (see {@link tagsFor}).\n *\n * A row change evicts **both**: the fine tag for that row, and the coarse tag\n * for its collection. The coarse eviction is not redundant, even though every\n * row's collection is already implied by the row itself — see the comment at\n * its use in {@link invalidateOnChanges}.\n *\n * The change feed comes from an engine's `engineEvents` via\n * {@link collectionChanges}, so a synced change reaching a client with no\n * pending journal (the common case, where a pull emits a `Patch` and no\n * `ViewRebuilt`) still invalidates: it reads the touched collections and rows\n * off the `PullCompleted` event, not off `ViewRebuilt` alone. `ViewRebuilt`\n * carries no row ids (it is the conservative \"something in these collections\n * shifted\" fallback), so it only ever contributes coarse tags.\n */\n\nimport type { ClientEngine } from \"@thomasfosterau/legation/internal\";\nimport { HttpCache, Telemetry } from \"@thomasfosterau/legation/internal\";\nimport * as Arr from \"effect/Array\";\nimport * as Effect from \"effect/Effect\";\nimport * as Metric from \"effect/Metric\";\nimport * as Stream from \"effect/Stream\";\nimport type * as Scope from \"effect/Scope\";\n\n/** The default coarse tag convention: `collection:{name}` (`HttpCache.tagFor`, shared with the edge, #206). */\nexport const defaultTagFor: (collection: string) => string = HttpCache.tagFor;\n\n/** The default fine, per-row tag convention: `collection:{name}:{id}` (`HttpCache.fineTagFor`). */\nexport const defaultFineTagFor: (collection: string, id: string) => string = HttpCache.fineTagFor;\n\n/**\n * The fine-tag row budget: a response enumerating more rows than this is\n * tagged coarse-only (see {@link tagsFor}) rather than carrying one fine tag\n * per row — a document listing thousands of rows cannot carry thousands of\n * tags on a bounded header.\n *\n * Derived against the real ceiling on the header this convention rides on —\n * `x-legation-cache-meta` (`CacheApiHttpCache.metaHeader`), which carries\n * `encodeURIComponent(JSON.stringify(meta))` and so must fit under Node's\n * default `--max-http-header-size` of 16 KiB (16384 bytes) wherever responses are\n * served by a Node HTTP server:\n *\n * - `encodeURIComponent` expands every byte outside its unreserved set to\n * `%XX` (3 bytes), so 3x the raw byte length is a safe upper bound on\n * encoded size regardless of which characters a collection name or row id\n * use.\n * - Reserve 4096 bytes (16 KiB / 4) for the rest of `CachedResponseMeta` —\n * `url`, three numeric timestamps, the coarse tag, and JSON's own\n * braces/keys/quotes — leaving `16384 - 4096 = 12288` bytes for fine tags.\n * - One fine tag, worst case: a JSON array entry `\"collection:{name}:{id}\",`\n * with a 40-char collection name and a 40-char row id (a UUIDv4 is 36\n * chars) is `2 (quotes) + 11 (\"collection:\") + 40 + 1 (\":\") + 40 + 1 (\",\")\n * = 95` raw bytes, i.e. at most `95 * 3 = 285` encoded bytes.\n * - `N = floor(12288 / 285) = 43`.\n */\nexport const FINE_TAG_ROW_BUDGET = 43;\n\n/** The collections and individual rows one engine event touched (`HttpCache.Touched`). */\nexport type Touched = HttpCache.Touched;\n\nconst EMPTY_TOUCHED: Touched = { collections: [], rows: [] };\n\n/** What an engine event touched, or nothing for other events. */\nconst touchedBy = (event: ClientEngine.EngineEvent): Touched => {\n switch (event._tag) {\n case \"PullCompleted\":\n return { collections: event.collections, rows: event.rows };\n case \"ViewRebuilt\":\n return { collections: event.collections ?? [], rows: [] };\n default:\n return EMPTY_TOUCHED;\n }\n};\n\n/**\n * Derive the change feed from an engine's event stream: the distinct\n * collections and rows each `PullCompleted` (always present) and scoped\n * `ViewRebuilt` (when present) touched, dropping events that touched\n * nothing. Feed the result to {@link invalidateOnChanges}.\n */\nexport const collectionChanges = (\n events: Stream.Stream<ClientEngine.EngineEvent>\n): Stream.Stream<Touched> =>\n events.pipe(\n Stream.map(touchedBy),\n Stream.filter(\n (touched) =>\n Arr.isReadonlyArrayNonEmpty(touched.collections) ||\n Arr.isReadonlyArrayNonEmpty(touched.rows)\n )\n );\n\n/** Options for {@link invalidateOnChanges}. */\nexport interface InvalidateOnChangesOptions {\n readonly store: HttpCache.ResponseCacheStore;\n /** Touched collections/rows per change (e.g. from {@link collectionChanges}). */\n readonly changes: Stream.Stream<Touched>;\n /** Map a collection name to its coarse cache tag; defaults to {@link defaultTagFor}. */\n readonly tagFor?: (collection: string) => string;\n /** Map a row to its fine cache tag; defaults to {@link defaultFineTagFor}. */\n readonly fineTagFor?: (collection: string, id: string) => string;\n}\n\n/**\n * Fork a scoped fibre that, per emitted change, evicts every cached response\n * tagged for the touched collections (coarse) **and** the touched rows\n * (fine) — `store.invalidateTags`. Invalidation cost stays proportional to\n * the store's tag enumeration — this scans no more than `invalidateTags`\n * already does. Runs until the scope closes; a store error is logged and\n * swallowed, never fatal to the fibre, so a transient cache failure doesn't\n * stop future invalidations.\n */\nexport const invalidateOnChanges = (\n options: InvalidateOnChangesOptions\n): Effect.Effect<void, never, Scope.Scope> =>\n Effect.gen(function* () {\n const tagFor = options.tagFor ?? defaultTagFor;\n const fineTagFor = options.fineTagFor ?? defaultFineTagFor;\n yield* Effect.forkScoped(\n Stream.runForEach(options.changes, (touched) => {\n // `evictionTags` is the union of the coarse and fine tags; the coarse\n // half is not redundant (see its doc comment), so do not narrow it to\n // the fine tags.\n const tags = HttpCache.evictionTags(touched, { tagFor, fineTagFor });\n return Arr.isReadonlyArrayEmpty(tags)\n ? Effect.void\n : options.store\n .invalidateTags(tags)\n .pipe(\n Effect.catch((error) => Effect.logWarning(\"HTTP cache invalidation failed\", error))\n );\n })\n );\n });\n\n// ─── Response tagging ────────────────────────────────────────────────────────\n\n/** Options for {@link tagsFor}. */\nexport interface TagsForOptions {\n /** Map a collection name to its coarse cache tag; defaults to {@link defaultTagFor}. */\n readonly tagFor?: (collection: string) => string;\n /** Map a row to its fine cache tag; defaults to {@link defaultFineTagFor}. */\n readonly fineTagFor?: (collection: string, id: string) => string;\n}\n\n/** What {@link tagsFor} returns. */\nexport interface TagsForResult {\n /** The tags to attach to the response (`HttpCachePolicy.tags`). */\n readonly tags: ReadonlyArray<string>;\n /**\n * `true` when `rowIds.length` exceeded {@link FINE_TAG_ROW_BUDGET}, so\n * `tags` carries the coarse tag only rather than one fine tag per row.\n */\n readonly budgetExceeded: boolean;\n}\n\n/**\n * Build the cache tags for a response that enumerates `rowIds` of\n * `collection`: the coarse tag, plus one fine tag per row when the row count\n * is within {@link FINE_TAG_ROW_BUDGET} — beyond it, the coarse tag alone\n * (`budgetExceeded: true`), since a fine tag per row would blow the header\n * budget the tag rides on. Pure and synchronous, for use building\n * `HttpCachePolicy.tags`; pair with {@link tagsForObserved} to also record\n * the fallback when it happens.\n */\nexport const tagsFor = (\n collection: string,\n rowIds: ReadonlyArray<string>,\n options: TagsForOptions = {}\n): TagsForResult => {\n const tagFor = options.tagFor ?? defaultTagFor;\n const coarse = tagFor(collection);\n if (rowIds.length > FINE_TAG_ROW_BUDGET) {\n return { tags: [coarse], budgetExceeded: true };\n }\n const fineTagFor = options.fineTagFor ?? defaultFineTagFor;\n return {\n tags: [coarse, ...rowIds.map((id) => fineTagFor(collection, id))],\n budgetExceeded: false,\n };\n};\n\n/**\n * {@link tagsFor}, plus an observable fallback: a response that exceeded the\n * budget is never silently coarse — it logs a warning and increments\n * `legation_cache_tag_budget_exceeded_total` (`Telemetry.cacheTagBudgetExceededTotal`),\n * following the same \"safely degraded, but counted\" idiom as the engine's\n * other bounded-hub fallbacks (`legation_hints_dropped_total`,\n * `legation_change_collapses_total`).\n */\nexport const tagsForObserved = (\n collection: string,\n rowIds: ReadonlyArray<string>,\n options: TagsForOptions = {}\n): Effect.Effect<TagsForResult> =>\n Effect.gen(function* () {\n const result = tagsFor(collection, rowIds, options);\n if (result.budgetExceeded) {\n yield* Effect.logWarning(\n `HTTP cache tag budget exceeded for collection \"${collection}\": ` +\n `${rowIds.length} rows > ${FINE_TAG_ROW_BUDGET}; tagged coarse-only`\n );\n yield* Metric.update(Telemetry.cacheTagBudgetExceededTotal, 1);\n }\n return result;\n });\n","/**\n * BrowserConnectivity\n *\n * Signals belong at the transport seam, not in core (issue #33). This layers\n * the browser's connectivity and focus signals onto any `ClientTransport`:\n *\n * - `navigator.onLine` is **combined with the wrapped transport's own status**\n * (worst of the two wins), so a carrier with real awareness —\n * `Transport.withInferredStatus`, `withFallback`, a status-aware socket —\n * keeps its signal, while the browser adds what only it knows (the radio is\n * off). For carriers that report themselves permanently \"connected\" (the\n * plain RPC transport), the navigator signal is the truthful one.\n * - Where there is a `window` (a page), the `online`/`offline` events drive\n * that signal. A service worker has no `window` and no such events, so there\n * the signal is a probe instead: `navigator.onLine` is read every\n * `pollInterval` and a status change is emitted when it flips (#328). The\n * reconnect edge then reaches the sync loop as it would from an event.\n * - `visibilitychange` to visible surfaces a poke (\"pull soon\"), so returning\n * to a tab converges it — the browser analogue of TanStack Query's window\n * focus refetching.\n *\n * push/pull pass straight through; only `status`, `statusChanges`, and `pokes`\n * are augmented. All listeners and probes are torn down when the scope closes.\n *\n * `statusChanges` has one addition to the combination above: the reconnect\n * edge. Each navigator `disconnected` → `connected` flip emits `connected`\n * once, whatever the carrier reports, in a worker or a window (#328). The sync\n * loop's push and pull therefore run at once, and a carrier that really is\n * down fails that push and backs off as usual. The next change then emits the\n * combined value again, which may read `disconnected`. `status` is not\n * affected: it always reads the worst of the two signals. So `statusChanges` is\n * an edge trigger for the sync loop, not a mirror of `status`: after a\n * reconnect edge it can read `connected` while `status` reads `disconnected`.\n * A connection label should read `status`.\n *\n * Auto-push does not consult this status, by design (#328). A push attempted\n * while `disconnected` goes out anyway: `navigator.onLine` is a hint and can be\n * wrong (in #315 it read `false` while the worker's WebSocket still worked),\n * and a failed push already backs off on its own retry policy. The reconnect\n * edge runs an immediate push and pull, so a journal that a failed push left\n * behind drains as soon as the navigator reports the return, without waiting on\n * the backoff.\n */\n\nimport { Transport } from \"@thomasfosterau/legation\";\nimport type { Protocol } from \"@thomasfosterau/legation/internal\";\nimport * as Duration from \"effect/Duration\";\nimport * as Effect from \"effect/Effect\";\nimport * as P from \"effect/Predicate\";\nimport * as Queue from \"effect/Queue\";\nimport * as Schedule from \"effect/Schedule\";\nimport * as Scope from \"effect/Scope\";\nimport * as Stream from \"effect/Stream\";\nimport * as SubscriptionRef from \"effect/SubscriptionRef\";\n\n/**\n * Options for {@link withBrowserConnectivity}.\n */\nexport interface BrowserConnectivityOptions {\n /**\n * How often a runtime with no `window` (a service worker) reads\n * `navigator.onLine` to detect a flip, since it has no `online`/`offline`\n * events. Defaults to `\"5 seconds\"`. The interval bounds how long a\n * reconnect takes to reach the sync loop. Ignored where `window` exists.\n */\n readonly pollInterval?: Duration.Input | undefined;\n}\n\n/** The global `navigator` when it reports `onLine`, else undefined (SSR, or a runtime without it). */\nconst globalNavigator = (): Navigator | undefined =>\n P.isObjectOrArray(globalThis.navigator) && P.isBoolean(globalThis.navigator.onLine)\n ? globalThis.navigator\n : undefined;\n\n/** Current `navigator.onLine` on the global scope, treating a missing navigator as online. */\nconst navigatorOnline = (): boolean => globalNavigator()?.onLine ?? true;\n\n/**\n * Register a DOM event listener on `target` for the scope's lifetime. No-op\n * when `target` is undefined (e.g. server-side rendering).\n */\nconst onEvent = (\n target: EventTarget | undefined,\n type: string,\n handler: () => void\n): Effect.Effect<void, never, Scope.Scope> =>\n target === undefined\n ? Effect.void\n : Effect.acquireRelease(\n Effect.sync(() => {\n target.addEventListener(type, handler);\n }),\n () => Effect.sync(() => target.removeEventListener(type, handler))\n ).pipe(Effect.asVoid);\n\n/** The pessimistic combination of two status signals. */\nconst worst = (\n a: Transport.ConnectionStatus,\n b: Transport.ConnectionStatus\n): Transport.ConnectionStatus =>\n a === \"disconnected\" || b === \"disconnected\"\n ? \"disconnected\"\n : a === \"connecting\" || b === \"connecting\"\n ? \"connecting\"\n : \"connected\";\n\n/**\n * Wrap a `ClientTransport` so its connection status combines\n * `navigator.onLine` with the transport's own signal, and a return-to-visible\n * surfaces a poke. Where there is no `window` (a service worker), the\n * navigator signal is probed every `pollInterval` instead of listened for.\n * Scoped: forked fibres and DOM listeners are torn down when the scope closes.\n */\nexport const withBrowserConnectivity = (\n transport: Transport.ClientTransport,\n options: BrowserConnectivityOptions = {}\n): Effect.Effect<Transport.ClientTransport, never, Scope.Scope> =>\n Effect.gen(function* () {\n const win: EventTarget | undefined = globalThis.window;\n const doc: (EventTarget & { readonly visibilityState?: string }) | undefined =\n globalThis.document;\n\n // Read once, at construction: the initial status and the probe's baseline\n // are the same reading.\n const initialOnline = navigatorOnline();\n const status = yield* SubscriptionRef.make<Transport.ConnectionStatus>(\n initialOnline ? \"connected\" : \"disconnected\"\n );\n // DOM callbacks are synchronous; bridge them into Effect via unsafe offers.\n const statusEvents = yield* Queue.unbounded<Transport.ConnectionStatus>();\n const focusPokes = yield* Queue.unbounded<Protocol.Poke>();\n\n yield* Effect.forkScoped(\n Stream.fromQueue(statusEvents).pipe(\n Stream.runForEach((next) => SubscriptionRef.set(status, next))\n )\n );\n\n if (win !== undefined) {\n yield* onEvent(win, \"online\", () => Queue.offerUnsafe(statusEvents, \"connected\"));\n yield* onEvent(win, \"offline\", () => Queue.offerUnsafe(statusEvents, \"disconnected\"));\n } else if (globalNavigator() !== undefined) {\n // No window, so no online/offline events: probe the navigator for a flip.\n // The last reading is compared, not the status ref, so a flip is emitted\n // once and an unchanged reading emits nothing.\n let lastOnline = initialOnline;\n yield* Effect.forkScoped(\n Effect.repeat(\n Effect.sync(() => {\n const online = navigatorOnline();\n if (online !== lastOnline) {\n lastOnline = online;\n Queue.offerUnsafe(statusEvents, online ? \"connected\" : \"disconnected\");\n }\n }),\n Schedule.spaced(options.pollInterval ?? \"5 seconds\")\n )\n );\n }\n yield* onEvent(doc, \"visibilitychange\", () => {\n if (doc?.visibilityState === \"visible\") {\n Queue.offerUnsafe(focusPokes, { reason: \"visibilitychange\" });\n }\n });\n\n return {\n push: transport.push,\n pull: transport.pull,\n pokes: Stream.merge(transport.pokes, Stream.fromQueue(focusPokes)),\n status: Effect.zipWith(SubscriptionRef.get(status), transport.status, worst),\n // Both sources emit their current value first (the seam's contract), so\n // the combination emits promptly; dedup keeps reconnect triggers calm.\n // The reconnect edge: a navigator flip from `disconnected` to `connected`\n // emits `connected` even if the carrier reads `disconnected` (#328).\n statusChanges: Stream.zipLatest(\n SubscriptionRef.changes(status),\n transport.statusChanges\n ).pipe(\n Stream.mapAccum(\n (): Transport.ConnectionStatus | undefined => undefined,\n (previousNavigator, [navigatorStatus, transportStatus]) => {\n const reconnect =\n previousNavigator === \"disconnected\" && navigatorStatus === \"connected\";\n const emitted = reconnect ? \"connected\" : worst(navigatorStatus, transportStatus);\n return [navigatorStatus, [emitted]] as const;\n }\n ),\n Stream.changesWith((a, b) => a === b)\n ),\n };\n });\n","/**\n * BackgroundSync\n *\n * Flush the journal after the tab is gone (#213). `BrowserConnectivity`\n * covers every case where a page is open — `online`/`offline`, and a\n * return-to-visible as a poke — but by construction none of those fire when\n * there is no page. A write journaled offline and then left behind by a\n * closed tab would wait until the user came back. Background Sync is the\n * browser's answer: the page (or the worker, while a page is open) registers\n * a tag, and when the browser judges connectivity to be back it wakes the\n * service worker with a `sync` event for that tag — tab or no tab.\n *\n * The flush **is `ClientEngine.push`**, not a replay loop. The journal is\n * already ordered, and every entry carries a per-client mutation id the\n * backend's ledger applies exactly once, so:\n *\n * - a push that landed but whose response was lost is re-sent and **skipped**\n * by the ledger, never applied twice — no client-generated-id-plus-re-fetch\n * scheme is needed to prove a retry was one's own;\n * - a write the backend refuses is a **permanent rejection**: acknowledged\n * and skipped so it cannot wedge the entries behind it, its optimistic\n * effect dropped, and the refusal kept in `mutationStates` with its reason\n * — surfaced, not discarded. The user's write never silently vanishes.\n *\n * A hand-rolled outbox that replays raw HTTP requests can have neither\n * property; that is the reason to delete one in favour of this.\n *\n * There is no retry ladder here either. A flush that fails on the transport\n * rejects the event's `waitUntil` promise, which is the browser's own cue to\n * reschedule the sync with its own backoff; the journal is untouched, and the\n * next connectivity signal flushes it regardless.\n *\n * ## The three pieces\n *\n * - {@link listen} — the worker's `sync` listener. Call it **synchronously at\n * the top level of the worker script**: a worker woken for a `sync` event\n * evaluates its script and dispatches the event straight after, so a\n * listener added once an Effect program has opened IndexedDB and built the\n * engine would miss exactly the wake this module exists for. The listener\n * holds the event open until a host attaches, then flushes.\n * - `ServiceWorkerHost.start({ backgroundSync: listener })` — attaches the\n * host's engine to the listener for the host's lifetime, and registers the\n * tag when the journal cannot drain now (see below).\n * - {@link register} — the page-side registration, for an app that wants to\n * ask from the page when a write could not be sent. Best-effort by design.\n *\n * ## Register only when the journal cannot drain\n *\n * Online, Chromium fires a fresh registration's `sync` event almost at once,\n * and the flush is a direct `push`. Registering after every mutation would\n * therefore give every online write its own push — bypassing the sync loop's\n * `pushDebounce`, and defeating paced compaction (#117), since\n * `dropIfPending` never touches an entry a push has claimed (#246). So the\n * host registers only in the two cases the sync loop cannot cover by itself:\n * a mutation journaled while the worker is **offline**, and a push that\n * **failed terminally** (the engine's retry policy exhausted). Online, the\n * sync loop drains the journal as it always has.\n *\n * ## Only the library's tag\n *\n * A service worker delivers every Background Sync registration on the origin\n * to the same `sync` handler. The listener answers {@link defaultTag} (or the\n * tag it was given) and nothing else: flushing in response to an unrelated\n * tag would push writes at a moment chosen by someone else's code. A foreign\n * tag's event is left untouched — no `waitUntil`, so whoever registered it\n * still owns its outcome.\n *\n * ## Absence is a working state\n *\n * Background Sync is Chromium-only: on Safari and Firefox there is no\n * `SyncManager`, and that is the common case, not a fault. {@link register}\n * reports `\"Unsupported\"` at debug level and nothing more; the journal still\n * flushes on the next `BrowserConnectivity` signal (reconnect or\n * return-to-visible) the next time a page is open.\n *\n * ## The identity boundary\n *\n * A background flush runs with no page and no user present, so it must not\n * push a journal that belongs to a superseded identity (#212). Two layers\n * guarantee that:\n *\n * - **Quarantine is structural.** A reset moves the superseded identity's\n * unpushed entries into a keyspace `push` cannot read, so a sync event never\n * pushes them, whoever is signed in.\n * - **The worker may not know a reset happened.** A tab that closed before the\n * app called `Client.reset` leaves the store stamped for the old identity,\n * and quarantine cannot help: nothing moved. So {@link ListenOptions.identity}\n * is **required** — an Effect that resolves the fingerprint of whoever is\n * signed in now, which the flush compares with the stamp first: anything but\n * an exact match skips the flush (`\"IdentityChanged\"`) and leaves the\n * journal for the next page boot to reset or upgrade. Skipping is the only\n * safe move from a context with no UI. An app with no signed-in identity at\n * all passes {@link identityUnchecked} to say so explicitly.\n *\n * ## Periodic Background Sync\n *\n * Deliberately not here. A periodic pull has a different permission story\n * (an installed-app engagement heuristic the browser can withdraw) and is an\n * easy way to spend a user's battery; it is a separate decision. A worker\n * that wants one can call `ClientEngine.pull` from its own `periodicsync`\n * listener.\n */\n\nimport type { Identity, StoreError, TransportError } from \"@thomasfosterau/legation\";\nimport type { ClientEngine } from \"@thomasfosterau/legation/internal\";\nimport * as Duration from \"effect/Duration\";\nimport * as Effect from \"effect/Effect\";\nimport * as Option from \"effect/Option\";\nimport * as P from \"effect/Predicate\";\nimport * as Schema from \"effect/Schema\";\nimport * as Scope from \"effect/Scope\";\nimport * as Stream from \"effect/Stream\";\nimport * as SubscriptionRef from \"effect/SubscriptionRef\";\nimport type * as Context from \"effect/Context\";\n\n/**\n * The tag the library registers and answers. Short on purpose: Chromium\n * refuses over-long tags.\n */\nexport const defaultTag = \"legation-flush\";\n\n/** How long a `sync` event waits for a host to attach before giving up. */\nexport const defaultReadyTimeout: Duration.Input = \"30 seconds\";\n\n// ─── Platform shapes ─────────────────────────────────────────────────────────\n//\n// Background Sync is not a cross-browser standard, so the DOM lib declares\n// neither `SyncManager` nor `ServiceWorkerRegistration.sync`, nor the\n// worker's `SyncEvent`. These are the structural slices this module touches,\n// which also lets a test pass a fake `self`.\n\n/** `SyncManager`: absent where the browser has no Background Sync. */\nexport interface SyncManager {\n readonly register: (tag: string) => Promise<void>;\n}\n\n/**\n * A `ServiceWorkerRegistration`, or any stand-in for one. Typed as `object`\n * rather than a slice with an optional `sync`, because TypeScript's weak-type\n * check would then refuse a real registration (whose declared type has no\n * `sync`); the member is probed at runtime instead (see `syncManagerOf`).\n */\nexport type SyncRegistration = object;\n\n/**\n * The view of a {@link SyncRegistration} that {@link syncManagerOf} probes:\n * any object may stand in, and `sync` is read without trusting its shape.\n */\ninterface ProbedRegistration {\n readonly sync?: unknown;\n}\n\n/** Whether a probed `sync` member is a `SyncManager`: an object with a `register` method. */\nconst isSyncManager = (sync: unknown): sync is SyncManager =>\n P.isObjectOrArray(sync) && \"register\" in sync && P.isFunction(sync.register);\n\n/** The registration's `SyncManager`, if the browser gives it one. */\nconst syncManagerOf = (registration: ProbedRegistration | undefined): SyncManager | undefined => {\n const sync = registration?.sync;\n return isSyncManager(sync) ? sync : undefined;\n};\n\n/** The slice of the worker's `SyncEvent` the listener reads. */\nexport interface SyncEvent {\n readonly tag: string;\n /** Chromium: `true` when the browser will not retry this registration again. */\n readonly lastChance?: boolean;\n readonly waitUntil: (promise: Promise<unknown>) => void;\n}\n\n/**\n * The slice of `ServiceWorkerGlobalScope` the listener needs — `self` in a\n * worker. `registration` is where the host registers the tag from.\n */\nexport interface SyncScope {\n readonly addEventListener: (type: \"sync\", listener: (event: SyncEvent) => void) => void;\n readonly registration?: SyncRegistration | undefined;\n}\n\n// ─── Registration ────────────────────────────────────────────────────────────\n\n/**\n * What asking for a sync did. None of these is a failure: registration is\n * advisory, and the journal flushes on the connectivity path whatever\n * happens here.\n *\n * - `Registered` — the browser will fire the tag's `sync` event.\n * - `Unsupported` — no service-worker registration, or no `SyncManager` on\n * it (Safari, Firefox): the connectivity path flushes.\n * - `Refused` — the browser declined (permission withheld, no active worker,\n * or — from inside a worker — no window of the origin open).\n */\nexport type RegisterOutcome = \"Registered\" | \"Unsupported\" | \"Refused\";\n\n/** Options for {@link register}. */\nexport interface RegisterOptions {\n /** Defaults to {@link defaultTag}; must match the listener's. */\n readonly tag?: string;\n /**\n * Where to register. Defaults to the page's own service-worker\n * registration (`navigator.serviceWorker.getRegistration()` — never\n * `ready`, which would wait forever on a page no worker controls).\n */\n readonly registration?: Effect.Effect<SyncRegistration | undefined>;\n}\n\n/** The page's service-worker registration, or `undefined` where there is none. */\nconst pageRegistration: Effect.Effect<SyncRegistration | undefined> = Effect.suspend(() => {\n const container: ServiceWorkerContainer | undefined = globalThis.navigator?.serviceWorker;\n return container === undefined\n ? Effect.undefined\n : Effect.tryPromise(() => container.getRegistration()).pipe(\n Effect.orElseSucceed(() => undefined)\n );\n});\n\n/**\n * Ask the browser to wake the worker with the library's `sync` event once\n * connectivity is back. Call it from the page after a journaled mutation, or\n * let `ServiceWorkerHost` do it from the worker (see {@link ListenOptions}).\n *\n * Never fails and never logs above debug level: absence is the common case\n * (Background Sync is Chromium-only), and the journal still flushes on the\n * next connectivity signal. Registering a tag that is already pending is a\n * no-op for the browser, so calling it once per mutation is fine.\n */\nexport const register = (options: RegisterOptions = {}): Effect.Effect<RegisterOutcome> =>\n Effect.gen(function* () {\n const tag = options.tag ?? defaultTag;\n const registration = yield* options.registration ?? pageRegistration;\n const sync = syncManagerOf(registration);\n if (sync === undefined) {\n yield* Effect.logDebug(\n \"Background Sync unavailable; the journal flushes on the next connectivity signal\"\n );\n return \"Unsupported\" as const;\n }\n return yield* Effect.tryPromise(() => sync.register(tag)).pipe(\n Effect.as(\"Registered\" as const),\n Effect.catch((cause) =>\n Effect.logDebug(\"Background Sync registration refused\", cause).pipe(\n Effect.as(\"Refused\" as const)\n )\n )\n );\n });\n\n/** Whether the worker's own view says the network is down. */\nconst navigatorOffline: Effect.Effect<boolean> = Effect.sync(\n () => globalThis.navigator?.onLine === false\n);\n\n/** Options for {@link registerOnJournal}. */\nexport interface RegisterOnJournalOptions extends RegisterOptions {\n /**\n * Whether the journal cannot drain right now. Defaults to\n * `navigator.onLine === false` in the worker (`WorkerNavigator.onLine`).\n */\n readonly offline?: Effect.Effect<boolean>;\n}\n\n/**\n * Register the tag for the scope's lifetime whenever `engine`'s journal\n * cannot drain now — the host's side of {@link register}, since in the\n * service-worker topology a page's mutation is journaled in the worker:\n *\n * - a mutation is journaled while {@link RegisterOnJournalOptions.offline}\n * holds; and\n * - a push fails terminally (`ClientEngine.syncFailures`, after the retry\n * policy is exhausted) — reachable network, unreachable backend.\n *\n * Never on an online mutation: the sync loop drains those, debounced and\n * compacted, and an online registration would fire `sync` at once and push\n * each write on its own (#246). Chromium only accepts a registration from a\n * worker while a window of the origin is open; a refusal is a debug line.\n */\nexport const registerOnJournal = (\n engine: ClientEngine.ClientEngine,\n options: RegisterOnJournalOptions = {}\n): Effect.Effect<void, never, Scope.Scope> =>\n Effect.gen(function* () {\n const offline = options.offline ?? navigatorOffline;\n // Two forks rather than one merged stream: each subscribes synchronously\n // (`startImmediately`) before this returns, so a mutation journaled\n // straight after the host starts is not missed. (`Stream.merge` would\n // subscribe on fibers of its own, later.)\n const registerOn = <A>(stream: Stream.Stream<A>) =>\n Effect.forkScoped(\n Stream.runForEach(stream, () => register(options)),\n {\n startImmediately: true,\n }\n );\n yield* registerOn(\n engine.engineEvents.pipe(\n Stream.filter((event) => event._tag === \"MutationJournaled\"),\n Stream.filterEffect(() => offline)\n )\n );\n yield* registerOn(\n engine.syncFailures.pipe(Stream.filter((failure) => failure.operation === \"push\"))\n );\n });\n\n// ─── The flush ───────────────────────────────────────────────────────────────\n\n/**\n * What a flush did. Each is a settled outcome — the `sync` event resolves and\n * the browser does not retry:\n *\n * - `Flushed` — the push completed (possibly with nothing pending).\n * Acknowledged entries are trimmed and their optimistic effects dropped; a\n * permanently rejected one settles as `rejected` in `mutationStates`, with\n * its reason.\n * - `IdentityChanged` — the store is not stamped for whoever is signed in now\n * (see {@link ListenOptions.identity}); nothing was pushed, and the journal\n * waits for the next page boot to apply the boundary's transition.\n * - `UpdateNeeded` — the server rejected this build's schema version; the\n * journal is retained and `onUpdateNeeded` has fired. Retrying cannot help\n * until the app updates.\n */\nexport type FlushOutcome = \"Flushed\" | \"IdentityChanged\" | \"UpdateNeeded\";\n\n/**\n * The explicit opt-out from the flush's identity check, for an app with **no\n * signed-in identity at all** (every session is the same public principal).\n * The flush then relies on the store's boundary alone: quarantined entries\n * are still unreachable from push, but a store still stamped for a user who\n * signed out in a tab that closed before `Client.reset` ran **would be\n * pushed**. Never pass this in an app that has sign-in.\n */\nexport const identityUnchecked: unique symbol = Symbol.for(\n \"@thomasfosterau/legation-browser/identityUnchecked\"\n);\n\n/**\n * Who is signed in now, as the app resolves it inside the worker — or\n * {@link identityUnchecked}. See {@link ListenOptions.identity}.\n */\nexport type IdentitySource =\n | Effect.Effect<Identity.Fingerprint | undefined>\n | typeof identityUnchecked;\n\n/** Options for {@link flush}. */\nexport interface FlushOptions {\n /** See {@link ListenOptions.identity}. */\n readonly identity: IdentitySource;\n}\n\n/**\n * Flush `engine`'s journal through the ordinary push path, behind the\n * identity check. A journal larger than half the engine's unpulled bound goes\n * in chunks with a pull before each chunk after the first, so a flush may\n * pull (#429); a smaller journal is pushed without pulling. Fails — so the\n * `sync` event rejects and the browser reschedules — on a transport or store\n * failure, with the journal intact. A pull between chunks that fails is a\n * transport failure of the same kind, so the flush never reports `Flushed`\n * over a journal it stopped short of.\n */\nexport const flush = (\n engine: ClientEngine.ClientEngine,\n options: FlushOptions\n): Effect.Effect<FlushOutcome, TransportError | StoreError> =>\n Effect.gen(function* () {\n if (options.identity !== identityUnchecked) {\n const current = yield* options.identity;\n const stamped = yield* engine.identity;\n // Exact match only. `undefined` (the app cannot say who is signed in),\n // an unstamped store, and even the benign anonymous → signed-in upgrade\n // are all left to a page boot: a context with no UI does not adopt an\n // identity on the user's behalf.\n if (current === undefined || stamped !== current) {\n yield* Effect.logDebug(\n \"Background Sync flush skipped: the replica is not stamped for the current identity\"\n );\n return \"IdentityChanged\" as const;\n }\n }\n return yield* engine.push.pipe(\n Effect.as(\"Flushed\" as const),\n Effect.catchTag(\"VersionNotSupportedError\", () => Effect.succeed(\"UpdateNeeded\" as const))\n );\n }).pipe(Effect.withSpan(\"legation.backgroundSync.flush\"));\n\n// ─── The listener ────────────────────────────────────────────────────────────\n\n/** No host attached the listener within its `readyTimeout`. */\nexport class HostNotReady extends Schema.TaggedError<\n HostNotReady,\n { readonly brand: unique symbol }\n>()(\"HostNotReady\", {\n tag: Schema.String,\n message: Schema.String,\n}) {}\n\n/** Options for {@link listen}. */\nexport interface ListenOptions {\n /** The tag to answer, and to register from the host. Defaults to {@link defaultTag}. */\n readonly tag?: string;\n /** The worker's global scope. Defaults to `globalThis` (`self` in a worker). */\n readonly scope?: SyncScope;\n /**\n * How long a `sync` event waits for a host to attach before rejecting, so\n * the browser reschedules it. Defaults to {@link defaultReadyTimeout}. A\n * worker that only boots its host on a page's handshake never attaches on\n * a tab-less wake; boot the host at worker start for background flushing.\n */\n readonly readyTimeout?: Duration.Input;\n /**\n * **Required.** The fingerprint of whoever is signed in **now**, as the app\n * resolves it from inside the worker (e.g. a session-subject hash it keeps\n * beside its session). A flush pushes only if the store's stamp equals it\n * exactly; `undefined` means \"cannot tell\" and skips.\n *\n * Required rather than optional because the store's own boundary covers\n * only a reset that ran: a tab closed before `Client.reset` leaves the old\n * identity's journal in the pushable keyspace, and a background flush would\n * send it under whoever's cookies the worker now carries. An app with no\n * sign-in passes {@link identityUnchecked}, which accepts that risk\n * explicitly.\n */\n readonly identity: IdentitySource;\n /**\n * Register the tag from the worker when the attached engine's journal\n * cannot drain now (see {@link registerOnJournal}). Defaults to `true`;\n * turn it off when the page registers with {@link register} itself.\n */\n readonly registerOnJournal?: boolean;\n /**\n * Whether the journal cannot drain now, for {@link registerOnJournal}.\n * Defaults to `navigator.onLine === false`.\n */\n readonly offline?: Effect.Effect<boolean>;\n}\n\n/**\n * The worker's `sync` listener, created by {@link listen}. It answers its\n * tag from the moment it exists; a host attaches an engine with\n * {@link Listener.serve}, which `ServiceWorkerHost.start` does when given the\n * listener as `backgroundSync`.\n */\nexport interface Listener {\n /** The tag this listener answers. */\n readonly tag: string;\n /**\n * Attach `engine` for the scope's lifetime: `sync` events for the tag —\n * including any that arrived before this call — flush its journal, run\n * under the caller's services (logger, tracer). Closing the scope detaches\n * it; a later event then waits for the next host.\n */\n readonly serve: (engine: ClientEngine.ClientEngine) => Effect.Effect<void, never, Scope.Scope>;\n}\n\ninterface Attached {\n readonly flush: Effect.Effect<FlushOutcome, TransportError | StoreError>;\n /** The host's services, so a flush logs and traces as the host does. */\n readonly services: Context.Context<never>;\n}\n\n/**\n * Install the `sync` listener on the worker's scope — **synchronously, at the\n * top level of the worker script** (see the module docs for why). Deliberately\n * a plain function rather than an Effect for that reason.\n *\n * ```ts\n * // service-worker.ts, top level\n * const backgroundSync = BackgroundSync.listen({ identity: currentFingerprint });\n * // ... later, once the engine's layers are built:\n * yield* ServiceWorkerHost.start({ backgroundSync, registry, nodes, urls });\n * ```\n */\nexport const listen = (options: ListenOptions): Listener => {\n const tag = options.tag ?? defaultTag;\n const scope: Partial<SyncScope> = options.scope ?? globalThis;\n const readyTimeout = options.readyTimeout ?? defaultReadyTimeout;\n const attached = Effect.runSync(SubscriptionRef.make(Option.none<Attached>()));\n\n // The first attached host, however long it takes to arrive (bounded).\n // `changes` emits the current value first and never ends, so the head\n // exists as soon as some host is attached.\n const whenAttached: Effect.Effect<Attached, HostNotReady> = SubscriptionRef.changes(\n attached\n ).pipe(\n Stream.filter(Option.isSome),\n Stream.map((some) => some.value),\n Stream.runHead,\n Effect.flatMap((head) => (Option.isSome(head) ? Effect.succeed(head.value) : Effect.never)),\n Effect.timeoutOrElse({\n duration: readyTimeout,\n orElse: () =>\n Effect.fail(\n new HostNotReady({\n tag,\n message: `No engine host attached within ${Duration.format(Duration.fromInputUnsafe(readyTimeout))}; the browser will reschedule the sync`,\n })\n ),\n })\n );\n\n const handle = (event: SyncEvent): Promise<FlushOutcome> =>\n Effect.runPromise(\n whenAttached.pipe(\n Effect.tapError((error) =>\n Effect.logDebug(\n event.lastChance === true\n ? `${error.message} (the browser's last chance; the journal flushes on the next connectivity signal)`\n : error.message\n )\n ),\n Effect.flatMap(({ flush: run, services }) =>\n run.pipe(\n Effect.tapError((error) =>\n error._tag === \"TransportError\"\n ? Effect.logDebug(\n event.lastChance === true\n ? \"Background Sync flush failed on its last chance; the journal flushes on the next connectivity signal\"\n : \"Background Sync flush failed; the browser will reschedule it\",\n error\n )\n : Effect.logWarning(\"Background Sync flush failed on the store\", error)\n ),\n Effect.provideContext(services)\n )\n )\n )\n );\n\n const listener = (event: SyncEvent): void => {\n // Not ours: leave the event alone so its owner decides its outcome.\n if (event.tag !== tag) return;\n event.waitUntil(handle(event));\n };\n scope.addEventListener?.(\"sync\", listener);\n\n const registration = Effect.sync(() => scope.registration);\n const serve: Listener[\"serve\"] = (engine) =>\n Effect.gen(function* () {\n const entry: Attached = {\n flush: flush(engine, { identity: options.identity }),\n services: yield* Effect.context<never>(),\n };\n yield* Effect.acquireRelease(SubscriptionRef.set(attached, Option.some(entry)), () =>\n // Detach only ourselves: a newer host may already have attached.\n SubscriptionRef.update(attached, (current) =>\n Option.isSome(current) && current.value === entry ? Option.none() : current\n )\n );\n if (options.registerOnJournal !== false) {\n yield* registerOnJournal(engine, {\n tag,\n registration,\n ...(options.offline === undefined ? {} : { offline: options.offline }),\n });\n }\n });\n\n return { tag, serve };\n};\n","/**\n * PushPoke\n *\n * The worker side of Web Push as a poke carrier (#214): a `push` listener that\n * turns the library's push into convergence for a client with no open tab. The\n * server side, which sends the push within the app's stated budget, is\n * `PushPoke.sink` in `@thomasfosterau/legation/internal`.\n *\n * ## What a recognised push does\n *\n * The push carries the library's marker (`PushPoke.payload`) and nothing else.\n * The listener recognises it and, by default, **registers a Background Sync**\n * (the flush path of #213), so the browser runs the sync when it judges the\n * connection good: the kinder default on a metered connection. An app that\n * wants the replica current at once opts in to `action: \"pull\"`, which pulls\n * directly. Either way, the pull the poke prompts is where the data travels.\n *\n * ## Coexisting with the app's own `push` handler\n *\n * A worker has one `push` event for every push on the origin. The listener\n * acts only on the library's marker and leaves every other push untouched (no\n * `waitUntil`), so the app's own handler, its notifications, keeps working.\n * Showing a notification for a silent push is the app's business; see the\n * README on the budget browsers enforce.\n *\n * ## The identity boundary\n *\n * A push may arrive for a user who has since signed out on this device. Before\n * acting, the listener compares who is signed in now with the replica's stamp,\n * as the Background Sync flush does (#212): anything but an exact match does\n * nothing (`\"IdentityChanged\"`), so a push for a superseded identity never\n * pulls, or schedules a push, under the new one.\n *\n * Install it **synchronously at the top level of the worker script**, like\n * `BackgroundSync.listen`: a worker woken for a `push` evaluates its script and\n * dispatches the event straight after.\n */\n\nimport type { ClientEngine } from \"@thomasfosterau/legation/internal\";\nimport { PushPoke as Marker } from \"@thomasfosterau/legation/internal\";\nimport * as Duration from \"effect/Duration\";\nimport * as Effect from \"effect/Effect\";\nimport * as Option from \"effect/Option\";\nimport * as P from \"effect/Predicate\";\nimport * as Result from \"effect/Result\";\nimport * as Scope from \"effect/Scope\";\nimport * as BackgroundSync from \"./BackgroundSync.js\";\n\n/** The slice of a worker's `PushEvent` the listener reads. */\nexport interface PushEvent {\n /** The push's data (`PushMessageData`, or `null` for a push without any). */\n readonly data: unknown;\n readonly waitUntil: (promise: Promise<unknown>) => void;\n}\n\n/** The slice of `ServiceWorkerGlobalScope` the listener needs — `self` in a worker. */\nexport interface PushScope {\n readonly addEventListener: (type: \"push\", listener: (event: PushEvent) => void) => void;\n readonly registration?: BackgroundSync.SyncRegistration;\n}\n\n/**\n * What a recognised push did.\n *\n * - `Registered` / `Unsupported` / `Refused` — the Background Sync\n * registration's outcome (`action: \"backgroundSync\"`, the default).\n * - `Pulled` / `PullFailed` — the engine pulled, or the pull failed and the\n * next connectivity signal pulls instead (`action: \"pull\"`).\n * - `IdentityChanged` — the replica is not stamped for whoever is signed in\n * now, so nothing was done.\n */\nexport type PokeOutcome =\n | BackgroundSync.RegisterOutcome\n | \"Pulled\"\n | \"PullFailed\"\n | \"IdentityChanged\";\n\n/** Options for {@link listen}. */\nexport interface ListenOptions {\n /**\n * **Required.** Who is signed in now, as `BackgroundSync.listen` takes it:\n * a recognised push acts only if the replica's stamp equals it exactly.\n * An app with no sign-in passes `BackgroundSync.identityUnchecked`.\n */\n readonly identity: BackgroundSync.IdentitySource;\n /**\n * What a recognised push does: `\"backgroundSync\"` (the default) registers the\n * library's sync tag; `\"pull\"` pulls at once.\n */\n readonly action?: \"backgroundSync\" | \"pull\";\n /** The sync tag to register; defaults to `BackgroundSync.defaultTag`. */\n readonly tag?: string;\n /** The worker's global scope. Defaults to `globalThis` (`self` in a worker). */\n readonly scope?: PushScope;\n /**\n * How long a recognised push waits for a host to attach an engine. Defaults\n * to `BackgroundSync.defaultReadyTimeout`.\n */\n readonly readyTimeout?: Duration.Input;\n}\n\n/** The `push` listener {@link listen} installs; a host attaches its engine with {@link Listener.serve}. */\nexport interface Listener {\n /**\n * Attach `engine` for the scope's lifetime, which `ServiceWorkerHost.start`\n * does when given the listener as `pushPoke`. A push that arrived before\n * then waits for it, up to the ready timeout.\n */\n readonly serve: (engine: ClientEngine.ClientEngine) => Effect.Effect<void, never, Scope.Scope>;\n}\n\n/** Whether a push is the library's poke; a push whose data cannot be read as text is not. */\nconst isLibraryPoke = (event: PushEvent): boolean => {\n const data = event.data;\n if (!P.hasProperty(data, \"text\") || !P.isFunction(data.text)) return false;\n const text = data.text;\n return Result.match(\n Result.try(() => String(text.call(data))),\n { onSuccess: Marker.isPoke, onFailure: () => false }\n );\n};\n\n/** An attached host: its engine, and a runner over its services (logger, tracer). */\ninterface Attached {\n readonly engine: ClientEngine.ClientEngine;\n readonly run: <A>(effect: Effect.Effect<A>) => Promise<A>;\n}\n\n/**\n * Install the `push` listener on the worker's scope, synchronously at the top\n * level of the worker script.\n *\n * ```ts\n * // service-worker.ts, top level\n * const backgroundSync = BackgroundSync.listen({ identity: currentFingerprint });\n * const pushPoke = PushPoke.listen({ identity: currentFingerprint });\n * // ... once the engine's layers are built:\n * yield* ServiceWorkerHost.start({ backgroundSync, pushPoke, registry, nodes });\n * ```\n */\nexport const listen = (options: ListenOptions): Listener => {\n const scope: Partial<PushScope> = options.scope ?? globalThis;\n const tag = options.tag ?? BackgroundSync.defaultTag;\n const readyTimeout = options.readyTimeout ?? BackgroundSync.defaultReadyTimeout;\n const timeoutMillis = Duration.toMillis(Duration.fromInputUnsafe(readyTimeout));\n // The attached host, and the pushes waiting for one. Plain state: the event\n // listener must decide synchronously, and runs nothing until a host brings\n // its services.\n let attached: Option.Option<Attached> = Option.none();\n let waiting: Array<(host: Attached) => void> = [];\n\n // The identity check of `BackgroundSync.flush`: an exact match, or nothing.\n const sameIdentity = (engine: ClientEngine.ClientEngine): Effect.Effect<boolean> =>\n options.identity === BackgroundSync.identityUnchecked\n ? Effect.succeed(true)\n : Effect.all(\n [options.identity, engine.identity.pipe(Effect.orElseSucceed(() => undefined))],\n { concurrency: 1 }\n ).pipe(Effect.map(([current, stamped]) => current !== undefined && stamped === current));\n\n const act = (engine: ClientEngine.ClientEngine): Effect.Effect<PokeOutcome> =>\n Effect.gen(function* () {\n if (!(yield* sameIdentity(engine))) {\n yield* Effect.logDebug(\n \"Push-poke skipped: the replica is not stamped for the current identity\"\n );\n return \"IdentityChanged\" as const;\n }\n if (options.action === \"pull\") {\n return yield* engine.pull.pipe(\n Effect.as(\"Pulled\" as const),\n Effect.catch((error) =>\n Effect.logDebug(\n \"Push-poke pull failed; the next connectivity signal pulls\",\n error\n ).pipe(Effect.as(\"PullFailed\" as const))\n )\n );\n }\n return yield* BackgroundSync.register({\n tag,\n registration: Effect.sync(() => scope.registration),\n });\n });\n\n // Act through the attached host, or wait for one up to the ready timeout.\n const handle = (): Promise<PokeOutcome | \"HostNotReady\"> =>\n Option.match(attached, {\n onSome: (host) => host.run(act(host.engine)),\n onNone: () =>\n new Promise<PokeOutcome | \"HostNotReady\">((resolve) => {\n const proceed = (host: Attached) => {\n clearTimeout(timer);\n resolve(host.run(act(host.engine)));\n };\n const timer = setTimeout(() => {\n waiting = waiting.filter((other) => other !== proceed);\n resolve(\"HostNotReady\");\n }, timeoutMillis);\n waiting = [...waiting, proceed];\n }),\n });\n\n const listener = (event: PushEvent): void => {\n // Not the library's push: leave it to the app's own handler.\n if (!isLibraryPoke(event)) return;\n event.waitUntil(handle());\n };\n scope.addEventListener?.(\"push\", listener);\n\n const serve: Listener[\"serve\"] = (engine) =>\n Effect.gen(function* () {\n const host: Attached = { engine, run: Effect.runPromiseWith(yield* Effect.context<never>()) };\n yield* Effect.acquireRelease(\n Effect.sync(() => {\n attached = Option.some(host);\n const ready = waiting;\n waiting = [];\n ready.forEach((proceed) => proceed(host));\n }),\n () =>\n Effect.sync(() => {\n // Detach only ourselves: a newer host may already have attached.\n if (Option.isSome(attached) && attached.value === host) attached = Option.none();\n })\n );\n });\n\n return { serve };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,MAAM,oBAAoB;AAC1B,MAAM,gBAAgB;;;;;AAMtB,MAAM,mBACJ;;;;;;;;AAgBF,MAAM,gBACJ,aAEA,OAAO,IAAI,aAAa;CACtB,MAAM,OAAO,OAAO,UAAU,UAAA,CAAW;CACzC,OAAO,OAAO,OAAO,UAAkC,WAAW;EAChE,MAAM,QAAQ,UACZ,OACE,OAAO,KAAK,IAAI,WAAW;GAAE,SAAS;GAAsC;EAAM,CAAC,CAAC,CACtF;EACF,MAAM,SAAS,OAAO,UAAU,IAAI,KAAK,QAAQ,CAAC;EAClD,IAAI,OAAO,UAAU,MAAM,GAAG;GAC5B,KAAK,OAAO,OAAO;GACnB;EACF;EACA,MAAM,UAAU,OAAO;EACvB,QAAQ,wBAAwB;GAC9B,MAAM,KAAK,QAAQ;GACnB,IAAI,CAAC,GAAG,iBAAiB,SAAS,aAAa,GAC7C,GAAG,kBAAkB,eAAe,EAAE,SAAS,MAAM,CAAC;EAE1D;EACA,QAAQ,kBAAkB;GACxB,MAAM,aAAyB;IAAE,IAAI,QAAQ;IAAQ,UAAU;GAAM;GACrE,WAAW,GAAG,wBAAwB;IACpC,WAAW,WAAW;IACtB,WAAW,GAAG,MAAM;GACtB;GACA,OAAO,OAAO,QAAQ,UAAU,CAAC;EACnC;EACA,QAAQ,gBAAgB,KAAK,QAAQ,KAAK;CAC5C,CAAC;AACH,CAAC;;;;;AAMH,MAAM,gBAAgB,gBAAsC;CAC1D,OAAO,UAAU,YAAY,MAAM,CAAC;AACtC;;AAGA,MAAM,uBAAuB,IAAiB,QAAgB,QAC5D,IAAI,cAAc,mBAAmB;CACnC;CACA,SAAS,uBAAuB,GAAG,KAAK,YAAY,cAAc;CAClE,GAAI,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,IAAI;AACrC,CAAC;;;;;;AAOH,MAAM,cACJ,YACA,MACA,SACA,aAEA,OAAO,UAA+C,WAAW;CAC/D,IAAI,UAAU;CACd,IAAI;CACJ,MAAM,UAAU,WAA+D;EAC7E,IAAI,SAAS;EACb,UAAU;EACV,OAAO,MAAM;CACf;CACA,MAAM,QAAQ,UACZ,OACE,OAAO,KACL,IAAI,cAAc,mBAAmB;EACnC,GAAG;EACH,SAAS,WAAW,WAAW,mBAAmB,QAAQ;EAC1D;CACF,CAAC,CACH,CACF;CACF,IAAI,WAAW,UAAU;EACvB,OACE,OAAO,KACL,IAAI,cAAc,mBAAmB;GACnC,QAAQ,QAAQ;GAChB,SAAS;GACT,GAAI,QAAQ,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,KAAK,QAAQ,IAAI;EAC1D,CAAC,CACH,CACF;EACA;CACF;CACA,MAAM,KAAK,WAAW;CACtB,IAAI,CAAC,GAAG,iBAAiB,SAAS,aAAa,GAAG;EAChD,OAAO,OAAO,KAAK,oBAAoB,IAAI,QAAQ,QAAQ,QAAQ,GAAG,CAAC,CAAC;EACxE;CACF;CACA,MAAM,QAAQ,OAAO,UAAU,GAAG,YAAY,eAAe,IAAI,CAAC;CAClE,IAAI,OAAO,UAAU,KAAK,GAAG;EAC3B,KAAK,MAAM,OAAO;EAClB;CACF;CACA,MAAM,cAAc,MAAM;CAC1B,YAAY,mBAAmB,OAAO,OAAO,QAAQ,MAAW,CAAC;CACjE,YAAY,gBAAgB,KAAK,YAAY,KAAK;CAClD,YAAY,gBAAgB,KAAK,YAAY,KAAK;CAClD,MAAM,SAAS,OAAO,UAAU;EAC9B,MAAM,UAAU,SAAS,YAAY,YAAY,aAAa,CAAC;EAC/D,QAAQ,kBAAkB;GACxB,SAAS,QAAQ;EACnB;EACA,QAAQ,gBAAgB,KAAK,QAAQ,KAAK;CAC5C,CAAC;CACD,IAAI,OAAO,UAAU,MAAM,GAAG;EAC5B,KAAK,OAAO,OAAO;EACnB,aAAa,WAAW;CAC1B;CACA,OAAO,OAAO,WAAW;EACvB,IAAI,SAAS;EACb,UAAU;EACV,aAAa,WAAW;CAC1B,CAAC;AACH,CAAC;;;;;;AAOH,MAAM,gBAAgB,eAAwD;CAG5E,OAAO,cAAc,KAAK;EACxB,MAAM,QACJ,WACE,YACA,YACA;GAAE,QAAQ;GAAO,SAAS;GAA0C;EAAI,IACvE,UAAU,MAAM,IAAI,GAAG,CAC1B,CAAC,CAAC,KAAK,OAAO,KAAK,UAAW,EAAE,SAAS,OAAO,KAAK,IAAI,MAAM,QAAQ,KAAA,CAAU,CAAC;EACpF,gBAAgB,QACd,WACE,YACA,YACA;GAAE,QAAQ;GAAiB,SAAS;GAA0C;EAAI,IACjF,UAAU,MAAM,IAAI,GAAG,CAC1B,CAAC,CAAC,KAAK,OAAO,KAAK,UAAW,OAAO,iBAAiB,aAAa,MAAM,QAAQ,KAAA,CAAU,CAAC;EAC9F,MAAM,QACJ,WACE,YACA,YACA;GAAE,QAAQ;GAAO,SAAS;GAAwC;EAAI,IACrE,UAAU,MAAM,MAAM,GAAG,CAC5B,CAAC,CAAC,KAAK,OAAO,KAAK,UAAU,QAAQ,CAAC,CAAC;EACzC,MAAM,KAAK,UACT,WACE,YACA,aACA;GAAE,QAAQ;GAAO,SAAS;GAAwC;EAAI,IACrE,UAAU,MAAM,IAAI;GAAE;GAAK;EAAM,CAAC,CACrC,CAAC,CAAC,KAAK,OAAO,MAAM;EACtB,SAAS,QACP,WACE,YACA,aACA;GAAE,QAAQ;GAAU,SAAS;GAA6C;EAAI,IAC7E,UAAU,MAAM,OAAO,GAAG,CAC7B,CAAC,CAAC,KAAK,OAAO,MAAM;EACtB,OAAO,WACL,YACA,aACA;GAAE,QAAQ;GAAS,SAAS;EAAgC,IAC3D,UAAU,MAAM,MAAM,CACzB,CAAC,CAAC,KAAK,OAAO,MAAM;EACpB,MAAM,WACJ,YACA,YACA;GAAE,QAAQ;GAAQ,SAAS;EAAmC,IAC7D,UAAU,MAAM,MAAM,CACzB;EACA,MAAM,WACJ,YACA,YACA;GAAE,QAAQ;GAAQ,SAAS;EAAuC,IACjE,UAAU,MAAM,WAAW,CAC9B,CAAC,CAAC,KAAK,OAAO,KAAK,SAAS,KAAK,OAAO,EAAE,QAAQ,CAAC,CAAC;CACtD,CAAC;AACH;;;;;;AAOA,MAAM,aACJ,YACA,QAEA,OAAO,UAA4B,WAAW;CAC5C,IAAI,UAAU;CACd,MAAM,UAAU,WAA4C;EAC1D,IAAI,SAAS;EACb,UAAU;EACV,OAAO,MAAM;CACf;CACA,MAAM,QAAQ,UACZ,OACE,OAAO,KACL,IAAI,WAAW;EACb,SAAS,WAAW,WAChB,mBACA;EACJ;CACF,CAAC,CACH,CACF;CACF,IAAI,WAAW,UAAU;EACvB,OAAO,OAAO,KAAK,IAAI,WAAW,EAAE,SAAS,iBAAiB,CAAC,CAAC,CAAC;EACjE;CACF;CACA,MAAM,KAAK,WAAW;CACtB,IAAI,CAAC,GAAG,iBAAiB,SAAS,aAAa,GAAG;EAChD,OACE,OAAO,KACL,IAAI,WAAW,EACb,SAAS,sDAAsD,GAAG,KAAK,YAAY,cAAc,gBACnG,CAAC,CACH,CACF;EACA;CACF;CACA,MAAM,QAAQ,OAAO,UAAU,GAAG,YAAY,eAAe,WAAW,CAAC;CACzE,IAAI,OAAO,UAAU,KAAK,GAAG;EAC3B,KAAK,MAAM,OAAO;EAClB;CACF;CACA,MAAM,cAAc,MAAM;CAC1B,MAAM,cAAc,aAAa,WAAW;CAC5C,YAAY,mBAAmB,OAAO,OAAO,IAAI;CACjD,YAAY,gBAAgB,KAAK,YAAY,KAAK;CAClD,YAAY,gBAAgB,KAAK,YAAY,KAAK;CAClD,MAAM,SAAS,OAAO,UAAU;EAC9B,MAAM,UAAU,YAAY,YAAY,aAAa;EAGrD,IAAI,QAAQ,MAAM,OAAO;GACvB,IAAI,GAAG,SAAS,OAAO,QAAQ,IAAI;IAAE,KAAK,GAAG;IAAK,OAAO,GAAG;GAAM,CAAC;QAC9D,QAAQ,OAAO,GAAG,GAAG;EAC5B,CAAC;CACH,CAAC;CACD,IAAI,OAAO,UAAU,MAAM,GAAG;EAC5B,KAAK,OAAO,OAAO;EACnB,MAAM;CACR;CAEA,OAAO,OAAO,WAAW;EACvB,IAAI,CAAC,SAAS;GACZ,UAAU;GACV,MAAM;EACR;CACF,CAAC;AACH,CAAC;;;;;AAMH,MAAa,iBACX,aAEA,MAAM,OAAO,cAAc,aAAa,CAAC,CACvC,OAAO,IAAI,aAAa;CACtB,MAAM,aAAa,OAAO,OAAO,eAC/B,aAAa,YAAY,iBAAiB,IACzC,WAAW,OAAO,WAAW,OAAO,GAAG,MAAM,CAAC,CACjD,CAAC,CAAC,KAAK,OAAO,KAAK;CACnB,OAAO,aAAa,UAAU;AAChC,CAAC,CACH;;;;;;;AAQF,MAAM,eACJ,eAEA,WACE,YACA,YACA;CAAE,QAAQ;CAAU,SAAS;AAAqC,IACjE,UAAU,MAAM,OAAO,CAC1B,CAAC,CAAC,KACA,OAAO,KAAK,YACV,QAAQ,SAAS,WACf,EAAE,SAAS,OAAO,KAAK,IAAI,CAAC;CAAE,KAAK,OAAO;CAAK,OAAO,OAAO;AAAM,CAAC,IAAI,CAAC,CAC3E,CACF,GACA,OAAO,UAAU,UAAU,IAAI,WAAW;CAAE,SAAS,MAAM;CAAS,OAAO;AAAM,CAAC,CAAC,CACrF;;;;;;;AAQF,MAAa,uBACX,aAEA,MAAM,OAAO,eAAe,cAAc,CAAC,CACzC,OAAO,IAAI,aAAa;CACtB,MAAM,aAAa,OAAO,OAAO,eAC/B,aAAa,YAAY,iBAAiB,IACzC,WAAW,OAAO,WAAW,OAAO,GAAG,MAAM,CAAC,CACjD,CAAC,CAAC,KAAK,OAAO,KAAK;CACnB,OAAO,eAAe,eAAe,GAAG;EACtC,QAAQ,QAAS,IAAI,qBAAqB,GAAG,IAAI,OAAO,OAAO,UAAU,YAAY,GAAG;EACxF,SAAS,YAAY,UAAU;CACjC,CAAC;AACH,CAAC,CACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AChUF,MAAM,mBACJ,aACiC;CACjC,GAAI,SAAS,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,QAAQ,OAAO;CAClE,GAAI,SAAS,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU,QAAQ,SAAS;AAC1E;;;;;;;;;AAUA,MAAa,SACX,YAEA,YAAY,cAAc,gBAAgB,OAAO,CAAC,CAAC,CAAC,KAClD,MAAM,QACJ,MAAM,MAAM,cAAc,SAAS,QAAQ,GAAG,oBAAoB,SAAS,QAAQ,CAAC,CACtF,CACF;;AAGF,MAAa,eACX,YAEA,MAAM,OAAO,CAAC,CAAC,KAAK,MAAM,QAAQ,UAAU,WAAW,CAAC;;;;;;;AAQ1D,MAAa,oBAAoB,aAC/B,qBAAqB,SAAS,WAAW,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;AAoBrD,MAAa,mBACX,UACA,YAEA,YAAY;CACV,GAAI,SAAS,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU,QAAQ,SAAS;CACxE,QAAQ,iBAAiB,QAAQ;CACjC;AACF,CAAC;;;;;;;;;;;;AC7GH,MAAaA,UACX,SAEA,OAAO,IAAI,aAAa;CACtB,MAAM,UAAU,OAAO,OAAO,eAC5B,OAAO,WAAW,IAAI,iBAAiB,IAAI,CAAC,IAC3C,OAAO,OAAO,WAAW,GAAG,MAAM,CAAC,CACtC;CACA,OAAO;EACL,OAAO,YACL,OAAO,IAAI;GAET,WAAW,QAAQ,YAAY,OAAO;GACtC,QAAQ,UAAU,IAAI,eAAe;IAAE,SAAS;IAAgC;GAAM,CAAC;EACzF,CAAC;EACH,WAAW,OAAO,IAAI,aAAa;GACjC,MAAM,QAAQ,OAAO,MAAM,UAAmB;GAC9C,OAAO,OAAO,eACZ,OAAO,WAAW;IAChB,MAAM,YAAY,UAAwB;KACxC,MAAM,YAAY,OAAO,MAAM,IAAI;IACrC;IACA,QAAQ,iBAAiB,WAAW,QAAyB;IAC7D,OAAO;GACT,CAAC,IACA,aACC,OAAO,WAAW,QAAQ,oBAAoB,WAAW,QAAyB,CAAC,CACvF;GACA,OAAO,EAAE,MAAM,MAAM,KAAK,KAAK,CAAC,CAAC,KAAK,OAAO,KAAK,EAAE;EACtD,CAAC;CACH;AACF,CAAC;;;;;;;;AC0CH,MAAa,qBAAqB;;;;;;;;;AAsBlC,MAAa,SACX,UAAoC,CAAC,MAErC,OAAO,IAAI,aAAa;CACtB,MAAM,EACJ,cAAc,oBACd,UACA,OACA,MACA,gBACA,aACE;CACJ,MAAM,SAAS,OAAO,aAAa;CACnC,MAAM,UAAU,OAAOC,OAA4B,WAAW;CAC9D,MAAM,OAAO,OAAOC,aAAW,IAAI;EACjC;EACA;EACA,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS;EAC7C,GAAI,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM;EACvC,GAAI,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,KAAK;CACvC,CAAC;CACD,OAAO,OAAO,QAAQ,KAAK,OAAO,MAAM;CACxC,IAAI,mBAAmB,KAAA,GAAW,OAAO,eAAe,MAAM,MAAM;CACpE,IAAI,aAAa,KAAA,GAAW,OAAO,SAAS,MAAM,MAAM;CACxD,OAAO;EAAE;EAAQ,eAAe,KAAK;CAAc;AACrD,CAAC;;;;;;;AAQH,MAAaC,SACX,UAAoC,CAAC,MAErC,MAAM,OAAO,CAAC,CAAC,KAAK,OAAO,QAAQ,OAAO,KAAK,CAAC;;;;AC3ClD,MAAM,oBAA6C,WAAW,WAAW;;;;;;;AAQzE,MAAM,eAAe,OAAoB,SACvC,OAAO,eACL,OAAO,UAAsB,QAAQ,WAAW;CAC9C,MACG,QACC,MACA;EAAE,MAAM;EAAa;CAAO,SAE1B,IAAI,SAAe,gBAAgB;EACjC,OAAO,OAAO,cAAc,YAAY,CAAC,CAAC;CAC5C,CAAC,CACL,CAAC,CACA,YAAY,KAAA,CAAS;AAC1B,CAAC,IACA,YAAY,OAAO,KAAK,OAAO,GAChC,EAAE,eAAe,KAAK,CACxB,CAAC,CAAC,KAAK,OAAO,MAAM;;;;;;;;;;;AAYtB,MAAM,iBACJ,YAEA,OAAO,IAAI,aAAa;CACtB,MAAM,gBAAgB,OAAO,IAAI,qBAAK,IAAI,IAA2B,CAAC;CACtE,MAAM,cAAc,OAAO,IAAI,qBAC7B,IAAI,IAA+D,CACrE;CAIA,OAAO,OAAO,WACZ,OAAO,WAAW,OAAO,KAAK,gBAAgB,QAAQ,OAAO,GAAG,CAAC,IAAI,SACnE,OAAO,IAAI,aAAa;EACtB,MAAM,UAAU,OAAO,IAAI,IAAI,aAAa;EAC5C,MAAM,QAAQ,OAAO,IAAI,IAAI,WAAW;EACxC,OAAO,OAAO,QACZ,QAAQ,OAAO,IACd,WAAW,KAAK,UAAU,MAAM,CAAC,CAAC,KAAK,OAAO,MAAM,GACrD,EACE,SAAS,KACX,CACF;EACA,OAAO,OAAO,QACZ,MAAM,OAAO,IACZ,UAAU,KAAK,eAAe,MAAM,MAAM,MAAM,IAAI,CAAC,CAAC,KAAK,OAAO,MAAM,GACzE,EAAE,SAAS,KAAK,CAClB;CACF,CAAC,CACH,CAAC,CAAC,KAAK,OAAO,MAAM,CACtB;CAEA,MAAM,UAAU,gBAAgB,IAAI,OAAO;CAC3C,MAAM,eAAqB,MACzB,OAAO,QAAQ,SAAS,CAAC;CAE3B,OAAO;EACL,QAAQ,WAAW,aAAa,SAAS,KAAK,MAAM,MAAM,CAAC;EAC3D,UAAU,WACR,gBAAgB,QAAQ,OAAO,CAAC,CAAC,KAAK,OAAO,WAAW,SAAS,KAAK,QAAQ,MAAM,CAAC,CAAC;EACxF,iBAAiB,QAAQ,kBACvB,gBAAgB,QAAQ,OAAO,CAAC,CAAC,KAC/B,OAAO,WAAW,SAAS,KAAK,eAAe,QAAQ,aAAa,CAAC,CACvE;EACF,YAAY,WACV,IAAI,OAAO,gBAAgB,QAAQ,IAAI,IAAI,GAAG,CAAC,CAAC,IAAI,OAAO,KAAK,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,KAChF,OAAO,QAAQ,aAAa,SAAS,KAAK,UAAU,MAAM,CAAC,CAAC,CAC9D;EACF,cAAc,WACZ,IAAI,OAAO,gBAAgB,QAAQ;GACjC,MAAM,OAAO,IAAI,IAAI,GAAG;GACxB,KAAK,OAAO,OAAO,KAAK,MAAM,CAAC;GAC/B,OAAO;EACT,CAAC,CAAC,CAAC,KAAK,OAAO,QAAQ,aAAa,SAAS,KAAK,YAAY,MAAM,CAAC,CAAC,CAAC;EACzE,iBAAiB,MAAM,SACrB,IAAI,OAAO,cAAc,QACvB,IAAI,IAAI,GAAG,CAAC,CAAC,IAAIC,iBAAe,SAAS,MAAM,IAAI,GAAG;GAAE;GAAM;EAAK,CAAC,CACtE,CAAC,CAAC,KAAK,OAAO,QAAQ,aAAa,SAAS,KAAK,eAAe,MAAM,IAAI,CAAC,CAAC,CAAC;EAC/E,mBAAmB,MAAM,SACvB,IAAI,OAAO,cAAc,QAAQ;GAC/B,MAAM,OAAO,IAAI,IAAI,GAAG;GACxB,KAAK,OAAOA,iBAAe,SAAS,MAAM,IAAI,CAAC;GAC/C,OAAO;EACT,CAAC,CAAC,CAAC,KAAK,OAAO,QAAQ,aAAa,SAAS,KAAK,iBAAiB,MAAM,IAAI,CAAC,CAAC,CAAC;EAClF,aAAa,MAAM,SAAS,aAAa,SAAS,KAAK,WAAW,MAAM,IAAI,CAAC;EAC7E,SAAS,QAAQ,aAAa,SAAS,KAAK,OAAO,GAAG,CAAC;EACvD,eAAe,MAAM,SACnB,gBAAgB,QAAQ,OAAO,CAAC,CAAC,KAC/B,OAAO,WAAW,SAAS,KAAK,aAAa,MAAM,IAAI,CAAC,CAC1D;EACF,UAAU,UAAU,gBAAgB,QAAQ,UAC1C,OAAO,QACL,gBAAgB,QAAQ,OAAO,CAAC,CAAC,KAC/B,OAAO,WAAW,SAChB,OAAO,OAAO,KAAK,QAAQ,UAAU,gBAAgB,QAAQ,KAAK,CAAC,CACrE,CACF,CACF;EAIF,WAAW,UAAU,gBAAgB,QAAQ,UAC3C,OAAO,IAAI,aAAa;GACtB,MAAM,QAAQ,OAAO,OAAO;GAC5B,MAAM,OAAO,OAAO,IAAI,KAAkC,KAAA,CAAS;GACnE,OAAO,OAAO,OACZ,OAAO,WAAW,gBAAgB,QAAQ,OAAO,IAAI,SACnD,OAAO,IAAI,aAAa;IACtB,MAAM,WAAW,OAAO,IAAI,UAAU,MAAM,KAAA,CAAS;IACrD,IAAI,aAAa,KAAA,GAAW,OAAO,MAAM,MAAM,UAAU,KAAK,IAAI;IAClE,MAAM,QAAQ,OAAO,MAAM,KAAK,KAAK;IACrC,OAAO,KACJ,SAAS,UAAU,gBAAgB,QAAQ,KAAK,CAAC,CACjD,KAAK,MAAM,QAAQ,KAAK,CAAC;IAC5B,OAAO,IAAI,IAAI,MAAM,KAAK;GAC5B,CAAC,CACH,CAAC,CAAC,KAAK,OAAO,MAAM,GACpB,KACF;EACF,CAAC;EACH,OAAO,YACL,aAAa,SACX,KAAK,SAAS,KAAA,IACV,OAAO,KACL,IAAI,eAAe,EACjB,SAAS,0DACX,CAAC,CACH,IACA,KAAK,KAAK,OAAO,CACvB;EACF,SAAS,SAAS,SAAS,aAAa,SAAS,KAAK,OAAO,SAAS,IAAI,CAAC;EAC3E,gBAAgB,SAAS,SAAS,aAAa,SAAS,KAAK,cAAc,SAAS,IAAI,CAAC;EACzF,gBAAgB,gBAAgB,QAAQ,OAAO,CAAC,CAAC,KAC/C,OAAO,WAAW,SAAS,KAAK,cAAc,CAChD;CACF;AACF,CAAC;;;;;;;AAQH,MAAa,OAAO,YAClB,OAAO,IAAI,aAAa;CACtB,MAAM,cAAc,QAAQ,eAAe;CAC3C,MAAM,QAAQ,QAAQ,SAAS,YAAY;CAE3C,MAAM,cAAc,OAAO,MAAM,KAAK;CACtC,MAAM,gBAAgB,OAAOC,OAA4B,WAAW,CAAC,CAAC,KACpE,MAAM,QAAQ,WAAW,CAC3B;CACA,MAAM,SAAS,OAAOC,SAAO,KAAK;EAAE,SAAS;EAAe,UAAU,QAAQ;CAAU,CAAC,CAAC,CAAC,KACzF,MAAM,QAAQ,WAAW,CAC3B;CAEA,MAAM,UAAU,OAAO,gBAAgB,KAA4B,MAAM;CACzE,MAAM,UAAU,OAAO,gBAAgB,KAAW,QAAQ;CAE1D,MAAM,aAAa,OAAO,IAAI,aAAa;EACzC,MAAM,eAAe,QAAQ,gBAAgB,QAAQ,QAAQ;EAI7D,MAAM,SAAS,cAAc,QAC3B,OAAO,aAAa,KAAK;GACvB,UAAU;GACV,eAAe,QAAQ;GACvB,UAAU,QAAQ;GAClB,GAAI,QAAQ,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,QAAQ,QAAQ;GACpE,GAAI,QAAQ,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU,QAAQ,SAAS;EACzE,CAAC,CAAC,CAAC,KAAK,OAAO,QAAQ,QAAQ,YAAY,CAAC,CAC9C;EACA,MAAM,cAAc,OAAOD,OAA4B,WAAW;EAClE,OAAOE,aAAW,IAAI;GACpB,SAAS;GACT;GACA,OAAO,QAAQ;GACf,GAAI,QAAQ,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU,QAAQ,SAAS;GACvE,GAAI,QAAQ,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM,QAAQ,KAAK;EAC7D,CAAC;EACD,OAAO,OAAO,WAAW,OAAO,QAAQ,KAAK,OAAO,MAAM,CAAC;EAC3D,OAAO,gBAAgB,IAAI,SAAS,WAAW,WAAW,MAAM,CAAC;EACjE,OAAO,gBAAgB,IAAI,SAAS,MAAM;EAI1C,OAAO,MAAM,MAAM,aAAa,KAAK,IAAI;CAC3C,CAAC;CAED,IAAI,UAAU,KAAA,GAAW;EACvB,OAAO,OAAO,WACZ,iJAEF;EACA,OAAO;CACT,OAKE,OAAO,OAAO,WACZ,YAAY,OAAO,QAAQ,aAAa,CAAC,CAAC,KACxC,OAAO,QAAQ,UAAU,GACzB,OAAO,UAAU,UAAU,OAAO,SAAS,sCAAsC,KAAK,CAAC,GACvF,OAAO,MACT,CACF;CAGF,MAAM,OAAO,OAAO,cAAc,OAAO;CAMzC,OAAO;EAAE,QALM,OAAO,SACpB,QAAQ,OACR,MACA,QAAQ,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM,QAAQ,KAAK,CAE3C;EAAG,MAAM,gBAAgB,QAAQ,OAAO;CAAE;AAC1D,CAAC;;;;;;;;;;;;;;;;;;;;;;AC9SH,MAAa,aAAa;AAE1B,MAAM,cAAc,aAAqB,UAAmB,IAAI,WAAW;CAAE;CAAS;AAAM,CAAC;AAE7F,MAAM,YAAe,SAAiB,QACpC,OAAO,WAAW;CAAE,KAAK;CAAK,OAAO,WAAW,OAAO;AAAE,CAAC;;AAG5D,MAAM,YAAY,OAAO,eACtB,QAAgB,KAAK,MAAM,mBAAmB,GAAG,CAAC,CACrD;AAEA,MAAM,YAAY,aAAiE;CACjF,MAAM,MAAM,SAAS,QAAQ,IAAI,UAAU;CAC3C,IAAI,QAAQ,MAAM,OAAO,KAAA;CACzB,OAAO,OAAO,eAAe,UAAU,GAAG,CAAC;AAC7C;;AAGA,MAAa,QAAQ,WAAoD;CACvE,QAAQ,YACN,SAAS,0BAA0B,QAAQ,aAAa,MAAM,MAAM,OAAO,CAAC,CAAC,CAAC,KAC5E,OAAO,KAAK,aAAa;EACvB,IAAI,aAAa,KAAA,GAAW,OAAO,KAAA;EACnC,MAAM,OAAO,SAAS,QAAQ;EAE9B,OAAO,SAAS,KAAA,IAAY,KAAA,IAAY;GAAE;GAAU;EAAK;CAC3D,CAAC,CACH;CACF,MAAM,SAAS,UAAU,SACvB,SAAS,wBAAwB,QAAQ,OAAO,YAAY;EAC1D,MAAM,UAAU,IAAI,QAAQ,SAAS,OAAO;EAC5C,QAAQ,IAAI,YAAY,mBAAmB,KAAK,UAAU,IAAI,CAAC,CAAC;EAChE,MAAM,SAAS,IAAI,SAAS,SAAS,MAAM;GACzC,QAAQ,SAAS;GACjB,YAAY,SAAS;GACrB;EACF,CAAC;EACD,MAAM,MAAM,IAAI,SAAS,MAAM;CACjC,CAAC;CACH,SAAS,YACP,OAAO,OAAO,SAAS,2BAA2B,QAAQ,aAAa,MAAM,OAAO,OAAO,CAAC,CAAC;CAC/F,iBAAiB,SACf,OAAO,IAAI,aAAa;EACtB,MAAM,WAAW,OAAO,SAAS,kCAAkC,MAAM,KAAK,CAAC;EAC/E,OAAO,OAAO,QACZ,WACC,YACC,OAAO,IAAI,aAAa;GACtB,MAAM,WAAW,OAAO,SAAS,0BAA0B,QAAQ,aACjE,MAAM,MAAM,OAAO,CACrB;GACA,MAAM,OAAO,aAAa,KAAA,IAAY,KAAA,IAAY,SAAS,QAAQ;GACnE,IAAI,SAAS,KAAA,KAAa,KAAK,KAAK,MAAM,QAAQ,KAAK,SAAS,GAAG,CAAC,GAClE,OAAO,SAAS,2BAA2B,QAAQ,aACjD,MAAM,OAAO,OAAO,CACtB;EAEJ,CAAC,GACH;GAAE,aAAa;GAAG,SAAS;EAAK,CAClC;CACF,CAAC;AACL;;AAGA,MAAa,QACX,MACA,iBAEA,SAAS,yBAAyB,KAAK,WACpC,gBAAiB,WAAW,OAAA,CAA6B,KAAK,IAAI,CACrE,CAAC,CAAC,KAAK,OAAO,IAAI,IAAI,CAAC;;;;;;;AA6BzB,MAAa,gBAAgB,YAAiC;CAC5D,MAAM,WAAW,YACf,OAAO,WAAW;EAChB,YAAY,QAAQ,SAAS,WAAW,MAAA,CAAO,OAAO;EACtD,QAAQ,UAAU,IAAI,eAAe;GAAE,SAAS,oBAAoB,QAAQ;GAAO;EAAM,CAAC;CAC5F,CAAC;CAEH,MAAM,iBACJ,SACA,WAEA,OAAO,IAAI,aAAa;EACtB,MAAM,WAAW,OAAO,QAAQ,OAAO;EACvC,IAAI,CAAC,SAAS,IAAI,OAAO;EACzB,MAAM,MAAM,OAAO,MAAM;EACzB,MAAM,aAAa,OAAO;EAC1B,MAAM,OAAqC;GACzC,KAAK,QAAQ;GACb,UAAU;GACV,SAAS,MAAM,SAAS,SAAS,OAAO,SAAS;GACjD,WAAW,eAAe,KAAA,IAAY,KAAA,IAAY,MAAM,SAAS,SAAS,UAAU;GACpF,MAAM,OAAO,QAAQ,CAAC;EACxB;EAEA,OAAO,QAAQ,MAAM,IAAI,SAAS,SAAS,MAAM,GAAG,IAAI;EACxD,OAAO;CACT,CAAC;CAEH,QAAQ,YACN,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,QAAQ,WAAW,QAAQ,QAAQ,OAAO,OAAO,IAAI,KAAA;EACpE,IAAI,WAAW,KAAA,GACb,OAAO;GAAE,UAAU,OAAO,QAAQ,OAAO;GAAG,YAAY,KAAA;EAAU;EAEpE,MAAM,SAAS,OAAO,QAAQ,MAAM,MAAM,OAAO;EACjD,MAAM,MAAM,OAAO,MAAM;EACzB,IAAI,WAAW,KAAA,GAAW;GACxB,MAAM,EAAE,MAAM,aAAa;GAC3B,IAAI,MAAM,KAAK,SACb,OAAO;IAAE;IAAU,YAAY,KAAA;GAAU;GAE3C,IAAI,KAAK,cAAc,KAAA,KAAa,MAAM,KAAK,WAC7C,OAAO;IACL;IACA,YAAY,OAAO,OAAO,cAAc,SAAS,MAAM,CAAC;GAC1D;EAEJ;EACA,OAAO;GAAE,UAAU,OAAO,cAAc,SAAS,MAAM;GAAG,YAAY,KAAA;EAAU;CAClF,CAAC;AACL;;;;;;;;;;;;AC/IA,MAAa,gBAAgD,UAAU;;AAGvE,MAAa,oBAAgE,UAAU;AAgCvF,MAAM,gBAAyB;CAAE,aAAa,CAAC;CAAG,MAAM,CAAC;AAAE;;AAG3D,MAAM,aAAa,UAA6C;CAC9D,QAAQ,MAAM,MAAd;EACE,KAAK,iBACH,OAAO;GAAE,aAAa,MAAM;GAAa,MAAM,MAAM;EAAK;EAC5D,KAAK,eACH,OAAO;GAAE,aAAa,MAAM,eAAe,CAAC;GAAG,MAAM,CAAC;EAAE;EAC1D,SACE,OAAO;CACX;AACF;;;;;;;AAQA,MAAa,qBACX,WAEA,OAAO,KACL,OAAO,IAAI,SAAS,GACpB,OAAO,QACJ,YACC,IAAI,wBAAwB,QAAQ,WAAW,KAC/C,IAAI,wBAAwB,QAAQ,IAAI,CAC5C,CACF;;;;;;;;;;AAsBF,MAAa,uBACX,YAEA,OAAO,IAAI,aAAa;CACtB,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,aAAa,QAAQ,cAAc;CACzC,OAAO,OAAO,WACZ,OAAO,WAAW,QAAQ,UAAU,YAAY;EAI9C,MAAM,OAAO,UAAU,aAAa,SAAS;GAAE;GAAQ;EAAW,CAAC;EACnE,OAAO,IAAI,qBAAqB,IAAI,IAChC,OAAO,OACP,QAAQ,MACL,eAAe,IAAI,CAAC,CACpB,KACC,OAAO,OAAO,UAAU,OAAO,WAAW,kCAAkC,KAAK,CAAC,CACpF;CACR,CAAC,CACH;AACF,CAAC;;;;;;;;;;AAgCH,MAAa,WACX,YACA,QACA,UAA0B,CAAC,MACT;CAElB,MAAM,UADS,QAAQ,UAAU,cAAA,CACX,UAAU;CAChC,IAAI,OAAO,SAAA,IACT,OAAO;EAAE,MAAM,CAAC,MAAM;EAAG,gBAAgB;CAAK;CAEhD,MAAM,aAAa,QAAQ,cAAc;CACzC,OAAO;EACL,MAAM,CAAC,QAAQ,GAAG,OAAO,KAAK,OAAO,WAAW,YAAY,EAAE,CAAC,CAAC;EAChE,gBAAgB;CAClB;AACF;;;;;;;;;AAUA,MAAa,mBACX,YACA,QACA,UAA0B,CAAC,MAE3B,OAAO,IAAI,aAAa;CACtB,MAAM,SAAS,QAAQ,YAAY,QAAQ,OAAO;CAClD,IAAI,OAAO,gBAAgB;EACzB,OAAO,OAAO,WACZ,kDAAkD,WAAW,KACxD,OAAO,OAAO,+BACrB;EACA,OAAO,OAAO,OAAO,UAAU,6BAA6B,CAAC;CAC/D;CACA,OAAO;AACT,CAAC;;;;ACzJH,MAAM,wBACJ,EAAE,gBAAgB,WAAW,SAAS,KAAK,EAAE,UAAU,WAAW,UAAU,MAAM,IAC9E,WAAW,YACX,KAAA;;AAGN,MAAM,wBAAiC,gBAAgB,CAAC,EAAE,UAAU;;;;;AAMpE,MAAM,WACJ,QACA,MACA,YAEA,WAAW,KAAA,IACP,OAAO,OACP,OAAO,eACL,OAAO,WAAW;CAChB,OAAO,iBAAiB,MAAM,OAAO;AACvC,CAAC,SACK,OAAO,WAAW,OAAO,oBAAoB,MAAM,OAAO,CAAC,CACnE,CAAC,CAAC,KAAK,OAAO,MAAM;;AAG1B,MAAM,SACJ,GACA,MAEA,MAAM,kBAAkB,MAAM,iBAC1B,iBACA,MAAM,gBAAgB,MAAM,eAC1B,eACA;;;;;;;;AASR,MAAa,2BACX,WACA,UAAsC,CAAC,MAEvC,OAAO,IAAI,aAAa;CACtB,MAAM,MAA+B,WAAW;CAChD,MAAM,MACJ,WAAW;CAIb,MAAM,gBAAgB,gBAAgB;CACtC,MAAM,SAAS,OAAO,gBAAgB,KACpC,gBAAgB,cAAc,cAChC;CAEA,MAAM,eAAe,OAAO,MAAM,UAAsC;CACxE,MAAM,aAAa,OAAO,MAAM,UAAyB;CAEzD,OAAO,OAAO,WACZ,OAAO,UAAU,YAAY,CAAC,CAAC,KAC7B,OAAO,YAAY,SAAS,gBAAgB,IAAI,QAAQ,IAAI,CAAC,CAC/D,CACF;CAEA,IAAI,QAAQ,KAAA,GAAW;EACrB,OAAO,QAAQ,KAAK,gBAAgB,MAAM,YAAY,cAAc,WAAW,CAAC;EAChF,OAAO,QAAQ,KAAK,iBAAiB,MAAM,YAAY,cAAc,cAAc,CAAC;CACtF,OAAO,IAAI,gBAAgB,MAAM,KAAA,GAAW;EAI1C,IAAI,aAAa;EACjB,OAAO,OAAO,WACZ,OAAO,OACL,OAAO,WAAW;GAChB,MAAM,SAAS,gBAAgB;GAC/B,IAAI,WAAW,YAAY;IACzB,aAAa;IACb,MAAM,YAAY,cAAc,SAAS,cAAc,cAAc;GACvE;EACF,CAAC,GACD,SAAS,OAAO,QAAQ,gBAAgB,WAAW,CACrD,CACF;CACF;CACA,OAAO,QAAQ,KAAK,0BAA0B;EAC5C,IAAI,KAAK,oBAAoB,WAC3B,MAAM,YAAY,YAAY,EAAE,QAAQ,mBAAmB,CAAC;CAEhE,CAAC;CAED,OAAO;EACL,MAAM,UAAU;EAChB,MAAM,UAAU;EAChB,OAAO,OAAO,MAAM,UAAU,OAAO,OAAO,UAAU,UAAU,CAAC;EACjE,QAAQ,OAAO,QAAQ,gBAAgB,IAAI,MAAM,GAAG,UAAU,QAAQ,KAAK;EAK3E,eAAe,OAAO,UACpB,gBAAgB,QAAQ,MAAM,GAC9B,UAAU,aACZ,CAAC,CAAC,KACA,OAAO,eACyC,KAAA,IAC7C,mBAAmB,CAAC,iBAAiB,qBAAqB;GAIzD,OAAO,CAAC,iBAAiB,CAFvB,sBAAsB,kBAAkB,oBAAoB,cAClC,cAAc,MAAM,iBAAiB,eAAe,CAC/C,CAAC;EACpC,CACF,GACA,OAAO,aAAa,GAAG,MAAM,MAAM,CAAC,CACtC;CACF;AACF,CAAC;;;;;;;;;;;;;;;;ACvEH,MAAa,aAAa;;AAG1B,MAAa,sBAAsC;;AA+BnD,MAAM,iBAAiB,SACrB,EAAE,gBAAgB,IAAI,KAAK,cAAc,QAAQ,EAAE,WAAW,KAAK,QAAQ;;AAG7E,MAAM,iBAAiB,iBAA0E;CAC/F,MAAM,OAAO,cAAc;CAC3B,OAAO,cAAc,IAAI,IAAI,OAAO,KAAA;AACtC;;AA+CA,MAAM,mBAAgE,OAAO,cAAc;CACzF,MAAM,YAAgD,WAAW,WAAW;CAC5E,OAAO,cAAc,KAAA,IACjB,OAAO,YACP,OAAO,iBAAiB,UAAU,gBAAgB,CAAC,CAAC,CAAC,KACnD,OAAO,oBAAoB,KAAA,CAAS,CACtC;AACN,CAAC;;;;;;;;;;;AAYD,MAAa,YAAY,UAA2B,CAAC,MACnD,OAAO,IAAI,aAAa;CACtB,MAAM,MAAM,QAAQ,OAAA;CACpB,MAAM,eAAe,OAAO,QAAQ,gBAAgB;CACpD,MAAM,OAAO,cAAc,YAAY;CACvC,IAAI,SAAS,KAAA,GAAW;EACtB,OAAO,OAAO,SACZ,kFACF;EACA,OAAO;CACT;CACA,OAAO,OAAO,OAAO,iBAAiB,KAAK,SAAS,GAAG,CAAC,CAAC,CAAC,KACxD,OAAO,GAAG,YAAqB,GAC/B,OAAO,OAAO,UACZ,OAAO,SAAS,wCAAwC,KAAK,CAAC,CAAC,KAC7D,OAAO,GAAG,SAAkB,CAC9B,CACF,CACF;AACF,CAAC;;AAGH,MAAM,mBAA2C,OAAO,WAChD,WAAW,WAAW,WAAW,KACzC;;;;;;;;;;;;;;;;AA0BA,MAAa,qBACX,QACA,UAAoC,CAAC,MAErC,OAAO,IAAI,aAAa;CACtB,MAAM,UAAU,QAAQ,WAAW;CAKnC,MAAM,cAAiB,WACrB,OAAO,WACL,OAAO,WAAW,cAAc,SAAS,OAAO,CAAC,GACjD,EACE,kBAAkB,KACpB,CACF;CACF,OAAO,WACL,OAAO,aAAa,KAClB,OAAO,QAAQ,UAAU,MAAM,SAAS,mBAAmB,GAC3D,OAAO,mBAAmB,OAAO,CACnC,CACF;CACA,OAAO,WACL,OAAO,aAAa,KAAK,OAAO,QAAQ,YAAY,QAAQ,cAAc,MAAM,CAAC,CACnF;AACF,CAAC;;;;;;;;;AA6BH,MAAa,oBAAmC,OAAO,IACrD,oDACF;;;;;;;;;;;AA0BA,MAAa,SACX,QACA,YAEA,OAAO,IAAI,aAAa;CACtB,IAAI,QAAQ,aAAa,mBAAmB;EAC1C,MAAM,UAAU,OAAO,QAAQ;EAC/B,MAAM,UAAU,OAAO,OAAO;EAK9B,IAAI,YAAY,KAAA,KAAa,YAAY,SAAS;GAChD,OAAO,OAAO,SACZ,oFACF;GACA,OAAO;EACT;CACF;CACA,OAAO,OAAO,OAAO,KAAK,KACxB,OAAO,GAAG,SAAkB,GAC5B,OAAO,SAAS,kCAAkC,OAAO,QAAQ,cAAuB,CAAC,CAC3F;AACF,CAAC,CAAC,CAAC,KAAK,OAAO,SAAS,+BAA+B,CAAC;;AAK1D,IAAa,eAAb,cAAkC,OAAO,YAGvC,CAAC,CAAC,gBAAgB;CAClB,KAAK,OAAO;CACZ,SAAS,OAAO;AAClB,CAAC,CAAC,CAAC,CAAC;;;;;;;;;;;;;AA8EJ,MAAaC,YAAU,YAAqC;CAC1D,MAAM,MAAM,QAAQ,OAAA;CACpB,MAAM,QAA4B,QAAQ,SAAS;CACnD,MAAM,eAAe,QAAQ,gBAAA;CAC7B,MAAM,WAAW,OAAO,QAAQ,gBAAgB,KAAK,OAAO,KAAe,CAAC,CAAC;CAK7E,MAAM,eAAsD,gBAAgB,QAC1E,QACF,CAAC,CAAC,KACA,OAAO,OAAO,OAAO,MAAM,GAC3B,OAAO,KAAK,SAAS,KAAK,KAAK,GAC/B,OAAO,SACP,OAAO,SAAS,SAAU,OAAO,OAAO,IAAI,IAAI,OAAO,QAAQ,KAAK,KAAK,IAAI,OAAO,KAAM,GAC1F,OAAO,cAAc;EACnB,UAAU;EACV,cACE,OAAO,KACL,IAAI,aAAa;GACf;GACA,SAAS,kCAAkC,SAAS,OAAO,SAAS,gBAAgB,YAAY,CAAC,EAAE;EACrG,CAAC,CACH;CACJ,CAAC,CACH;CAEA,MAAM,UAAU,UACd,OAAO,WACL,aAAa,KACX,OAAO,UAAU,UACf,OAAO,SACL,MAAM,eAAe,OACjB,GAAG,MAAM,QAAQ,qFACjB,MAAM,OACZ,CACF,GACA,OAAO,SAAS,EAAE,OAAO,KAAK,eAC5B,IAAI,KACF,OAAO,UAAU,UACf,MAAM,SAAS,mBACX,OAAO,SACL,MAAM,eAAe,OACjB,yGACA,gEACJ,KACF,IACA,OAAO,WAAW,6CAA6C,KAAK,CAC1E,GACA,OAAO,eAAe,QAAQ,CAChC,CACF,CACF,CACF;CAEF,MAAM,YAAY,UAA2B;EAE3C,IAAI,MAAM,QAAQ,KAAK;EACvB,MAAM,UAAU,OAAO,KAAK,CAAC;CAC/B;CACA,MAAM,mBAAmB,QAAQ,QAAQ;CAEzC,MAAM,eAAe,OAAO,WAAW,MAAM,YAAY;CACzD,MAAM,SAA4B,WAChC,OAAO,IAAI,aAAa;EACtB,MAAM,QAAkB;GACtB,OAAO,MAAM,QAAQ,EAAE,UAAU,QAAQ,SAAS,CAAC;GACnD,UAAU,OAAO,OAAO,QAAe;EACzC;EACA,OAAO,OAAO,eAAe,gBAAgB,IAAI,UAAU,OAAO,KAAK,KAAK,CAAC,SAE3E,gBAAgB,OAAO,WAAW,YAChC,OAAO,OAAO,OAAO,KAAK,QAAQ,UAAU,QAAQ,OAAO,KAAK,IAAI,OACtE,CACF;EACA,IAAI,QAAQ,sBAAsB,OAChC,OAAO,kBAAkB,QAAQ;GAC/B;GACA;GACA,GAAI,QAAQ,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,QAAQ,QAAQ;EACtE,CAAC;CAEL,CAAC;CAEH,OAAO;EAAE;EAAK;CAAM;AACtB;;;;AC7bA,MAAM,iBAAiB,UAA8B;CACnD,MAAM,OAAO,MAAM;CACnB,IAAI,CAAC,EAAE,YAAY,MAAM,MAAM,KAAK,CAAC,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO;CACrE,MAAM,OAAO,KAAK;CAClB,OAAO,OAAO,MACZ,OAAO,UAAU,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,GACxC;EAAE,WAAWC,SAAO;EAAQ,iBAAiB;CAAM,CACrD;AACF;;;;;;;;;;;;;AAoBA,MAAa,UAAU,YAAqC;CAC1D,MAAM,QAA4B,QAAQ,SAAS;CACnD,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,gBAAgB,SAAS,SAAS,SAAS,gBAAgB,YAAY,CAAC;CAI9E,IAAI,WAAoC,OAAO,KAAK;CACpD,IAAI,UAA2C,CAAC;CAGhD,MAAM,gBAAgB,WACpB,QAAQ,aAAaC,oBACjB,OAAO,QAAQ,IAAI,IACnB,OAAO,IACL,CAAC,QAAQ,UAAU,OAAO,SAAS,KAAK,OAAO,oBAAoB,KAAA,CAAS,CAAC,CAAC,GAC9E,EAAE,aAAa,EAAE,CACnB,CAAC,CAAC,KAAK,OAAO,KAAK,CAAC,SAAS,aAAa,YAAY,KAAA,KAAa,YAAY,OAAO,CAAC;CAE7F,MAAM,OAAO,WACX,OAAO,IAAI,aAAa;EACtB,IAAI,EAAE,OAAO,aAAa,MAAM,IAAI;GAClC,OAAO,OAAO,SACZ,wEACF;GACA,OAAO;EACT;EACA,IAAI,QAAQ,WAAW,QACrB,OAAO,OAAO,OAAO,KAAK,KACxB,OAAO,GAAG,QAAiB,GAC3B,OAAO,OAAO,UACZ,OAAO,SACL,6DACA,KACF,CAAC,CAAC,KAAK,OAAO,GAAG,YAAqB,CAAC,CACzC,CACF;EAEF,OAAO,OAAOC,SAAwB;GACpC;GACA,cAAc,OAAO,WAAW,MAAM,YAAY;EACpD,CAAC;CACH,CAAC;CAGH,MAAM,eACJ,OAAO,MAAM,UAAU;EACrB,SAAS,SAAS,KAAK,IAAI,IAAI,KAAK,MAAM,CAAC;EAC3C,cACE,IAAI,SAAuC,YAAY;GACrD,MAAM,WAAW,SAAmB;IAClC,aAAa,KAAK;IAClB,QAAQ,KAAK,IAAI,IAAI,KAAK,MAAM,CAAC,CAAC;GACpC;GACA,MAAM,QAAQ,iBAAiB;IAC7B,UAAU,QAAQ,QAAQ,UAAU,UAAU,OAAO;IACrD,QAAQ,cAAc;GACxB,GAAG,aAAa;GAChB,UAAU,CAAC,GAAG,SAAS,OAAO;EAChC,CAAC;CACL,CAAC;CAEH,MAAM,YAAY,UAA2B;EAE3C,IAAI,CAAC,cAAc,KAAK,GAAG;EAC3B,MAAM,UAAU,OAAO,CAAC;CAC1B;CACA,MAAM,mBAAmB,QAAQ,QAAQ;CAEzC,MAAM,SAA4B,WAChC,OAAO,IAAI,aAAa;EACtB,MAAM,OAAiB;GAAE;GAAQ,KAAK,OAAO,eAAe,OAAO,OAAO,QAAe,CAAC;EAAE;EAC5F,OAAO,OAAO,eACZ,OAAO,WAAW;GAChB,WAAW,OAAO,KAAK,IAAI;GAC3B,MAAM,QAAQ;GACd,UAAU,CAAC;GACX,MAAM,SAAS,YAAY,QAAQ,IAAI,CAAC;EAC1C,CAAC,SAEC,OAAO,WAAW;GAEhB,IAAI,OAAO,OAAO,QAAQ,KAAK,SAAS,UAAU,MAAM,WAAW,OAAO,KAAK;EACjF,CAAC,CACL;CACF,CAAC;CAEH,OAAO,EAAE,MAAM;AACjB"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
export { __exportAll as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thomasfosterau/legation-browser",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser-runtime adapters for the legation engine: service-worker engine host, IndexedDB client store, tab mirrors over BroadcastChannel, and a Cache API HTTP cache",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"broadcastchannel",
|
|
7
|
+
"browser",
|
|
8
|
+
"effect",
|
|
9
|
+
"indexeddb",
|
|
10
|
+
"offline-first",
|
|
11
|
+
"service-worker",
|
|
12
|
+
"sync-engine",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/thomasfosterau/legation/tree/trunk/packages/browser#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/thomasfosterau/legation/issues"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Thomas Foster",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/thomasfosterau/legation.git",
|
|
24
|
+
"directory": "packages/browser"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"CHANGELOG.md"
|
|
31
|
+
],
|
|
32
|
+
"type": "module",
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"main": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"import": "./dist/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./package.json": "./package.json"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@thomasfosterau/legation": "^0.1.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@effect/platform-browser": "4.1.2",
|
|
51
|
+
"@effect/vitest": "4.1.2",
|
|
52
|
+
"effect": "4.1.2",
|
|
53
|
+
"fake-indexeddb": "^6.2.7",
|
|
54
|
+
"vitest": "5.0.5"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@effect/platform-browser": "^4.0.0",
|
|
58
|
+
"effect": "^4.0.0"
|
|
59
|
+
}
|
|
6
60
|
}
|