@thomasfosterau/legation-alchemy-cloudflare 0.0.0 → 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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["make","make","make","make","make","make","DurableFanoutBroker.make","DurableCvrStore.make"],"sources":["../src/RoomProtocol.ts","../src/BindingTransport.ts","../src/D1Executor.ts","../src/DurableCvrStore.ts","../src/DurableFanoutBroker.ts","../src/EdgeInvalidation.ts","../src/FanoutShard.ts","../src/Room.ts","../src/RoomTransport.ts","../src/Worker.ts"],"sourcesContent":["/**\n * RoomProtocol\n *\n * The room's wire format, chosen for WebSocket **hibernation**: requests\n * (push/pull/read) travel as stateless HTTP POST JSON — a hibernated room\n * wakes, answers, and may be evicted again — while the WebSocket carries\n * exactly one thing: contentless **poke** frames. No server-side session\n * state survives eviction, so none is kept.\n *\n * Shard ↔ room traffic is not on this wire: it is typed RPC on the Durable\n * Object stubs (`FanoutShard.ShardObject`, `Room.RoomObject`).\n */\n\nimport type * as Effect from \"effect/Effect\";\nimport type { ProtocolError } from \"@thomasfosterau/legation\";\nimport { Protocol } from \"@thomasfosterau/legation/internal\";\n\ntype Decoder<A> = (input: unknown) => Effect.Effect<A, ProtocolError>;\n\n/**\n * The room's HTTP routes. A room answers a route by the request path's last\n * segment, so a request forwarded with its base path intact (`/sync/push`)\n * reaches the same route as `/push`.\n */\nexport const routes = {\n push: \"/push\",\n pull: \"/pull\",\n read: \"/read\",\n /** WebSocket upgrade: the poke channel. */\n socket: \"/socket\",\n} as const;\n\n/** One of the room's {@link routes}. */\nexport type Route = (typeof routes)[keyof typeof routes];\n\nconst ROUTES: ReadonlySet<string> = new Set([routes.push, routes.pull, routes.read, routes.socket]);\n\n/**\n * The route a request path addresses: its last segment, when that names one of\n * the {@link routes}.\n */\nexport const routeOf = (pathname: string): Route | undefined => {\n const route = pathname.slice(pathname.lastIndexOf(\"/\"));\n // SAFETY: `ROUTES` holds exactly the values of `routes`.\n return ROUTES.has(route) ? (route as Route) : undefined;\n};\n\n/** The single frame a room socket ever sends. */\nexport interface PokeFrame {\n readonly type: \"poke\";\n}\n\n/** The single poke frame a room socket sends, as a value to serialize. */\nexport const pokeFrame: PokeFrame = { type: \"poke\" };\n\n/** Re-exported request decoders — the room's HTTP bodies are the protocol's. */\nexport const decodePushRequest: Decoder<Protocol.PushRequest> = Protocol.decodePushRequest;\n/** The protocol's pull-request decoder. */\nexport const decodePullRequest: Decoder<Protocol.PullRequest> = Protocol.decodePullRequest;\n/** The protocol's read-request decoder. */\nexport const decodeReadRequest: Decoder<Protocol.ReadRequest> = Protocol.decodeReadRequest;\n","/**\n * BindingTransport\n *\n * A `ClientTransport` that reaches the sync server over a Cloudflare\n * **service binding** (`env.API.fetch`) rather than over the public network.\n *\n * This is the carrier for the ordinary two-Worker shape: a SvelteKit Worker\n * owns the domain and every HTML/SSR request, and the API Worker holding the\n * database is *binding-only* — `workersDev: false`, no custom domain — so the\n * front door stays the single enforcement point for security headers and\n * cache/auth ordering. `RoomTransport` covers the browser (public HTTP plus a\n * poke socket); nothing covered `env.API.fetch(...)`, which is what an SSR\n * render has to use.\n *\n * Two things have to survive the hop, and both are about *identity*:\n *\n * - **The inbound cookie.** The bound Worker resolves the caller's room from\n * the session, so a call that arrives without the browser's cookie is a\n * different caller — at best rejected, at worst answered from some other\n * room. Its CVR store is not the browser's, so its cookie names a\n * conversation the browser is not in, and the browser's first pull degrades\n * to the reset path with nothing surfaced.\n * - **The origin.** The bound Worker sees which front door the request came\n * through — origin checks, per-origin config, logging. A binding call\n * carries no origin of its own, so the inbound one is forwarded, and a\n * relative sync path resolves against the inbound URL.\n *\n * The client group id and the pull cookie need no special handling here: they\n * travel in the protocol bodies (`PullRequest.clientGroupId`,\n * `PullResponse.cookie`), which this carrier moves verbatim. That is the whole\n * CVR conversation, and it is what {@link Transport.ClientTransport} promises.\n *\n * There is no poke channel over a binding: a service binding is\n * request/response, and an SSR render pulls once. `pokes` is empty, which\n * costs nothing — pull is the correctness path (ADR-0008) and pokes are a\n * latency optimization for a long-lived client.\n */\n\nimport * as Effect from \"effect/Effect\";\nimport * as Stream from \"effect/Stream\";\nimport {\n TransportError,\n type Transport,\n type VersionNotSupportedError,\n} from \"@thomasfosterau/legation\";\nimport type { Protocol } from \"@thomasfosterau/legation/internal\";\nimport { routes } from \"./RoomProtocol.js\";\n\n/**\n * The object of a Cloudflare service binding (`env.API`). Declared structurally\n * so the transport needs no Workers types and a test can pass a plain\n * fetch-alike.\n */\nexport interface ServiceBinding {\n readonly fetch: (request: Request) => Promise<Response>;\n}\n\n/**\n * The inbound request being rendered — its cookie and origin are forwarded,\n * and a relative {@link MakeOptions.path} resolves against its URL. A standard\n * `Request` (SvelteKit's `event.request`) satisfies this structurally.\n */\nexport interface InboundRequest {\n readonly url: string;\n readonly headers: { readonly get: (name: string) => string | null };\n}\n\n/** Options for {@link make}. */\nexport interface MakeOptions {\n /** The service binding to the Worker running the sync server. */\n readonly binding: ServiceBinding;\n /** The inbound SSR request whose cookie and origin are forwarded. */\n readonly request: InboundRequest;\n /**\n * The sync base the bound Worker serves. A path (default `/sync`) resolves\n * against the inbound request's origin; an absolute URL overrides it.\n */\n readonly path?: string;\n /** Extra headers attached to every call (e.g. an internal service token). */\n readonly headers?: Readonly<Record<string, string>>;\n}\n\n/** The headers a binding call carries over from the inbound request. */\nconst forwarded = (request: InboundRequest): Record<string, string> => {\n const headers: Record<string, string> = {};\n const cookie = request.headers.get(\"cookie\");\n if (cookie !== null && cookie !== \"\") headers.cookie = cookie;\n // A top-level navigation usually carries no Origin header; the inbound URL's\n // origin is the front door either way, which is what the bound Worker wants.\n headers.origin = request.headers.get(\"origin\") ?? new URL(request.url).origin;\n return headers;\n};\n\n/**\n * Build the transport. Each call is one `binding.fetch` of a JSON POST —\n * no network hop, no public route on the bound Worker.\n */\nexport const make = (options: MakeOptions): Transport.ClientTransport => {\n const base = new URL(options.path ?? \"/sync\", options.request.url);\n const carried = forwarded(options.request);\n // Trailing slashes would double up against the protocol's leading-slash routes.\n const prefix = base.href.replace(/\\/+$/, \"\");\n\n const post =\n <A>(route: string) =>\n (body: unknown): Effect.Effect<A, TransportError | VersionNotSupportedError> =>\n Effect.tryPromise({\n try: async () => {\n const response = await options.binding.fetch(\n new Request(`${prefix}${route}`, {\n method: \"POST\",\n body: JSON.stringify(body),\n headers: { \"content-type\": \"application/json\", ...carried, ...options.headers },\n })\n );\n if (!response.ok) {\n const detail = (await response.json().catch(() => ({}))) as { error?: string };\n throw new TransportError({\n message: detail.error ?? `binding ${route} answered ${response.status}`,\n });\n }\n // Trusted JSON from this app's own Worker.\n return (await response.json()) as A;\n },\n catch: (cause) =>\n cause instanceof TransportError\n ? cause\n : new TransportError({ message: `binding ${route} request failed`, cause }),\n });\n\n return {\n push: post<Protocol.PushResponse>(routes.push),\n pull: post<Protocol.PullResponse>(routes.pull),\n read: post<Protocol.ReadResponse>(routes.read),\n // A binding is request/response: no socket, no pokes. Pull is correctness.\n pokes: Stream.empty,\n status: Effect.succeed(\"connected\"),\n statusChanges: Stream.succeed(\"connected\"),\n };\n};\n","/**\n * D1Executor\n *\n * A `SqlBackend` executor over Alchemy's Effect-native D1 client: point\n * `SqlBackend.make({ executor, ... })` at the `QueryDatabaseClient` that\n * `Cloudflare.D1.QueryDatabase(database)` binds, and the room's push/pull runs\n * against it. Statements go through the client's `prepare` / `bind` / `all` /\n * `run`, so the database is bound, named and typed the way the rest of an\n * Alchemy Worker binds it.\n *\n * The client's statements need Alchemy's `RuntimeContext` (they run only\n * inside a Worker or Durable Object event); {@link make} captures it once, so\n * the executor it returns needs nothing. The client reports a failed statement\n * as a defect (its effects have no error channel); the executor turns that\n * defect into a `BackendError`.\n *\n * **Transactions:** D1 exposes no interactive transactions (only `batch()`),\n * so `transaction` runs the effect as-is and the executor sets\n * `transactional: false`. `SqlBackend` then keeps trunk's order on D1: each\n * handler runs, then its mutation id is bumped, and the bump is not atomic with\n * the handler's writes. A pull between the two can carry a mutation's effect\n * without its watermark, and a crash after the writes and before the bump\n * makes the retry run the handler again, so a non-idempotent handler can apply\n * twice. A transient failure also leaves the handler's writes committed. D1\n * has no isolation either, so a handler's reads are not protected from another\n * client group's commit, and an increment can be lost (#353). This does not\n * meet Backend guarantee 1 (#342, #353). Apps that need that guarantee should\n * use Durable Object storage or Postgres via Hyperdrive; on D1, prefer\n * single-statement, idempotent handlers.\n */\n\nimport type { QueryDatabaseClient } from \"@alchemy.run/cloudflare/D1\";\nimport { RuntimeContext } from \"alchemy/RuntimeContext\";\nimport * as Effect from \"effect/Effect\";\nimport { BackendError } from \"@thomasfosterau/legation\";\nimport type { SqlBackend, Sql } from \"@thomasfosterau/legation/sql\";\n\n/**\n * Build the executor over a bound D1 client. Needs the `RuntimeContext` of the\n * Worker or Durable Object it runs in (the inner, per-instance phase of an\n * Alchemy Durable Object has it).\n */\nexport const make = (\n db: QueryDatabaseClient\n): Effect.Effect<SqlBackend.SqlExecutor, never, RuntimeContext> =>\n Effect.map(Effect.service(RuntimeContext), (runtime) => {\n /** Run one statement with the captured context; a failed statement is a `BackendError`. */\n const execute = <A>(operation: string, statement: Effect.Effect<A, never, RuntimeContext>) =>\n statement.pipe(\n Effect.provideService(RuntimeContext, runtime),\n Effect.catchDefect((cause) =>\n Effect.fail(new BackendError({ message: `D1 ${operation} failed`, cause }))\n )\n );\n return {\n query: (fragment: Sql.SqlFragment) =>\n execute(\n \"query\",\n db\n .prepare(fragment.text)\n .bind(...fragment.params)\n .all<SqlBackend.SqlRow>()\n ).pipe(Effect.map((result): ReadonlyArray<SqlBackend.SqlRow> => result.results ?? [])),\n run: (fragment: Sql.SqlFragment) =>\n execute(\n \"run\",\n db\n .prepare(fragment.text)\n .bind(...fragment.params)\n .run()\n ).pipe(Effect.asVoid),\n // See the module doc: no interactive transactions on D1, so the ledger's\n // claim-first order is not used (`transactional: false`).\n transaction: (effect) => effect,\n transactional: false,\n };\n });\n","/**\n * DurableCvrStore\n *\n * `Cvr.CvrStore` over a room's Durable Object storage, through Alchemy's\n * Effect-native `DurableObjectStorage` (`state.storage` of\n * `Cloudflare.DurableObjectState`): the CVRs live co-located with the user's\n * room (ADR-0022's reference topology, superseding the shared-KV variant of\n * ADR-0011 for this deployment). DO storage structured-clones `Map`s\n * natively, so the CVR's nested maps round-trip without serialization code.\n *\n * The storage's effects need Alchemy's `RuntimeContext`; {@link make} captures\n * it once. A failed storage call is a defect there (its effects have no error\n * channel); the store turns it into a `StoreError`.\n */\n\nimport type { DurableObjectStorage } from \"@alchemy.run/cloudflare/Workers\";\nimport { RuntimeContext } from \"alchemy/RuntimeContext\";\nimport * as Effect from \"effect/Effect\";\nimport { StoreError } from \"@thomasfosterau/legation\";\nimport type { Cvr } from \"@thomasfosterau/legation/internal\";\n\nconst KEY_PREFIX = \"legation:cvr:\";\n\n/** The stored form of one CVR: its nested row map. */\ntype StoredRows = ReadonlyMap<string, ReadonlyMap<string, string | number>>;\n\n/** Build the store over the room's `state.storage`, in the room's runtime context. */\nexport const make = (\n storage: DurableObjectStorage\n): Effect.Effect<Cvr.CvrStore, never, RuntimeContext> =>\n Effect.map(Effect.service(RuntimeContext), (runtime) => {\n /** Run one storage call with the captured context; a failure is a `StoreError`. */\n const execute = <A>(operation: string, call: Effect.Effect<A, never, RuntimeContext>) =>\n call.pipe(\n Effect.provideService(RuntimeContext, runtime),\n Effect.catchDefect((cause) =>\n Effect.fail(\n new StoreError({ message: `Durable Object storage ${operation} failed`, cause })\n )\n )\n );\n return {\n get: (cookie) =>\n execute(\"get\", storage.get<StoredRows>(`${KEY_PREFIX}${cookie}`)).pipe(\n Effect.map((rows) => (rows === undefined ? undefined : { rows }))\n ),\n set: (cookie, cvr) => execute(\"put\", storage.put(`${KEY_PREFIX}${cookie}`, cvr.rows)),\n remove: (cookie) =>\n execute(\"delete\", storage.delete(`${KEY_PREFIX}${cookie}`)).pipe(Effect.asVoid),\n };\n });\n","/**\n * DurableFanoutBroker\n *\n * The room-side `FanoutBroker` over the shard namespace of an Alchemy Durable\n * Object (`yield* LegationShard`): registration and publishing address one\n * shard **per topic** (`getByName(topic)` is the shard function), through the\n * shard's typed RPC stub (`FanoutShard.ShardObject`). A publish asks each topic's\n * shard for its leased rooms and pokes every room once, with all of its\n * matched topics, through {@link MakeOptions.poke}; the poke feed is fed by\n * {@link DurableBroker.notify} — the room hands an inbound poke's topics to\n * its bus.\n *\n * Registration sends one `register` per topic; a room's topic set is small\n * (one entry per active window), and leases make the calls idempotent. Both\n * are fire-and-forget latency optimizations: a lost call only delays the next\n * interval pull, so a failed call is dropped and a hung one is bounded by\n * {@link MakeOptions.timeout}. Alchemy's RPC stubs take no abort signal, so a\n * call that times out is abandoned rather than aborted: the broker stops\n * waiting for it, and workerd cancels it with the event that made it.\n */\n\nimport type * as Workers from \"@alchemy.run/cloudflare/Workers\";\nimport { RuntimeContext } from \"alchemy/RuntimeContext\";\nimport * as Arr from \"effect/Array\";\nimport * as Duration from \"effect/Duration\";\nimport * as Effect from \"effect/Effect\";\nimport * as Exit from \"effect/Exit\";\nimport * as PubSub from \"effect/PubSub\";\nimport * as R from \"effect/Record\";\nimport type * as Scope from \"effect/Scope\";\nimport type { FanoutBroker, FanoutTopics } from \"@thomasfosterau/legation/internal\";\nimport type * as FanoutShard from \"./FanoutShard.js\";\n\n/** Options for {@link make}. */\nexport interface MakeOptions {\n /** The shard namespace (`yield* LegationShard` at the room's init). */\n readonly shards: Workers.DurableObject<FanoutShard.ShardObject>;\n /**\n * Poke one room with the topics of a publish that matched its leases. The\n * room passes its own namespace's `poke` RPC (and delivers its own pokes\n * locally).\n */\n readonly poke: (\n roomId: string,\n topics: ReadonlyArray<FanoutTopics.Topic>\n ) => Effect.Effect<void, never, RuntimeContext>;\n /**\n * Bound on one shard or room call. A call that exceeds it is logged and\n * dropped, so a hung shard cannot hold the publish that awaits it. Default 5\n * seconds.\n */\n readonly timeout?: Duration.Input | undefined;\n}\n\n/** The broker plus the room's poke inlet (wire it to the room's `poke` RPC). */\nexport interface DurableBroker {\n readonly broker: FanoutBroker.FanoutBroker;\n /** Feed an inbound poke into the broker's subscribers. */\n readonly notify: (topics: ReadonlyArray<FanoutTopics.Topic>) => Effect.Effect<void>;\n}\n\n/** A shard's answer when its call failed or timed out: nobody to poke. */\nconst noRooms: ReadonlyArray<string> = [];\n\n/** How many shard (or room) calls run at once. */\nconst CONCURRENCY = 8;\n\n/**\n * Build the room's {@link DurableBroker} over the shard namespace; its poke feed\n * lives as long as the scope. Needs the room's `RuntimeContext`, which it\n * captures for its calls.\n */\nexport const make = (\n options: MakeOptions\n): Effect.Effect<DurableBroker, never, Scope.Scope | RuntimeContext> =>\n Effect.gen(function* () {\n const runtime = yield* RuntimeContext;\n const hub = yield* Effect.acquireRelease(\n PubSub.dropping<ReadonlyArray<FanoutTopics.Topic>>({ capacity: 256 }),\n (pubsub) => PubSub.shutdown(pubsub)\n );\n const timeout = options.timeout ?? \"5 seconds\";\n\n /**\n * One bounded, fire-and-forget call: a failure (the stub's `RpcCallError`\n * included, which its types do not show) is dropped, since pull is the\n * correctness path; a call past the bound is logged and dropped.\n */\n const call = <A>(\n what: string,\n effect: Effect.Effect<A, never, RuntimeContext>,\n fallback: A\n ): Effect.Effect<A> =>\n effect.pipe(\n Effect.provideService(RuntimeContext, runtime),\n Effect.exit,\n Effect.map(Exit.match({ onFailure: () => fallback, onSuccess: (value) => value })),\n Effect.timeoutOrElse({\n duration: timeout,\n orElse: () =>\n Effect.as(\n Effect.logWarning(`DurableFanoutBroker: ${what} timed out; dropped`),\n fallback\n ),\n })\n );\n\n return {\n broker: {\n register: (roomId, topics, ttl) =>\n Effect.forEach(\n topics,\n (topic) =>\n call(\n `register to shard \"${topic}\"`,\n options.shards.getByName(topic).register(roomId, Duration.toMillis(ttl)),\n undefined\n ),\n { discard: true, concurrency: CONCURRENCY }\n ),\n publish: (topics) =>\n Effect.gen(function* () {\n // Each topic's shard answers its leased rooms; a room leased on\n // several of the topics is poked once, with all of them.\n const answers = yield* Effect.forEach(\n [...topics],\n (topic) =>\n call(\n `publish to shard \"${topic}\"`,\n options.shards.getByName(topic).publish(),\n noRooms\n ).pipe(Effect.map((rooms) => ({ topic, rooms }))),\n { concurrency: CONCURRENCY }\n );\n const matched = Arr.groupBy(\n Arr.flatMap(answers, ({ topic, rooms }) =>\n Arr.map(rooms, (roomId) => ({ roomId, topic }))\n ),\n ({ roomId }) => roomId\n );\n yield* Effect.forEach(\n R.toEntries(matched),\n ([roomId, matches]) =>\n call(\n `poke of room \"${roomId}\"`,\n options.poke(\n roomId,\n Arr.map(matches, ({ topic }) => topic)\n ),\n undefined\n ),\n { discard: true, concurrency: CONCURRENCY }\n );\n }),\n pokes: () =>\n Effect.gen(function* () {\n const subscription = yield* PubSub.subscribe(hub);\n return { take: PubSub.take(subscription) };\n }),\n },\n notify: (topics) => PubSub.publish(hub, topics).pipe(Effect.asVoid),\n };\n });\n","/**\n * EdgeInvalidation\n *\n * Server-side cache eviction on Cloudflare from the hints a committed write\n * publishes (#206): the `InvalidationSink`s for a Workers KV read-through cache\n * and for the Workers cache's tag purge, and the hop between the two Workers\n * that a typical deployment has.\n *\n * - {@link kv} deletes `{collection}:{id}` for every row a write touched.\n * - {@link cachePurge} purges the edge cache by tag, with the tag spelling of\n * `HttpCache` (`collection:{name}`, `collection:{name}:{id}`), the same one\n * the browser's Cache API invalidation uses: one convention, one place.\n * - {@link forward} and {@link receive} carry what a write touched from the\n * Worker that commits it to the Worker that holds the cache.\n *\n * ## Where the cache is\n *\n * `cache.purge({ tags })` purges only the calling Worker's own cache. In the\n * usual two-Worker shape the cache belongs to the front door (the SvelteKit\n * Worker that serves every page), while writes land in the API Worker behind\n * a service binding (`BindingTransport`). So the API Worker runs\n * `InvalidationSink.run(EdgeInvalidation.forward(target))` where its bus is,\n * and the front door runs the sinks: its RPC entrypoint passes each payload to\n * {@link receive}. Use an RPC method on a `WorkerEntrypoint` for the target,\n * not a `fetch` route: an RPC method is reachable only through the binding,\n * while a route on the front door's `fetch` is reachable from the internet,\n * and anyone could then purge its cache.\n *\n * ## Failure\n *\n * Fire-and-forget. A failed KV delete, a failed forward and a purge that\n * rejects are logged, and so is a purge that resolves `{ success: false }`:\n * `cache.purge` reports a Cloudflare-side failure that way rather than by\n * rejecting, so a `catch` alone never sees it. None of them fails or delays\n * the write, which has already committed by the time its hint is published.\n * The cache's TTL is the backstop for a lost eviction.\n */\n\nimport * as Arr from \"effect/Array\";\nimport * as Effect from \"effect/Effect\";\nimport * as Option from \"effect/Option\";\nimport * as Schema from \"effect/Schema\";\nimport { HttpCache, type InvalidationSink } from \"@thomasfosterau/legation/internal\";\n\n/** The part of a Workers KV namespace binding the KV sink uses. Structural, so a test can pass a fake. */\nexport interface KvNamespace {\n readonly delete: (key: string) => Promise<void>;\n}\n\n/**\n * The part of the Workers cache context the purge sink uses (`ctx.cache`).\n * Structural, so a test can pass a fake.\n */\nexport interface CachePurger {\n readonly purge: (options: {\n tags: Array<string>;\n }) => Promise<{ readonly success: boolean; readonly errors?: ReadonlyArray<unknown> }>;\n}\n\n/** The default KV key of a row: `{collection}:{id}`. */\nexport const kvKey = (collection: string, id: string): string => `${collection}:${id}`;\n\n/** Options for {@link kv}. */\nexport interface KvOptions {\n /** The key a row is cached under; defaults to {@link kvKey}. */\n readonly keyFor?: (collection: string, id: string) => string;\n /** How many deletes run at once. Default 8. */\n readonly concurrency?: number;\n}\n\n/**\n * A sink that deletes each touched row's entry from a KV read-through cache.\n * A failed delete is logged; the entry's TTL is the backstop.\n */\nexport const kv = (\n namespace: KvNamespace,\n options: KvOptions = {}\n): InvalidationSink.InvalidationSink => {\n const keyFor = options.keyFor ?? kvKey;\n return {\n invalidate: (touched) =>\n Effect.forEach(\n Arr.dedupe(touched.rows.map((row) => keyFor(row.collection, row.id))),\n (key) =>\n Effect.tryPromise(() => namespace.delete(key)).pipe(\n Effect.catch((error) =>\n Effect.logWarning(`EdgeInvalidation: KV delete of \"${key}\" failed`, error)\n )\n ),\n { concurrency: options.concurrency ?? 8, discard: true }\n ),\n };\n};\n\n/** Options for {@link cachePurge}. */\nexport interface CachePurgeOptions extends HttpCache.EvictionTagOptions {\n /**\n * The most tags one `purge` call carries; more are split across calls.\n * Default 30, a conservative bound; raise it if your plan allows more tags\n * per purge.\n */\n readonly maxTagsPerPurge?: number;\n}\n\n/**\n * A sink that purges the edge cache by tag: the coarse tag of every touched\n * collection and the fine tag of every touched row (`HttpCache.evictionTags`).\n * A purge that rejects, and one that resolves `{ success: false }`, is logged.\n */\nexport const cachePurge = (\n cache: CachePurger,\n options: CachePurgeOptions = {}\n): InvalidationSink.InvalidationSink => {\n const size = Math.max(1, options.maxTagsPerPurge ?? 30);\n return {\n invalidate: (touched) =>\n Effect.forEach(\n Arr.chunksOf(HttpCache.evictionTags(touched, options), size),\n (tags) =>\n Effect.tryPromise(() => cache.purge({ tags: [...tags] })).pipe(\n Effect.flatMap((result) =>\n result.success\n ? Effect.void\n : Effect.logWarning(\n \"EdgeInvalidation: cache purge reported failure\",\n result.errors ?? []\n )\n ),\n Effect.catch((error) =>\n Effect.logWarning(\"EdgeInvalidation: cache purge failed\", error)\n )\n ),\n { concurrency: 1, discard: true }\n ),\n };\n};\n\n/**\n * The Worker that holds the cache, as the committing Worker reaches it: an RPC\n * method on the front door's `WorkerEntrypoint`, through a service binding.\n * It takes {@link encode}'s payload and passes it to {@link receive}.\n */\nexport interface InvalidationTarget {\n readonly invalidate: (payload: string) => Promise<void>;\n}\n\n/** The wire form of what a write touched. */\nconst TouchedJson = Schema.fromJsonString(\n Schema.Struct({\n collections: Schema.Array(Schema.String),\n rows: Schema.Array(Schema.Struct({ collection: Schema.String, id: Schema.String })),\n })\n);\n\n/** Encode what a write touched for {@link InvalidationTarget.invalidate}. */\nexport const encode = (touched: HttpCache.Touched): string =>\n Schema.encodeSync(TouchedJson)({\n collections: [...touched.collections],\n rows: touched.rows.map((row) => ({ collection: row.collection, id: row.id })),\n });\n\n/**\n * A sink that forwards what a write touched to the Worker holding the cache.\n * A failed call is logged.\n */\nexport const forward = (target: InvalidationTarget): InvalidationSink.InvalidationSink => ({\n invalidate: (touched) =>\n Effect.tryPromise(() => target.invalidate(encode(touched))).pipe(\n Effect.catch((error) =>\n Effect.logWarning(\"EdgeInvalidation: forwarding an invalidation failed\", error)\n )\n ),\n});\n\n/**\n * The receiving end of {@link forward}: decode a payload and run `sink` on it.\n * A payload that does not decode is logged and dropped. Call it from the RPC\n * method the binding reaches.\n */\nexport const receive =\n (sink: InvalidationSink.InvalidationSink) =>\n (payload: string): Effect.Effect<void> =>\n Option.match(Schema.decodeUnknownOption(TouchedJson)(payload), {\n onNone: () => Effect.logWarning(\"EdgeInvalidation: dropped a payload that does not decode\"),\n onSome: (touched) => sink.invalidate(touched),\n });\n","/**\n * FanoutShard\n *\n * The Durable Object side of ADR-0023's broker, as an Alchemy Durable Object:\n * one shard instance per topic (the object is named BY the topic, so\n * cardinality is managed by the platform — cold topics cost nothing until\n * registered). The shard holds **leases** in its transactional storage:\n * `roomId → expiresAt` for its topic. `register` writes a lease; `publish`\n * answers the unexpired rooms, and expired leases are dropped as publishes pass\n * by — crashed rooms age out with no distributed cleanup.\n *\n * The publishing room pokes the rooms a shard answers\n * (`DurableFanoutBroker`), rather than the shard calling the rooms itself:\n * a shard that called rooms would need the room namespace, and the room needs\n * the shard namespace, which Alchemy's layers cannot provide to each other.\n * Answering the room ids keeps the shard free of any binding.\n *\n * The app declares the class and binds this implementation to it:\n *\n * ```ts\n * export class LegationShard extends Cloudflare.DurableObject<\n * LegationShard,\n * FanoutShard.ShardObject\n * >()(\"LegationShard\") {}\n *\n * export const LegationShardLive = LegationShard.make(FanoutShard.make());\n * ```\n */\n\nimport * as Workers from \"@alchemy.run/cloudflare/Workers\";\nimport type { RuntimeContext } from \"alchemy/RuntimeContext\";\nimport * as Arr from \"effect/Array\";\nimport * as Clock from \"effect/Clock\";\nimport * as Effect from \"effect/Effect\";\n\nconst LEASE_PREFIX = \"legation:lease:\";\n\n/** The shard's RPC surface, as the room-side broker calls it through a stub. */\nexport interface ShardObject {\n /** Lease (or renew) `roomId` on this shard's topic for `ttlMillis`. */\n readonly register: (\n roomId: string,\n ttlMillis: number\n ) => Effect.Effect<void, never, RuntimeContext>;\n /**\n * A publish to this shard's topic: the rooms whose lease is still live, to be\n * poked by the publisher. Expired leases are deleted as a side effect.\n */\n readonly publish: () => Effect.Effect<ReadonlyArray<string>, never, RuntimeContext>;\n}\n\n/** The lease operations, storage-backed (exported for direct testing). */\nexport const leases = {\n register: (\n storage: Workers.DurableObjectStorage,\n roomId: string,\n ttlMillis: number,\n now: number\n ): Effect.Effect<void, never, RuntimeContext> =>\n storage.put(`${LEASE_PREFIX}${roomId}`, now + ttlMillis),\n /** The unexpired room ids, in listing order; expired entries are deleted as a side effect. */\n live: (\n storage: Workers.DurableObjectStorage,\n now: number\n ): Effect.Effect<ReadonlyArray<string>, never, RuntimeContext> =>\n Effect.gen(function* () {\n const entries = [...(yield* storage.list<number>({ prefix: LEASE_PREFIX }))];\n const expired = entries.filter(([, expiresAt]) => expiresAt <= now).map(([key]) => key);\n if (Arr.isReadonlyArrayNonEmpty(expired)) yield* storage.delete(expired);\n return entries\n .filter(([, expiresAt]) => expiresAt > now)\n .map(([key]) => key.slice(LEASE_PREFIX.length));\n }),\n};\n\n/**\n * The shard's Durable Object implementation, for `LegationShard.make(...)`:\n * its state is resolved at init, its leases read and written per call.\n */\nexport const make = (): Effect.Effect<\n Effect.Effect<ShardObject>,\n never,\n Workers.DurableObjectState\n> =>\n Effect.gen(function* () {\n const state = yield* Workers.DurableObjectState;\n return Effect.succeed({\n register: (roomId, ttlMillis) =>\n Effect.flatMap(Clock.currentTimeMillis, (now) =>\n leases.register(state.storage, roomId, ttlMillis, now)\n ),\n publish: () =>\n Effect.flatMap(Clock.currentTimeMillis, (now) => leases.live(state.storage, now)),\n });\n });\n","/**\n * Room\n *\n * The per-user Durable Object of ADR-0022's reference topology, as an Alchemy\n * Durable Object: it runs the sync `Server` for one user — push/pull/read over\n * stateless HTTP (hibernation-proof), CVRs in the object's transactional\n * storage (co-located with the user), a `FanoutBus` over the topic shards for\n * cross-room fan-out, and WebSockets that carry exactly one thing: contentless\n * poke frames, sent when a publish matches the room's leases, so hibernated\n * sockets cost nothing between writes.\n *\n * The app declares the class (the shard class beside it) and binds\n * {@link make} to it, resolving the bindings the room needs at init:\n *\n * ```ts\n * export class LegationRoom extends Cloudflare.DurableObject<LegationRoom, Room.RoomObject>()(\n * \"LegationRoom\"\n * ) {}\n *\n * export const LegationRoomLive = LegationRoom.make(\n * Effect.gen(function* () {\n * const db = yield* Cloudflare.D1.QueryDatabase(AppDatabase);\n * return yield* Room.make({\n * nodes,\n * backend: Effect.map(D1Executor.make(db), (executor) => SqlBackend.make({ executor, ... })),\n * policy: Effect.succeed(Authorization.policy({ ... })),\n * shards: LegationShard,\n * });\n * })\n * );\n * ```\n *\n * The server is built once per in-memory instance, in the object's\n * per-instance phase (Alchemy runs it under `blockConcurrencyWhile` when the\n * object is constructed, a wake from hibernation included).\n */\n\nimport * as Workers from \"@alchemy.run/cloudflare/Workers\";\nimport type { HttpEffect } from \"alchemy/Http\";\nimport { RuntimeContext } from \"alchemy/RuntimeContext\";\nimport * as Clock from \"effect/Clock\";\nimport type * as Duration from \"effect/Duration\";\nimport * as Effect from \"effect/Effect\";\nimport * as HttpServerRequest from \"effect/http/HttpServerRequest\";\nimport * as HttpServerResponse from \"effect/http/HttpServerResponse\";\nimport * as Ref from \"effect/Ref\";\nimport * as Scope from \"effect/Scope\";\nimport {\n Authorization,\n Backend,\n Query,\n type Node,\n type ProtocolError,\n type TransportError,\n type VersionNotSupportedError,\n} from \"@thomasfosterau/legation\";\nimport {\n Cvr,\n FanoutBus,\n type FanoutTopics,\n InvalidationBus,\n type Subset,\n SyncServer,\n} from \"@thomasfosterau/legation/internal\";\nimport * as DurableCvrStore from \"./DurableCvrStore.js\";\nimport * as DurableFanoutBroker from \"./DurableFanoutBroker.js\";\nimport type * as FanoutShard from \"./FanoutShard.js\";\nimport {\n decodePullRequest,\n decodePushRequest,\n decodeReadRequest,\n pokeFrame,\n routeOf,\n routes,\n} from \"./RoomProtocol.js\";\n\n/** What the room's per-instance phase runs with: the object's state and runtime context. */\nexport type RoomServices = Workers.DurableObjectState | RuntimeContext;\n\n/** The room's surface: its HTTP routes, its poke RPC and its socket handlers. */\nexport interface RoomObject {\n /** The room's HTTP routes ({@link routes}): push, pull, read and the poke socket. */\n readonly fetch: HttpEffect<RoomServices>;\n /**\n * The RPC a publish calls when it matched this room's leases: the topics go\n * to the room's bus, and every socket gets a contentless poke frame.\n */\n readonly poke: (topics: ReadonlyArray<string>) => Effect.Effect<void, never, RuntimeContext>;\n /** The poke channel is one-way; inbound frames are ignored. */\n readonly webSocketMessage: () => Effect.Effect<void>;\n /** A closed poke socket needs no cleanup: the room keeps no per-socket state. */\n readonly webSocketClose: () => Effect.Effect<void>;\n}\n\n/** Configuration for {@link make}: the node graph, the backend, the auth policy and the shards. */\nexport interface RoomConfig<R = never> {\n readonly nodes: Node.NodeRegistry;\n /**\n * The authoritative storage contract (e.g. `SqlBackend` over\n * `D1Executor.make(db)`), built in the room's per-instance phase.\n */\n readonly backend: Effect.Effect<Backend.Backend, never, RoomServices>;\n /** Deny-by-default per-collection filters, built in the per-instance phase. */\n readonly policy: Effect.Effect<Authorization.AuthPolicy, never, RoomServices>;\n /** Resolve a client group to its identity (session binding). */\n readonly identity?:\n | Effect.Effect<Authorization.IdentityResolver, never, RoomServices>\n | undefined;\n /** Named-query resolution (server-authoritative subsets). */\n readonly queries?: Effect.Effect<Query.QueryResolver, never, RoomServices> | undefined;\n /** The topic-shard namespace: the app's shard class (`LegationShard`), yielded at init. */\n readonly shards: Effect.Effect<Workers.DurableObject<FanoutShard.ShardObject>, never, R>;\n /** Shard lease heartbeat (default 30 seconds; TTL is 3×). */\n readonly heartbeat?: Duration.Input | undefined;\n /**\n * Bound on each shard or room call the room makes and each lease\n * registration; a call that exceeds it is logged and dropped, so a hung\n * shard cannot hold a push's publish open. Default 5 seconds.\n */\n readonly publishTimeout?: Duration.Input | undefined;\n /**\n * The clock the room's boot, heartbeat and request handling run on, as a test seam.\n * Unset in production (the real clock); a test passes its `TestClock` so\n * `publishTimeout` and the heartbeat move with virtual time.\n */\n readonly clock?: Clock.Clock | undefined;\n}\n\n/** Run an effect on the room's clock: the configured one, or the real clock when none is set. */\nconst onRoomClock = <A, E, R>(\n clock: Clock.Clock | undefined,\n effect: Effect.Effect<A, E, R>\n): Effect.Effect<A, E, R> =>\n clock === undefined ? effect : effect.pipe(Effect.provideService(Clock.Clock, clock));\n\n/** What a protocol request can fail with: an undecodable body, or the handler's own errors. */\ntype AnswerError = ProtocolError | TransportError | VersionNotSupportedError;\n\n/** A failed protocol request, answered as a 400 carrying the error's message. */\nconst badRequest = (error: AnswerError) =>\n Effect.succeed(HttpServerResponse.jsonUnsafe({ error: error.message }, { status: 400 }));\n\n/** Answer one JSON protocol request with the matching handler operation. */\nconst answer = (handle: Effect.Effect<unknown, AnswerError>) =>\n handle.pipe(\n Effect.map((response) => HttpServerResponse.jsonUnsafe(response)),\n Effect.catchTags({\n ProtocolError: badRequest,\n TransportError: badRequest,\n VersionNotSupportedError: badRequest,\n })\n );\n\nconst notFound = HttpServerResponse.text(\"not found\", { status: 404 });\n\n/**\n * The room's Durable Object implementation, for `LegationRoom.make(...)`. Init\n * resolves the object's state, its own namespace (to poke sibling rooms) and\n * the shard namespace; the per-instance phase builds the sync server.\n */\nexport const make = <R>(\n config: RoomConfig<R>\n): Effect.Effect<\n Effect.Effect<RoomObject, never, RoomServices>,\n never,\n Workers.DurableObjectState | Workers.DurableObjectScope | R\n> =>\n Effect.gen(function* () {\n const state = yield* Workers.DurableObjectState;\n // SAFETY: this implementation is bound to the room class, so its own\n // namespace's objects are rooms with this `RoomObject`.\n const rooms = (yield* Workers.DurableObjectScope) as Workers.DurableObject<RoomObject>;\n const shards = yield* config.shards;\n\n return onRoomClock(\n config.clock,\n Effect.gen(function* () {\n // The scope lives as long as this in-memory instance; eviction tears\n // the whole isolate down, so no explicit close is needed.\n const scope = yield* Scope.make();\n const runtime = yield* RuntimeContext;\n const roomId = state.id.name ?? state.id.toString();\n\n /** Every poke socket gets one contentless frame. */\n const nudgeSockets = Effect.flatMap(state.getWebSockets(), (sockets) =>\n Effect.forEach(sockets, (socket) => socket.send(JSON.stringify(pokeFrame)), {\n concurrency: \"unbounded\",\n discard: true,\n })\n );\n\n return yield* Effect.gen(function* () {\n // Both bounds apply: FanoutBus wraps every broker call (lease registration included)\n // with `publishTimeout`, and the broker bounds each shard call with it too. The\n // broker's own 5 second default would cap any `publishTimeout` above 5 seconds, so the\n // forwarding here is what honours a longer bound.\n const durable: DurableFanoutBroker.DurableBroker = yield* DurableFanoutBroker.make({\n shards,\n // A publish that matched this room's own lease is delivered here, not over RPC.\n poke: (target, topics) =>\n target === roomId ? deliver(topics) : rooms.getByName(target).poke(topics),\n timeout: config.publishTimeout,\n });\n /** An inbound poke: the bus's matcher pokes the affected groups' pull loops. */\n const deliver = (topics: ReadonlyArray<FanoutTopics.Topic>) =>\n Effect.andThen(durable.notify(topics), nudgeSockets);\n // Late binding: the bus leases the room's windows, which the\n // handler's introspection provides once built.\n const subsetsRef = yield* Ref.make<Effect.Effect<ReadonlyArray<Subset.Subset>>>(\n Effect.succeed([])\n );\n const bus = yield* FanoutBus.make({\n roomId,\n broker: durable.broker,\n nodes: config.nodes,\n subsets: Ref.get(subsetsRef).pipe(Effect.flatten),\n heartbeat: config.heartbeat ?? \"30 seconds\",\n timeout: config.publishTimeout,\n });\n const cvrs = yield* DurableCvrStore.make(state.storage);\n let build = SyncServer.makeWithIntrospection.pipe(\n Effect.provideService(Backend.Backend, yield* config.backend),\n Effect.provideService(Authorization.AuthPolicy, yield* config.policy),\n Effect.provideService(Cvr.CvrStore, cvrs),\n Effect.provideService(InvalidationBus.InvalidationBus, bus)\n );\n if (config.identity !== undefined) {\n build = build.pipe(\n Effect.provideService(Authorization.IdentityResolver, yield* config.identity)\n );\n }\n if (config.queries !== undefined) {\n build = build.pipe(Effect.provideService(Query.QueryResolver, yield* config.queries));\n }\n const { handler, introspection } = yield* build;\n yield* Ref.set(\n subsetsRef,\n introspection.groups.pipe(\n Effect.map((groups) => groups.flatMap((group) => group.subsets))\n )\n );\n\n const fetch: RoomObject[\"fetch\"] = onRoomClock(\n config.clock,\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const route = routeOf(new URL(request.url, \"https://room\").pathname);\n if (route === routes.socket) {\n // The poke channel: accept under the hibernation API and hold\n // no per-socket state — eviction costs nothing.\n const [response] = yield* Workers.upgrade();\n return response;\n }\n if (route === routes.push) {\n const body = yield* request.json;\n return yield* answer(\n Effect.flatMap(decodePushRequest(body), (decoded) => handler.handlePush(decoded))\n );\n }\n if (route === routes.pull) {\n const body = yield* request.json;\n return yield* answer(\n Effect.flatMap(decodePullRequest(body), (decoded) => handler.handlePull(decoded))\n );\n }\n if (route === routes.read) {\n const body = yield* request.json;\n return yield* answer(\n Effect.flatMap(decodeReadRequest(body), (decoded) => handler.handleRead(decoded))\n );\n }\n return notFound;\n })\n );\n\n return {\n fetch,\n poke: (topics) =>\n onRoomClock(\n config.clock,\n deliver(topics).pipe(Effect.provideService(RuntimeContext, runtime))\n ),\n webSocketMessage: () => Effect.void,\n webSocketClose: () => Effect.void,\n } satisfies RoomObject;\n }).pipe(Scope.provide(scope));\n })\n );\n });\n","/**\n * RoomTransport\n *\n * The browser-side `ClientTransport` for the room topology: push, pull,\n * and fallthrough reads travel as stateless HTTP POSTs (each one wakes the\n * hibernated room, is answered, and lets it sleep again), while a single\n * WebSocket carries the room's contentless poke frames. The socket\n * reconnects with backoff and its state drives the connection status; sync\n * stays correct over HTTP alone (a dead socket only delays pokes — the\n * pull interval remains the safety net).\n *\n * Runs on standard `fetch`/`WebSocket` globals — no Workers types — so the\n * same transport serves browsers, service workers, and Node 22+.\n */\n\nimport * as Effect from \"effect/Effect\";\nimport * as Queue from \"effect/Queue\";\nimport * as Stream from \"effect/Stream\";\nimport {\n TransportError,\n type Transport,\n type VersionNotSupportedError,\n} from \"@thomasfosterau/legation\";\nimport type { Protocol } from \"@thomasfosterau/legation/internal\";\nimport { routes } from \"./RoomProtocol.js\";\n\n/** Options for {@link make}. */\nexport interface MakeOptions {\n /** The sync base URL, e.g. `https://app.example.com/sync`. */\n readonly url: string;\n /** Extra headers (e.g. the session token) attached to every request. */\n readonly headers?: Readonly<Record<string, string>>;\n /** Socket reconnect backoff cap in milliseconds. Default 10_000. */\n readonly maxReconnectDelayMillis?: number;\n}\n\nconst post =\n <A>(options: MakeOptions, route: string) =>\n (body: unknown): Effect.Effect<A, TransportError | VersionNotSupportedError> =>\n Effect.tryPromise({\n try: async () => {\n // oxlint-disable-next-line effect/avoid-native-fetch -- `make` returns a requirement-free ClientTransport (public API), and swapping in FetchHttpClient changes the wire request: it adds trace-propagation headers (a CORS preflight cross-origin) and aborts on interruption.\n const response = await fetch(`${options.url}${route}`, {\n method: \"POST\",\n body: JSON.stringify(body),\n headers: { \"content-type\": \"application/json\", ...options.headers },\n });\n if (!response.ok) {\n const detail = (await response.json().catch(() => ({}))) as { error?: string };\n throw new TransportError({\n message: detail.error ?? `room ${route} answered ${response.status}`,\n });\n }\n // Trusted JSON from this app's own room.\n return (await response.json()) as A;\n },\n catch: (cause) =>\n cause instanceof TransportError\n ? cause\n : new TransportError({ message: `room ${route} request failed`, cause }),\n });\n\n/**\n * Build the transport. The poke socket dials lazily on first subscription\n * and re-dials with linear backoff; each frame becomes one contentless\n * poke.\n */\nexport const make = (options: MakeOptions): Transport.ClientTransport => {\n const socketUrl = `${options.url.replace(/^http/, \"ws\")}${routes.socket}`;\n const pokes = Stream.callback<Protocol.Poke>((queue) =>\n Effect.acquireRelease(\n Effect.sync(() => {\n let socket: WebSocket | undefined;\n let closed = false;\n let attempt = 0;\n const dial = () => {\n if (closed) return;\n socket = new WebSocket(socketUrl);\n socket.onmessage = () => {\n attempt = 0;\n void Queue.offer(queue, {}).pipe(Effect.runPromise);\n };\n socket.onclose = () => {\n if (closed) return;\n attempt += 1;\n const delay = Math.min(1000 * attempt, options.maxReconnectDelayMillis ?? 10_000);\n setTimeout(dial, delay);\n };\n socket.onerror = () => socket?.close();\n };\n dial();\n return () => {\n closed = true;\n socket?.close();\n };\n }),\n (close) => Effect.sync(close)\n )\n );\n\n return {\n push: (request) => post<Protocol.PushResponse>(options, routes.push)(request),\n pull: (request) => post<Protocol.PullResponse>(options, routes.pull)(request),\n read: (request) => post<Protocol.ReadResponse>(options, routes.read)(request),\n pokes,\n // Correctness never depends on the socket; report the HTTP tier.\n status: Effect.succeed(\"connected\"),\n statusChanges: Stream.succeed(\"connected\"),\n };\n};\n","/**\n * Worker\n *\n * The stateless entry of the reference topology: a helper for an Alchemy\n * Worker's `fetch` that resolves the caller's identity to their room and\n * forwards the sync routes there, through the room namespace's stub\n * (`rooms.getByName(roomId).fetch(request)`, WebSocket upgrades included).\n * Everything else about the Worker — the REST/JSON:API baseline, auth, static\n * assets — is the app's; this helper claims only the sync base path and\n * answers `Option.none()` for everything else, so it composes inside any\n * `fetch`:\n *\n * ```ts\n * const rooms = yield* LegationRoom;\n * return {\n * fetch: Effect.gen(function* () {\n * const routed = yield* Worker.routeSync({ rooms, roomIdOf });\n * return Option.isSome(routed) ? routed.value : yield* app;\n * }),\n * };\n * ```\n *\n * The request reaches the room unchanged, base path included; the room\n * answers a route by the path's last segment (`RoomProtocol.routeOf`).\n */\n\nimport type * as Workers from \"@alchemy.run/cloudflare/Workers\";\nimport * as Effect from \"effect/Effect\";\nimport type { HttpServerError } from \"effect/http/HttpServerError\";\nimport * as HttpServerRequest from \"effect/http/HttpServerRequest\";\nimport * as HttpServerResponse from \"effect/http/HttpServerResponse\";\nimport * as Option from \"effect/Option\";\nimport type * as Room from \"./Room.js\";\nimport { routeOf } from \"./RoomProtocol.js\";\n\n/** Configuration for {@link routeSync}. */\nexport interface RouterConfig<R = never> {\n /** The room namespace (`yield* LegationRoom` at the Worker's init). */\n readonly rooms: Workers.DurableObject<Room.RoomObject>;\n /**\n * Resolve the caller's room id — the per-user identity, typically from\n * the verified session. `undefined` rejects the request with 401:\n * anonymous callers never reach a room.\n */\n readonly roomIdOf: (\n request: HttpServerRequest.HttpServerRequest\n ) => Effect.Effect<string | undefined, never, R>;\n /** The base path the sync routes hang off. Default `/sync`. */\n readonly basePath?: string | undefined;\n}\n\n/**\n * Route one request: `Option.none()` for a path outside the base, a 404 for an\n * unknown route under it, a 401 for an anonymous caller, otherwise the room's\n * own response.\n */\nexport const routeSync = <R = never>(\n config: RouterConfig<R>\n): Effect.Effect<\n Option.Option<HttpServerResponse.HttpServerResponse>,\n HttpServerError,\n HttpServerRequest.HttpServerRequest | R\n> =>\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const base = config.basePath ?? \"/sync\";\n const pathname = new URL(request.url, \"https://worker\").pathname;\n if (!pathname.startsWith(`${base}/`)) return Option.none();\n if (routeOf(pathname) === undefined || pathname.lastIndexOf(\"/\") !== base.length) {\n return Option.some(HttpServerResponse.text(\"not found\", { status: 404 }));\n }\n\n const roomId = yield* config.roomIdOf(request);\n if (roomId === undefined) {\n return Option.some(HttpServerResponse.text(\"unauthorized\", { status: 401 }));\n }\n return Option.some(yield* config.rooms.getByName(roomId).fetch(request));\n });\n"],"mappings":"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"}
@@ -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,14 +1,61 @@
1
1
  {
2
2
  "name": "@thomasfosterau/legation-alchemy-cloudflare",
3
- "version": "0.0.0",
4
- "description": "Placeholder reserved for npm trusted publishing; the first real release of @thomasfosterau/legation-alchemy-cloudflare is 0.1.0.",
3
+ "version": "0.1.0",
4
+ "description": "Cloudflare deployment of the legation sync engine on Alchemy's Effect-native Cloudflare provider: per-user Durable Object rooms, topic-sharded fan-out Durable Objects, D1 backend executor",
5
+ "keywords": [
6
+ "alchemy",
7
+ "cloudflare",
8
+ "d1",
9
+ "durable-objects",
10
+ "effect",
11
+ "sync-engine",
12
+ "typescript"
13
+ ],
14
+ "homepage": "https://github.com/thomasfosterau/legation/tree/trunk/packages/alchemy-cloudflare#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/thomasfosterau/legation/issues"
17
+ },
5
18
  "license": "MIT",
