@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,329 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { Authorization, Backend, Node, ProtocolError, Query, Transport } from "@thomasfosterau/legation";
3
+ import { Cvr, FanoutBroker, FanoutTopics, HttpCache, InvalidationSink, Protocol } from "@thomasfosterau/legation/internal";
4
+ import { RuntimeContext } from "alchemy/RuntimeContext";
5
+ import * as Duration from "effect/Duration";
6
+ import * as Option from "effect/Option";
7
+ import * as Workers from "@alchemy.run/cloudflare/Workers";
8
+ import { DurableObjectStorage } from "@alchemy.run/cloudflare/Workers";
9
+ import * as Clock from "effect/Clock";
10
+ import * as HttpServerRequest from "effect/http/HttpServerRequest";
11
+ import * as HttpServerResponse from "effect/http/HttpServerResponse";
12
+ import * as Scope from "effect/Scope";
13
+ import { QueryDatabaseClient } from "@alchemy.run/cloudflare/D1";
14
+ import { SqlBackend } from "@thomasfosterau/legation/sql";
15
+ import { HttpEffect } from "alchemy/Http";
16
+ import { HttpServerError } from "effect/http/HttpServerError";
17
+ declare namespace BindingTransport_d_exports {
18
+ export { InboundRequest, MakeOptions$2 as MakeOptions, ServiceBinding, make$6 as make };
19
+ }
20
+ /**
21
+ * The object of a Cloudflare service binding (`env.API`). Declared structurally
22
+ * so the transport needs no Workers types and a test can pass a plain
23
+ * fetch-alike.
24
+ */
25
+ interface ServiceBinding {
26
+ readonly fetch: (request: Request) => Promise<Response>;
27
+ }
28
+ /**
29
+ * The inbound request being rendered — its cookie and origin are forwarded,
30
+ * and a relative {@link MakeOptions.path} resolves against its URL. A standard
31
+ * `Request` (SvelteKit's `event.request`) satisfies this structurally.
32
+ */
33
+ interface InboundRequest {
34
+ readonly url: string;
35
+ readonly headers: {
36
+ readonly get: (name: string) => string | null;
37
+ };
38
+ }
39
+ /** Options for {@link make}. */
40
+ interface MakeOptions$2 {
41
+ /** The service binding to the Worker running the sync server. */
42
+ readonly binding: ServiceBinding;
43
+ /** The inbound SSR request whose cookie and origin are forwarded. */
44
+ readonly request: InboundRequest;
45
+ /**
46
+ * The sync base the bound Worker serves. A path (default `/sync`) resolves
47
+ * against the inbound request's origin; an absolute URL overrides it.
48
+ */
49
+ readonly path?: string;
50
+ /** Extra headers attached to every call (e.g. an internal service token). */
51
+ readonly headers?: Readonly<Record<string, string>>;
52
+ }
53
+ /**
54
+ * Build the transport. Each call is one `binding.fetch` of a JSON POST —
55
+ * no network hop, no public route on the bound Worker.
56
+ */
57
+ declare const make$6: (options: MakeOptions$2) => Transport.ClientTransport;
58
+ declare namespace D1Executor_d_exports {
59
+ export { make$5 as make };
60
+ }
61
+ /**
62
+ * Build the executor over a bound D1 client. Needs the `RuntimeContext` of the
63
+ * Worker or Durable Object it runs in (the inner, per-instance phase of an
64
+ * Alchemy Durable Object has it).
65
+ */
66
+ declare const make$5: (db: QueryDatabaseClient) => Effect.Effect<SqlBackend.SqlExecutor, never, RuntimeContext>;
67
+ declare namespace DurableCvrStore_d_exports {
68
+ export { make$4 as make };
69
+ }
70
+ /** Build the store over the room's `state.storage`, in the room's runtime context. */
71
+ declare const make$4: (storage: DurableObjectStorage) => Effect.Effect<Cvr.CvrStore, never, RuntimeContext>;
72
+ declare namespace FanoutShard_d_exports {
73
+ export { ShardObject, leases, make$3 as make };
74
+ }
75
+ /** The shard's RPC surface, as the room-side broker calls it through a stub. */
76
+ interface ShardObject {
77
+ /** Lease (or renew) `roomId` on this shard's topic for `ttlMillis`. */
78
+ readonly register: (roomId: string, ttlMillis: number) => Effect.Effect<void, never, RuntimeContext>;
79
+ /**
80
+ * A publish to this shard's topic: the rooms whose lease is still live, to be
81
+ * poked by the publisher. Expired leases are deleted as a side effect.
82
+ */
83
+ readonly publish: () => Effect.Effect<ReadonlyArray<string>, never, RuntimeContext>;
84
+ }
85
+ /** The lease operations, storage-backed (exported for direct testing). */
86
+ declare const leases: {
87
+ register: (storage: Workers.DurableObjectStorage, roomId: string, ttlMillis: number, now: number) => Effect.Effect<void, never, RuntimeContext>;
88
+ /** The unexpired room ids, in listing order; expired entries are deleted as a side effect. */
89
+ live: (storage: Workers.DurableObjectStorage, now: number) => Effect.Effect<ReadonlyArray<string>, never, RuntimeContext>;
90
+ };
91
+ /**
92
+ * The shard's Durable Object implementation, for `LegationShard.make(...)`:
93
+ * its state is resolved at init, its leases read and written per call.
94
+ */
95
+ declare const make$3: () => Effect.Effect<Effect.Effect<ShardObject>, never, Workers.DurableObjectState>;
96
+ declare namespace DurableFanoutBroker_d_exports {
97
+ export { DurableBroker, MakeOptions$1 as MakeOptions, make$2 as make };
98
+ }
99
+ /** Options for {@link make}. */
100
+ interface MakeOptions$1 {
101
+ /** The shard namespace (`yield* LegationShard` at the room's init). */
102
+ readonly shards: Workers.DurableObject<ShardObject>;
103
+ /**
104
+ * Poke one room with the topics of a publish that matched its leases. The
105
+ * room passes its own namespace's `poke` RPC (and delivers its own pokes
106
+ * locally).
107
+ */
108
+ readonly poke: (roomId: string, topics: ReadonlyArray<FanoutTopics.Topic>) => Effect.Effect<void, never, RuntimeContext>;
109
+ /**
110
+ * Bound on one shard or room call. A call that exceeds it is logged and
111
+ * dropped, so a hung shard cannot hold the publish that awaits it. Default 5
112
+ * seconds.
113
+ */
114
+ readonly timeout?: Duration.Input | undefined;
115
+ }
116
+ /** The broker plus the room's poke inlet (wire it to the room's `poke` RPC). */
117
+ interface DurableBroker {
118
+ readonly broker: FanoutBroker.FanoutBroker;
119
+ /** Feed an inbound poke into the broker's subscribers. */
120
+ readonly notify: (topics: ReadonlyArray<FanoutTopics.Topic>) => Effect.Effect<void>;
121
+ }
122
+ /**
123
+ * Build the room's {@link DurableBroker} over the shard namespace; its poke feed
124
+ * lives as long as the scope. Needs the room's `RuntimeContext`, which it
125
+ * captures for its calls.
126
+ */
127
+ declare const make$2: (options: MakeOptions$1) => Effect.Effect<DurableBroker, never, Scope.Scope | RuntimeContext>;
128
+ declare namespace EdgeInvalidation_d_exports {
129
+ export { CachePurgeOptions, CachePurger, InvalidationTarget, KvNamespace, KvOptions, cachePurge, encode, forward, kv, kvKey, receive };
130
+ }
131
+ /** The part of a Workers KV namespace binding the KV sink uses. Structural, so a test can pass a fake. */
132
+ interface KvNamespace {
133
+ readonly delete: (key: string) => Promise<void>;
134
+ }
135
+ /**
136
+ * The part of the Workers cache context the purge sink uses (`ctx.cache`).
137
+ * Structural, so a test can pass a fake.
138
+ */
139
+ interface CachePurger {
140
+ readonly purge: (options: {
141
+ tags: Array<string>;
142
+ }) => Promise<{
143
+ readonly success: boolean;
144
+ readonly errors?: ReadonlyArray<unknown>;
145
+ }>;
146
+ }
147
+ /** The default KV key of a row: `{collection}:{id}`. */
148
+ declare const kvKey: (collection: string, id: string) => string;
149
+ /** Options for {@link kv}. */
150
+ interface KvOptions {
151
+ /** The key a row is cached under; defaults to {@link kvKey}. */
152
+ readonly keyFor?: (collection: string, id: string) => string;
153
+ /** How many deletes run at once. Default 8. */
154
+ readonly concurrency?: number;
155
+ }
156
+ /**
157
+ * A sink that deletes each touched row's entry from a KV read-through cache.
158
+ * A failed delete is logged; the entry's TTL is the backstop.
159
+ */
160
+ declare const kv: (namespace: KvNamespace, options?: KvOptions) => InvalidationSink.InvalidationSink;
161
+ /** Options for {@link cachePurge}. */
162
+ interface CachePurgeOptions extends HttpCache.EvictionTagOptions {
163
+ /**
164
+ * The most tags one `purge` call carries; more are split across calls.
165
+ * Default 30, a conservative bound; raise it if your plan allows more tags
166
+ * per purge.
167
+ */
168
+ readonly maxTagsPerPurge?: number;
169
+ }
170
+ /**
171
+ * A sink that purges the edge cache by tag: the coarse tag of every touched
172
+ * collection and the fine tag of every touched row (`HttpCache.evictionTags`).
173
+ * A purge that rejects, and one that resolves `{ success: false }`, is logged.
174
+ */
175
+ declare const cachePurge: (cache: CachePurger, options?: CachePurgeOptions) => InvalidationSink.InvalidationSink;
176
+ /**
177
+ * The Worker that holds the cache, as the committing Worker reaches it: an RPC
178
+ * method on the front door's `WorkerEntrypoint`, through a service binding.
179
+ * It takes {@link encode}'s payload and passes it to {@link receive}.
180
+ */
181
+ interface InvalidationTarget {
182
+ readonly invalidate: (payload: string) => Promise<void>;
183
+ }
184
+ /** Encode what a write touched for {@link InvalidationTarget.invalidate}. */
185
+ declare const encode: (touched: HttpCache.Touched) => string;
186
+ /**
187
+ * A sink that forwards what a write touched to the Worker holding the cache.
188
+ * A failed call is logged.
189
+ */
190
+ declare const forward: (target: InvalidationTarget) => InvalidationSink.InvalidationSink;
191
+ /**
192
+ * The receiving end of {@link forward}: decode a payload and run `sink` on it.
193
+ * A payload that does not decode is logged and dropped. Call it from the RPC
194
+ * method the binding reaches.
195
+ */
196
+ declare const receive: (sink: InvalidationSink.InvalidationSink) => (payload: string) => Effect.Effect<void>;
197
+ declare namespace Room_d_exports {
198
+ export { RoomConfig, RoomObject, RoomServices, make$1 as make };
199
+ }
200
+ /** What the room's per-instance phase runs with: the object's state and runtime context. */
201
+ type RoomServices = Workers.DurableObjectState | RuntimeContext;
202
+ /** The room's surface: its HTTP routes, its poke RPC and its socket handlers. */
203
+ interface RoomObject {
204
+ /** The room's HTTP routes ({@link routes}): push, pull, read and the poke socket. */
205
+ readonly fetch: HttpEffect<RoomServices>;
206
+ /**
207
+ * The RPC a publish calls when it matched this room's leases: the topics go
208
+ * to the room's bus, and every socket gets a contentless poke frame.
209
+ */
210
+ readonly poke: (topics: ReadonlyArray<string>) => Effect.Effect<void, never, RuntimeContext>;
211
+ /** The poke channel is one-way; inbound frames are ignored. */
212
+ readonly webSocketMessage: () => Effect.Effect<void>;
213
+ /** A closed poke socket needs no cleanup: the room keeps no per-socket state. */
214
+ readonly webSocketClose: () => Effect.Effect<void>;
215
+ }
216
+ /** Configuration for {@link make}: the node graph, the backend, the auth policy and the shards. */
217
+ interface RoomConfig<R = never> {
218
+ readonly nodes: Node.NodeRegistry;
219
+ /**
220
+ * The authoritative storage contract (e.g. `SqlBackend` over
221
+ * `D1Executor.make(db)`), built in the room's per-instance phase.
222
+ */
223
+ readonly backend: Effect.Effect<Backend.Backend, never, RoomServices>;
224
+ /** Deny-by-default per-collection filters, built in the per-instance phase. */
225
+ readonly policy: Effect.Effect<Authorization.AuthPolicy, never, RoomServices>;
226
+ /** Resolve a client group to its identity (session binding). */
227
+ readonly identity?: Effect.Effect<Authorization.IdentityResolver, never, RoomServices> | undefined;
228
+ /** Named-query resolution (server-authoritative subsets). */
229
+ readonly queries?: Effect.Effect<Query.QueryResolver, never, RoomServices> | undefined;
230
+ /** The topic-shard namespace: the app's shard class (`LegationShard`), yielded at init. */
231
+ readonly shards: Effect.Effect<Workers.DurableObject<ShardObject>, never, R>;
232
+ /** Shard lease heartbeat (default 30 seconds; TTL is 3×). */
233
+ readonly heartbeat?: Duration.Input | undefined;
234
+ /**
235
+ * Bound on each shard or room call the room makes and each lease
236
+ * registration; a call that exceeds it is logged and dropped, so a hung
237
+ * shard cannot hold a push's publish open. Default 5 seconds.
238
+ */
239
+ readonly publishTimeout?: Duration.Input | undefined;
240
+ /**
241
+ * The clock the room's boot, heartbeat and request handling run on, as a test seam.
242
+ * Unset in production (the real clock); a test passes its `TestClock` so
243
+ * `publishTimeout` and the heartbeat move with virtual time.
244
+ */
245
+ readonly clock?: Clock.Clock | undefined;
246
+ }
247
+ /**
248
+ * The room's Durable Object implementation, for `LegationRoom.make(...)`. Init
249
+ * resolves the object's state, its own namespace (to poke sibling rooms) and
250
+ * the shard namespace; the per-instance phase builds the sync server.
251
+ */
252
+ declare const make$1: <R>(config: RoomConfig<R>) => Effect.Effect<Effect.Effect<RoomObject, never, RoomServices>, never, Workers.DurableObjectState | Workers.DurableObjectScope | R>;
253
+ declare namespace RoomProtocol_d_exports {
254
+ export { PokeFrame, Route, decodePullRequest, decodePushRequest, decodeReadRequest, pokeFrame, routeOf, routes };
255
+ }
256
+ type Decoder<A> = (input: unknown) => Effect.Effect<A, ProtocolError>;
257
+ /**
258
+ * The room's HTTP routes. A room answers a route by the request path's last
259
+ * segment, so a request forwarded with its base path intact (`/sync/push`)
260
+ * reaches the same route as `/push`.
261
+ */
262
+ declare const routes: {
263
+ readonly push: "/push";
264
+ readonly pull: "/pull";
265
+ readonly read: "/read";
266
+ /** WebSocket upgrade: the poke channel. */
267
+ readonly socket: "/socket";
268
+ };
269
+ /** One of the room's {@link routes}. */
270
+ type Route = (typeof routes)[keyof typeof routes];
271
+ /**
272
+ * The route a request path addresses: its last segment, when that names one of
273
+ * the {@link routes}.
274
+ */
275
+ declare const routeOf: (pathname: string) => Route | undefined;
276
+ /** The single frame a room socket ever sends. */
277
+ interface PokeFrame {
278
+ readonly type: "poke";
279
+ }
280
+ /** The single poke frame a room socket sends, as a value to serialize. */
281
+ declare const pokeFrame: PokeFrame;
282
+ /** Re-exported request decoders — the room's HTTP bodies are the protocol's. */
283
+ declare const decodePushRequest: Decoder<Protocol.PushRequest>;
284
+ /** The protocol's pull-request decoder. */
285
+ declare const decodePullRequest: Decoder<Protocol.PullRequest>;
286
+ /** The protocol's read-request decoder. */
287
+ declare const decodeReadRequest: Decoder<Protocol.ReadRequest>;
288
+ declare namespace RoomTransport_d_exports {
289
+ export { MakeOptions, make };
290
+ }
291
+ /** Options for {@link make}. */
292
+ interface MakeOptions {
293
+ /** The sync base URL, e.g. `https://app.example.com/sync`. */
294
+ readonly url: string;
295
+ /** Extra headers (e.g. the session token) attached to every request. */
296
+ readonly headers?: Readonly<Record<string, string>>;
297
+ /** Socket reconnect backoff cap in milliseconds. Default 10_000. */
298
+ readonly maxReconnectDelayMillis?: number;
299
+ }
300
+ /**
301
+ * Build the transport. The poke socket dials lazily on first subscription
302
+ * and re-dials with linear backoff; each frame becomes one contentless
303
+ * poke.
304
+ */
305
+ declare const make: (options: MakeOptions) => Transport.ClientTransport;
306
+ declare namespace Worker_d_exports {
307
+ export { RouterConfig, routeSync };
308
+ }
309
+ /** Configuration for {@link routeSync}. */
310
+ interface RouterConfig<R = never> {
311
+ /** The room namespace (`yield* LegationRoom` at the Worker's init). */
312
+ readonly rooms: Workers.DurableObject<RoomObject>;
313
+ /**
314
+ * Resolve the caller's room id — the per-user identity, typically from
315
+ * the verified session. `undefined` rejects the request with 401:
316
+ * anonymous callers never reach a room.
317
+ */
318
+ readonly roomIdOf: (request: HttpServerRequest.HttpServerRequest) => Effect.Effect<string | undefined, never, R>;
319
+ /** The base path the sync routes hang off. Default `/sync`. */
320
+ readonly basePath?: string | undefined;
321
+ }
322
+ /**
323
+ * Route one request: `Option.none()` for a path outside the base, a 404 for an
324
+ * unknown route under it, a 401 for an anonymous caller, otherwise the room's
325
+ * own response.
326
+ */
327
+ declare const routeSync: <R = never>(config: RouterConfig<R>) => Effect.Effect<Option.Option<HttpServerResponse.HttpServerResponse>, HttpServerError, HttpServerRequest.HttpServerRequest | R>;
328
+ export { BindingTransport_d_exports as BindingTransport, D1Executor_d_exports as D1Executor, DurableCvrStore_d_exports as DurableCvrStore, DurableFanoutBroker_d_exports as DurableFanoutBroker, EdgeInvalidation_d_exports as EdgeInvalidation, FanoutShard_d_exports as FanoutShard, Room_d_exports as Room, RoomProtocol_d_exports as RoomProtocol, RoomTransport_d_exports as RoomTransport, Worker_d_exports as Worker };
329
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":["MakeOptions","make","make","make","make","MakeOptions","FanoutShard.ShardObject","make","FanoutShard.ShardObject","make","Room.RoomObject"],"sources":["../src/BindingTransport.ts","../src/D1Executor.ts","../src/DurableCvrStore.ts","../src/FanoutShard.ts","../src/DurableFanoutBroker.ts","../src/EdgeInvalidation.ts","../src/Room.ts","../src/RoomProtocol.ts","../src/RoomTransport.ts","../src/Worker.ts"],"mappings":"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"}