19
+ "author": "Thomas Foster",
6
20
  "repository": {
7
21
  "type": "git",
8
22
  "url": "git+https://github.com/thomasfosterau/legation.git",
9
23
  "directory": "packages/alchemy-cloudflare"
10
24
  },
25
+ "files": [
26
+ "dist",
27
+ "README.md",
28
+ "LICENSE",
29
+ "CHANGELOG.md"
30
+ ],
31
+ "type": "module",
32
+ "sideEffects": false,
33
+ "main": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js"
39
+ },
40
+ "./package.json": "./package.json"
41
+ },
11
42
  "publishConfig": {
12
43
  "access": "public"
44
+ },
45
+ "dependencies": {
46
+ "@thomasfosterau/legation": "^0.1.0"
47
+ },
48
+ "devDependencies": {
49
+ "@alchemy.run/cloudflare": "2.0.0-beta.85",
50
+ "@cloudflare/workers-types": "^5.20261104.1",
51
+ "@effect/vitest": "4.1.2",
52
+ "alchemy": "2.0.0-beta.85",
53
+ "effect": "4.1.2",
54
+ "vitest": "5.0.5"
55
+ },
56
+ "peerDependencies": {
57
+ "@alchemy.run/cloudflare": "2.0.0-beta.85",
58
+ "alchemy": "2.0.0-beta.85",
59
+ "effect": "^4.1.0"
13
60
  }
14
- }
61
+ }