@streamsy/serve 0.0.0-tegami-trusted-publish-setup → 0.5.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/dist/index.js ADDED
@@ -0,0 +1,335 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.js";
2
+ import { STATE_CONTRACT_HEADER, STATE_VERSION_HEADER, STREAM_CONTRACT_HEADER, STREAM_VERSION_HEADER, VALUE_CONTRACT_HEADER, canonicalJson, contractFingerprint, valueEtag } from "./contract.js";
3
+ import { Array, Cause, Duration, Effect, Exit, Layer, Order, Record, Runtime, Schema } from "effect";
4
+ import { HttpRouter, HttpServerRequest, HttpServerRespondable, HttpServerResponse } from "effect/http";
5
+ import { Http, Offset } from "@streamsy/core";
6
+ import { StreamsReader } from "@streamsy/core/protocol";
7
+ import { ShutdownSignal, layerShutdownSignal } from "@streamsy/core/http";
8
+ // src/errors.ts
9
+ /** The public error body. `cases` gives one validating constructor per tag. */
10
+ const PublicErrorSchema = Schema.TaggedUnion({
11
+ InvalidParams: {
12
+ route: Schema.String,
13
+ parameter: Schema.String,
14
+ detail: Schema.String
15
+ },
16
+ ProtocolVersionUnsupported: {
17
+ route: Schema.String,
18
+ supported: Schema.Literal(1),
19
+ received: Schema.String,
20
+ recovery: Schema.Literal("replay-from-start")
21
+ },
22
+ ResumeRejected: {
23
+ route: Schema.String,
24
+ reason: Schema.Literals([
25
+ "invalid-offset",
26
+ "history-unavailable",
27
+ "contract-changed"
28
+ ]),
29
+ recovery: Schema.Literal("replay-from-start")
30
+ },
31
+ ContractChanged: {
32
+ route: Schema.String,
33
+ recovery: Schema.Literal("refetch")
34
+ },
35
+ TransportUnavailable: {
36
+ route: Schema.String,
37
+ detail: Schema.String
38
+ },
39
+ WireEncodeFailed: {
40
+ route: Schema.String,
41
+ detail: Schema.String
42
+ }
43
+ });
44
+ var InvalidParams = class extends Schema.TaggedError()("InvalidParams", PublicErrorSchema.cases.InvalidParams.fields, { httpApiStatus: 400 }) {
45
+ responseHeaders = {};
46
+ [HttpServerRespondable.symbol]() {
47
+ return Effect.succeed(errorResponse(this, 400, this.responseHeaders));
48
+ }
49
+ };
50
+ var ProtocolVersionUnsupported = class extends Schema.TaggedError()("ProtocolVersionUnsupported", PublicErrorSchema.cases.ProtocolVersionUnsupported.fields, { httpApiStatus: 409 }) {
51
+ responseHeaders = {};
52
+ [HttpServerRespondable.symbol]() {
53
+ return Effect.succeed(errorResponse(this, 409, this.responseHeaders));
54
+ }
55
+ };
56
+ var ResumeRejected = class extends Schema.TaggedError()("ResumeRejected", PublicErrorSchema.cases.ResumeRejected.fields, { httpApiStatus: 409 }) {
57
+ responseHeaders = {};
58
+ [HttpServerRespondable.symbol]() {
59
+ return Effect.succeed(errorResponse(this, 409, this.responseHeaders));
60
+ }
61
+ };
62
+ var ContractChanged = class extends Schema.TaggedError()("ContractChanged", PublicErrorSchema.cases.ContractChanged.fields, { httpApiStatus: 409 }) {
63
+ responseHeaders = {};
64
+ [HttpServerRespondable.symbol]() {
65
+ return Effect.succeed(errorResponse(this, 409, this.responseHeaders));
66
+ }
67
+ };
68
+ var TransportUnavailable = class extends Schema.TaggedError()("TransportUnavailable", PublicErrorSchema.cases.TransportUnavailable.fields, { httpApiStatus: 503 }) {
69
+ responseHeaders = {};
70
+ [HttpServerRespondable.symbol]() {
71
+ return Effect.succeed(errorResponse(this, 503, this.responseHeaders));
72
+ }
73
+ };
74
+ var WireEncodeFailed = class extends Schema.TaggedError()("WireEncodeFailed", PublicErrorSchema.cases.WireEncodeFailed.fields, { httpApiStatus: 500 }) {
75
+ responseHeaders = {};
76
+ [HttpServerRespondable.symbol]() {
77
+ return Effect.succeed(errorResponse(this, 500, this.responseHeaders));
78
+ }
79
+ };
80
+ /** Response metadata stays out of the public JSON body. */
81
+ function withHeaders(error, headers) {
82
+ error.responseHeaders = headers;
83
+ return error;
84
+ }
85
+ function errorResponse(error, status, headers) {
86
+ return HttpServerResponse.jsonUnsafe(Schema.encodeSync(PublicErrorSchema)(error), {
87
+ status,
88
+ headers: {
89
+ ...Http.securityHeaders,
90
+ ...headers,
91
+ "cache-control": "no-store"
92
+ }
93
+ });
94
+ }
95
+ // src/host.ts
96
+ const drain = (timeout = Duration.seconds(2)) => (serve) => {
97
+ return Layer.effectDiscard(Effect.gen(function* () {
98
+ const signal = yield* ShutdownSignal;
99
+ yield* Effect.addFinalizer(() => signal.drain(timeout));
100
+ })).pipe(Layer.provideMerge(serve), Layer.provide(layerShutdownSignal));
101
+ };
102
+ /**
103
+ * `HttpRouter.serve` with the Streamsy drain applied in the only correct place.
104
+ *
105
+ * Before the serve layer closes, the drain signals every live read: a parked
106
+ * long poll answers `204` with its offset and cursor, and an open SSE body ends
107
+ * after its last whole batch. It then waits until no live read is running, or
108
+ * `drainTimeout` passes. After that the serve layer interrupts any handler that
109
+ * is still running.
110
+ *
111
+ * The host provides storage, the listener and the Effect main:
112
+ *
113
+ * ```ts
114
+ * const Server = Serve.layer(routes).pipe(
115
+ * Layer.provide(MemoryStorage.layer(streams)),
116
+ * Layer.provide(listener()),
117
+ * );
118
+ * BunRuntime.runMain(Layer.launch(Server), { teardown: Serve.teardown });
119
+ * ```
120
+ */
121
+ const layer = (routes, options) => HttpRouter.serve(routes, options).pipe(drain(options?.drainTimeout));
122
+ /**
123
+ * Exit policy for a long-running Streamsy host after `Serve.layer` drains.
124
+ * Pass it to the runtime's `runMain` as `{ teardown: Serve.teardown }`.
125
+ *
126
+ * An exit that holds only interrupts maps to 0: after the drain, an interrupt
127
+ * from `SIGINT` or `SIGTERM` is the normal way for the server to stop. Every
128
+ * other exit uses `Runtime.defaultTeardown`, so a startup failure stays
129
+ * non-zero. Any interrupt-only exit maps to 0, including one that no signal
130
+ * caused. In a `Layer.launch` main the fiber ends only on failure or a signal.
131
+ */
132
+ const teardown = (exit, onExit) => Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause) ? onExit(0) : Runtime.defaultTeardown(exit, onExit);
133
+ // src/serve.ts
134
+ var serve_exports = /* @__PURE__ */ __exportAll({
135
+ ContractChanged: () => ContractChanged,
136
+ InvalidParams: () => InvalidParams,
137
+ ProtocolVersionUnsupported: () => ProtocolVersionUnsupported,
138
+ ResumeRejected: () => ResumeRejected,
139
+ TransportUnavailable: () => TransportUnavailable,
140
+ WireEncodeFailed: () => WireEncodeFailed,
141
+ layer: () => layer,
142
+ mount: () => mount,
143
+ params: () => params,
144
+ route: () => route,
145
+ state: () => state,
146
+ stream: () => stream,
147
+ teardown: () => teardown,
148
+ value: () => value
149
+ });
150
+ /** Decode the matched route's parameters with the stream's codecs. */
151
+ const params = Effect.fn("Serve.params")(function* (declaration) {
152
+ const { route: matchedRoute } = yield* HttpRouter.RouteContext;
153
+ return yield* HttpRouter.schemaPathParams(declaration.paramSchema).pipe(Effect.mapError(() => new InvalidParams({
154
+ route: matchedRoute.path,
155
+ parameter: "params",
156
+ detail: "Invalid path parameters"
157
+ })));
158
+ });
159
+ /** Own every method at this path, including beneath protocol wildcard routes. */
160
+ function route(path, handler) {
161
+ return HttpRouter.add("*", path, Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => request.method === "GET" || request.method === "HEAD" ? handler : Effect.succeed(HttpServerResponse.empty({
162
+ status: 405,
163
+ headers: {
164
+ ...Http.securityHeaders,
165
+ allow: "GET, HEAD"
166
+ }
167
+ }))));
168
+ }
169
+ /** Mount one declared stream; state addresses select the Durable State contract. */
170
+ function mount(declaration, path, options) {
171
+ return route(path, Effect.gen(function* () {
172
+ const p = yield* params(declaration);
173
+ const address = yield* Effect.try({
174
+ try: () => declaration.at(p),
175
+ catch: () => new InvalidParams({
176
+ route: path,
177
+ parameter: "params",
178
+ detail: "Parameters cannot form a canonical stream id"
179
+ })
180
+ });
181
+ return yield* read(address, options ?? {}, "collections" in address ? "state" : "stream", "collections" in address ? collectionDescriptor(address.collections) : Effect.succeed(null));
182
+ }));
183
+ }
184
+ /** Durable Streams response for one resolved address. */
185
+ function stream(address, options = {}) {
186
+ return read(address, options, "stream", Effect.succeed(null));
187
+ }
188
+ /** Durable State response with replay-from-start recovery. Stored controls pass through. */
189
+ function state(address, options = {}) {
190
+ return read(address, options, "state", collectionDescriptor(address.collections));
191
+ }
192
+ function collectionDescriptor(value) {
193
+ return Effect.gen(function* () {
194
+ const { route: matchedRoute } = yield* HttpRouter.RouteContext;
195
+ const collections = yield* Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Struct({ key: Schema.Union([Schema.String, Schema.Finite]) })))(value).pipe(Effect.mapError(() => new WireEncodeFailed({
196
+ route: matchedRoute.path,
197
+ detail: "Invalid collection metadata"
198
+ })));
199
+ return Record.fromEntries(Array.map(Record.toEntries(collections), ([type, collection]) => [type, collection.key]));
200
+ });
201
+ }
202
+ const read = Effect.fn("Serve.read")(function* (address, options, kind, descriptor) {
203
+ const request = yield* HttpServerRequest.HttpServerRequest;
204
+ const { route: matchedRoute } = yield* HttpRouter.RouteContext;
205
+ const path = matchedRoute.path;
206
+ const versionHeader = kind === "state" ? STATE_VERSION_HEADER : STREAM_VERSION_HEADER;
207
+ const contractHeader = kind === "state" ? STATE_CONTRACT_HEADER : STREAM_CONTRACT_HEADER;
208
+ const source = "template" in address.stream && Schema.is(Schema.String)(address.stream.template) ? address.stream.template : "";
209
+ const collections = yield* descriptor;
210
+ const fingerprint = contractFingerprint({
211
+ kind,
212
+ route: path,
213
+ source,
214
+ id: address.id,
215
+ collections,
216
+ params: Array.sort(Array.map(Array.filter(path.split("/"), (segment) => segment.startsWith(":")), (segment) => segment.slice(1)), Order.String),
217
+ version: 1,
218
+ recovery: "replay-from-start",
219
+ contract: options.contract ?? null
220
+ });
221
+ const headers = {
222
+ [versionHeader]: "1",
223
+ [contractHeader]: fingerprint
224
+ };
225
+ const reject = (reason) => Effect.fail(withHeaders(new ResumeRejected({
226
+ route: path,
227
+ reason,
228
+ recovery: "replay-from-start"
229
+ }), headers));
230
+ const received = request.headers[versionHeader] ?? "1";
231
+ if (received !== "1") return yield* withHeaders(new ProtocolVersionUnsupported({
232
+ route: path,
233
+ supported: 1,
234
+ received,
235
+ recovery: "replay-from-start"
236
+ }), headers);
237
+ if (request.headers[contractHeader] !== void 0 && request.headers[contractHeader] !== fingerprint) return yield* reject("contract-changed");
238
+ if (kind === "state" && request.headers["x-streamsy-state-reset"] === "snapshot") return HttpServerResponse.text("Snapshot reset is unsupported; replay from -1", {
239
+ status: 400,
240
+ headers: {
241
+ ...headers,
242
+ ...Http.securityHeaders
243
+ }
244
+ });
245
+ const offset = new URL(request.url, "http://streamsy.internal").searchParams.get("offset");
246
+ const resume = offset !== null && offset !== "-1" && offset !== "now" && request.method !== "HEAD";
247
+ if (resume && !Schema.is(Offset)(offset)) return yield* reject("invalid-offset");
248
+ const service = yield* address.stream;
249
+ return yield* Http.read(address.id, options).pipe(Effect.provideService(StreamsReader, service.wire), Effect.map(HttpServerResponse.setHeaders(headers)), Effect.catchTags({
250
+ StreamNotFound: () => resume ? reject("history-unavailable") : Effect.succeed(HttpServerResponse.empty({
251
+ status: 404,
252
+ headers: {
253
+ ...headers,
254
+ ...Http.securityHeaders
255
+ }
256
+ })),
257
+ StreamGone: () => resume ? reject("history-unavailable") : Effect.succeed(HttpServerResponse.empty({
258
+ status: 410,
259
+ headers: {
260
+ ...headers,
261
+ ...Http.securityHeaders
262
+ }
263
+ })),
264
+ InvalidReadRequest: () => reject("invalid-offset"),
265
+ NotSupported: () => Effect.succeed(HttpServerResponse.empty({
266
+ status: 400,
267
+ headers: {
268
+ ...headers,
269
+ ...Http.securityHeaders
270
+ }
271
+ })),
272
+ StorageFault: () => Effect.fail(withHeaders(new TransportUnavailable({
273
+ route: path,
274
+ detail: "Storage unavailable"
275
+ }), headers)),
276
+ TransportFault: () => Effect.fail(withHeaders(new TransportUnavailable({
277
+ route: path,
278
+ detail: "Transport unavailable"
279
+ }), headers))
280
+ }));
281
+ });
282
+ /** Canonical JSON response for any computation, including mapped projection state. */
283
+ const value = Effect.fn("Serve.value")(function* (schema, compute, options = {}) {
284
+ const request = yield* HttpServerRequest.HttpServerRequest;
285
+ const { route: matchedRoute } = yield* HttpRouter.RouteContext;
286
+ const path = matchedRoute.path;
287
+ const cache = options.cache ?? {
288
+ visibility: "private",
289
+ maxAgeSeconds: 0,
290
+ mustRevalidate: true
291
+ };
292
+ const cacheControl = `${cache.visibility}, max-age=${cache.maxAgeSeconds}${cache.mustRevalidate ? ", must-revalidate" : ""}`;
293
+ const fingerprint = contractFingerprint({
294
+ kind: "value",
295
+ route: path,
296
+ cache: cacheControl,
297
+ contract: options.contract ?? null
298
+ });
299
+ const headers = {
300
+ ...Http.securityHeaders,
301
+ [VALUE_CONTRACT_HEADER]: fingerprint
302
+ };
303
+ if (request.headers["x-streamsy-value-contract"] !== void 0 && request.headers["x-streamsy-value-contract"] !== fingerprint) return yield* withHeaders(new ContractChanged({
304
+ route: path,
305
+ recovery: "refetch"
306
+ }), headers);
307
+ const computed = yield* compute;
308
+ const encoded = yield* Schema.encodeEffect(schema)(computed).pipe(Effect.mapError(() => withHeaders(new WireEncodeFailed({
309
+ route: path,
310
+ detail: "Value cannot be encoded"
311
+ }), headers)));
312
+ const canonical = yield* Effect.try({
313
+ try: () => canonicalJson(encoded),
314
+ catch: () => withHeaders(new WireEncodeFailed({
315
+ route: path,
316
+ detail: "Value is not finite JSON"
317
+ }), headers)
318
+ });
319
+ const etag = valueEtag(canonical);
320
+ const responseHeaders = {
321
+ ...headers,
322
+ "cache-control": cacheControl,
323
+ etag
324
+ };
325
+ const ifNoneMatch = request.headers["if-none-match"];
326
+ if (ifNoneMatch !== void 0 && Array.some(ifNoneMatch.split(","), (tag) => tag.trim() === "*" || tag.trim().replace(/^W\//, "") === etag)) return HttpServerResponse.empty({
327
+ status: 304,
328
+ headers: responseHeaders
329
+ });
330
+ return HttpServerResponse.text(request.method === "HEAD" ? "" : canonical, { headers: {
331
+ ...responseHeaders,
332
+ "content-type": "application/json"
333
+ } });
334
+ });
335
+ export { PublicErrorSchema, serve_exports as Serve };
package/dist/node.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { Duration, Layer } from "effect";
2
+ import { HttpServer } from "effect/http";
3
+ import { ServerOptions } from "node:http";
4
+ export declare const DEFAULT_PORT = 3000;
5
+ export declare const DEFAULT_HOSTNAME = "127.0.0.1";
6
+ export interface ListenerOptions {
7
+ readonly port?: number;
8
+ readonly hostname?: string;
9
+ /**
10
+ * Options for `node:http` `createServer`. The Node defaults keep a parked
11
+ * long poll and an open SSE body alive: `requestTimeout` bounds only the
12
+ * time to receive a request, not the time to answer it.
13
+ */
14
+ readonly server?: ServerOptions;
15
+ readonly gracefulShutdownTimeout?: Duration.Input;
16
+ }
17
+ /** The Node listener for a Streamsy host. */
18
+ export declare const listener: (options?: ListenerOptions) => Layer.Layer<import("effect/http/Etag").Generator | import("effect/http/HttpPlatform").HttpPlatform | HttpServer.HttpServer | import("@effect/platform-node/NodeServices").NodeServices, import("effect/http/HttpServerError").ServeError, never>;
package/dist/node.js ADDED
@@ -0,0 +1,57 @@
1
+ import { Effect, Layer } from "effect";
2
+ import { HttpServer, HttpServerResponse } from "effect/http";
3
+ import { createServer } from "node:http";
4
+ import { NodeHttpServer } from "@effect/platform-node";
5
+ // src/node.ts
6
+ const DEFAULT_PORT = 3e3;
7
+ const DEFAULT_HOSTNAME = "127.0.0.1";
8
+ const listenOptions = (options) => ({
9
+ port: options.port ?? 3e3,
10
+ host: options.hostname ?? "127.0.0.1",
11
+ ...options.gracefulShutdownTimeout === void 0 ? { disablePreemptiveShutdown: true } : { gracefulShutdownTimeout: options.gracefulShutdownTimeout }
12
+ });
13
+ /**
14
+ * Streamsy's protocol answers some requests with a raw body that holds a web
15
+ * `Response`. The web hosts (Bun, Deno, workerd) send that `Response` as it
16
+ * is. The Node server passes a raw body to `ServerResponse.end`, which cannot
17
+ * write a `Response`, so the request never ends. This turns such a body into
18
+ * a stream body with the same status and headers before Node writes it.
19
+ */
20
+ const writableOnNode = (response) => {
21
+ const body = response.body;
22
+ if (!("body" in body) || !(body.body instanceof Response)) return response;
23
+ return HttpServerResponse.fromWeb(body.body).pipe(HttpServerResponse.setStatus(response.status, response.statusText), HttpServerResponse.setHeaders(response.headers), HttpServerResponse.mergeCookies(response.cookies));
24
+ };
25
+ const nodeServer = Layer.effect(HttpServer.HttpServer, Effect.gen(function* () {
26
+ const server = yield* HttpServer.HttpServer;
27
+ return HttpServer.make({
28
+ address: server.address,
29
+ serve: (httpEffect, middleware) => server.serve(Effect.map(middleware === void 0 ? httpEffect : middleware(httpEffect), writableOnNode))
30
+ });
31
+ }));
32
+ /**
33
+ * `NodeHttpServer` binds the port when the listener Layer builds. It adds its
34
+ * `request` handler only when the routes serve, and it removes that handler
35
+ * before the server closes. Node drops a request that has no `request`
36
+ * listener and never answers it. Bun and Deno answer such a request with 404.
37
+ *
38
+ * This server always has one `request` listener of its own. When no other
39
+ * listener is attached, it answers 503 with `Retry-After` and closes the
40
+ * connection. A client that retries then reaches the routes after they serve.
41
+ */
42
+ const createUnavailableAwareServer = (options) => {
43
+ const server = createServer(options);
44
+ server.on("request", (_request, response) => {
45
+ if (server.listenerCount("request") > 1) return;
46
+ response.writeHead(503, {
47
+ "content-type": "text/plain; charset=utf-8",
48
+ "retry-after": "1",
49
+ connection: "close"
50
+ });
51
+ response.end("service unavailable");
52
+ });
53
+ return server;
54
+ };
55
+ /** The Node listener for a Streamsy host. */
56
+ const listener = (options = {}) => nodeServer.pipe(Layer.provideMerge(NodeHttpServer.layer(() => createUnavailableAwareServer(options.server ?? {}), listenOptions(options))));
57
+ export { DEFAULT_HOSTNAME, DEFAULT_PORT, listener };
@@ -0,0 +1,56 @@
1
+ import { r as Wakeups } from "./wakeup--kIoqiIE.js";
2
+ import { Context, Effect, Layer, Option, Scope } from "effect";
3
+ import { HttpRouter, HttpServerRespondable, HttpServerResponse } from "effect/http";
4
+ import { Storage, StorageFault } from "@streamsy/core";
5
+ type ObjectServices<R = never> = Storage | Wakeups | R;
6
+ type ObjectApp<R = never> = Layer.Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", ObjectServices<R>> | HttpRouter.Request.From<"Error", HttpServerRespondable.Respondable>>;
7
+ interface Placement {
8
+ readonly name: (streamPath: string) => string;
9
+ }
10
+ /** The parse side of a stream, which is all a placement rule reads. */
11
+ interface Parser<Params> {
12
+ parse(id: string): Option.Option<Params>;
13
+ }
14
+ /**
15
+ * One rule in a stream placement: a stream and the object name its
16
+ * addresses belong to.
17
+ *
18
+ * The owner takes the same parameters its own stream decodes, so the object name
19
+ * and the storage routing agree by construction. `owner` uses method syntax on
20
+ * purpose: it gives the parameter its contextually typed shape at the call site
21
+ * and keeps the rule assignable to the erased table.
22
+ */
23
+ interface OwnerRule<Params> {
24
+ readonly stream: Parser<Params>;
25
+ /** Names the object that owns every stream matching `stream`. */
26
+ owner(params: Params): string;
27
+ }
28
+ /**
29
+ * A rule after its parameter type is erased. `byRoute` reads each rule only
30
+ * through that rule's own `parse`, so the erased parameter is never observed.
31
+ */
32
+ interface ErasedOwnerRule {
33
+ readonly stream: Parser<unknown>;
34
+ owner(params: never): string;
35
+ }
36
+ /**
37
+ * Declare one placement rule with its stream's parameters in scope.
38
+ *
39
+ * The helper exists so the owner parameter is typed and checked. A rule passed
40
+ * to `Placement.byRoute` without it would see the erased parameter set instead.
41
+ */
42
+ declare const rule: <Params>(spec: OwnerRule<Params>) => ErasedOwnerRule;
43
+ declare const Placement: {
44
+ readonly byStream: () => Placement;
45
+ readonly byKey: (key: (streamPath: string) => string) => Placement;
46
+ /**
47
+ * The first matching rule names the object.
48
+ *
49
+ * A path that no rule matches yields `""`, which the router turns into
50
+ * `400 Invalid placement key`. An owner that throws is a `500`. A rule whose
51
+ * stream cannot decode a segment does not own that path, so the table falls
52
+ * through to the next rule.
53
+ */
54
+ readonly byRoute: (rules: ReadonlyArray<ErasedOwnerRule>) => Placement;
55
+ };
56
+ export { ObjectServices as i, rule as n, ObjectApp as r, Placement as t };
@@ -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 };
@@ -0,0 +1,126 @@
1
+ import { i as makeWakeups, r as Wakeups, t as RequestTouch } from "./wakeup-CcJE8k3i.js";
2
+ import { Context, Effect, Exit, Layer, Option, Scope } from "effect";
3
+ import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
4
+ import { Http } from "@streamsy/core";
5
+ import { securityHeaders, streamPath } from "@streamsy/core/http";
6
+ // src/cloudflare/host-program.ts
7
+ /**
8
+ * Reconciliation belongs to the mutation's finalizer. A request interruption
9
+ * can be pending when the mutation's own uninterruptible region ends, so a
10
+ * second sequential uninterruptible effect is not sufficient. A failed
11
+ * reconciliation is logged and does not replace the mutation's result.
12
+ */
13
+ const withMutationReconciliation = (effect, reconcile) => effect.pipe(Effect.ensuring(reconcile.pipe(Effect.uninterruptible, Effect.catchCause((cause) => Effect.logError("Wakeup reconciliation failed", cause)))));
14
+ // src/cloudflare/object-runtime.ts
15
+ /** Both object hosts install this middleware, including around user routes. */
16
+ function objectApp(app, context) {
17
+ const middleware = HttpRouter.middleware((handler) => Effect.gen(function* () {
18
+ const request = yield* HttpServerRequest.HttpServerRequest;
19
+ const marker = { touched: false };
20
+ const wakeups = Context.get(context, Wakeups);
21
+ return yield* withMutationReconciliation(handler, Effect.suspend(() => [
22
+ "PUT",
23
+ "POST",
24
+ "DELETE"
25
+ ].includes(request.method) || marker.touched ? wakeups.reconcile : Effect.void)).pipe(Effect.catchDefect(() => Effect.succeed(HttpServerResponse.text("Internal server error", {
26
+ status: 500,
27
+ headers: Http.securityHeaders
28
+ }))), Effect.provideService(RequestTouch, marker));
29
+ }).pipe(Effect.provide(context)), { global: true });
30
+ return Layer.merge(app, middleware);
31
+ }
32
+ const unavailable = () => HttpServerResponse.text("Storage unavailable", {
33
+ status: 503,
34
+ headers: {
35
+ ...Http.securityHeaders,
36
+ "retry-after": "1"
37
+ }
38
+ });
39
+ /** One lazy successful acquisition per object owner. Failed attempts close before retry. */
40
+ function acquireObject(layer, compile, wakeups) {
41
+ return Effect.gen(function* () {
42
+ const owner = yield* Scope.Scope;
43
+ return yield* Effect.gen(function* () {
44
+ const attempt = yield* Scope.fork(owner);
45
+ return yield* Effect.gen(function* () {
46
+ const context = yield* Layer.buildWithScope(layer, attempt);
47
+ const scheduler = yield* makeWakeups(context, wakeups);
48
+ return yield* compile(scheduler.context, scheduler.alarm).pipe(Effect.provideService(Scope.Scope, attempt));
49
+ }).pipe(Effect.onExit((exit) => Exit.isFailure(exit) ? Scope.close(attempt, exit) : Effect.void));
50
+ }).pipe(Effect.cachedWithTTL((exit) => Exit.isSuccess(exit) ? "Infinity" : 0));
51
+ });
52
+ }
53
+ /** Request markers are all supplied by this exact object context. */
54
+ function providedApp(app, context) {
55
+ return objectApp(app, context).pipe(HttpRouter.provideRequest(Layer.succeedContext(context)));
56
+ }
57
+ // src/cloudflare/placement.ts
58
+ /**
59
+ * Declare one placement rule with its stream's parameters in scope.
60
+ *
61
+ * The helper exists so the owner parameter is typed and checked. A rule passed
62
+ * to `Placement.byRoute` without it would see the erased parameter set instead.
63
+ */
64
+ const rule = (spec) => spec;
65
+ const Placement = {
66
+ byStream: () => ({ name: (streamPath) => streamPath }),
67
+ byKey: (key) => ({ name: key }),
68
+ /**
69
+ * The first matching rule names the object.
70
+ *
71
+ * A path that no rule matches yields `""`, which the router turns into
72
+ * `400 Invalid placement key`. An owner that throws is a `500`. A rule whose
73
+ * stream cannot decode a segment does not own that path, so the table falls
74
+ * through to the next rule.
75
+ */
76
+ byRoute: (rules) => ({ name: (streamPath) => {
77
+ for (const entry of rules) {
78
+ const params = entry.stream.parse(streamPath);
79
+ if (Option.isNone(params)) continue;
80
+ return entry.owner(params.value);
81
+ }
82
+ return "";
83
+ } })
84
+ };
85
+ // src/cloudflare/router.ts
86
+ const badRequest = (message) => new Response(message, {
87
+ status: 400,
88
+ headers: securityHeaders
89
+ });
90
+ const internalError = () => new Response("Internal server error", {
91
+ status: 500,
92
+ headers: securityHeaders
93
+ });
94
+ const invalidPlacement = () => badRequest("Invalid placement key");
95
+ const resolvePlacement = (placement, streamPath) => {
96
+ try {
97
+ const candidate = placement.name(streamPath);
98
+ const name = String(candidate);
99
+ return candidate === name && name.length > 0 ? {
100
+ ok: true,
101
+ name
102
+ } : {
103
+ ok: false,
104
+ response: invalidPlacement()
105
+ };
106
+ } catch {
107
+ return {
108
+ ok: false,
109
+ response: internalError()
110
+ };
111
+ }
112
+ };
113
+ const router = (options) => {
114
+ const path = streamPath(options.prefix);
115
+ const placement = options.placement ?? Placement.byStream();
116
+ return { fetch(request, env) {
117
+ const url = new URL(request.url);
118
+ const streamPath = path.strip(url.pathname);
119
+ if (!streamPath || streamPath === url.pathname) return badRequest(`Stream path required: ${path.requiredPathPattern()}`);
120
+ const child = resolvePlacement(placement, streamPath);
121
+ if (!child.ok) return child.response;
122
+ const namespace = options.namespace(env);
123
+ return namespace.get(namespace.idFromName(child.name)).fetch(request);
124
+ } };
125
+ };
126
+ export { acquireObject as a, rule as i, router as n, providedApp as o, Placement as r, unavailable as s, resolvePlacement as t };
@@ -0,0 +1,52 @@
1
+ import { Context, Duration, Effect, Layer, Option } from "effect";
2
+ import { Storage, StorageFault } from "@streamsy/core";
3
+ import { DurableObjectStorage } from "@cloudflare/workers-types";
4
+ declare const AlarmSlot_base: Context.ServiceClass<AlarmSlot, "@streamsy/serve/cloudflare/AlarmSlot", {
5
+ readonly current: Effect.Effect<Option.Option<number>>;
6
+ readonly arm: (at: number) => Effect.Effect<void>;
7
+ readonly clear: Effect.Effect<void>;
8
+ }>;
9
+ declare class AlarmSlot extends AlarmSlot_base {}
10
+ declare const alarmSlotLayer: (storage: DurableObjectStorage) => Layer.Layer<AlarmSlot>;
11
+ /**
12
+ * A bounded run that stopped at its bound returns `{ more: true }`. The
13
+ * scheduler then re-arms at once and does not count the pass as a failure.
14
+ */
15
+ interface WakeupProgress {
16
+ readonly more: boolean;
17
+ }
18
+ /** One source of deadline-driven work, derived from committed state. */
19
+ interface Wakeup<R = never> {
20
+ readonly name: string;
21
+ readonly next: Effect.Effect<Option.Option<number>, StorageFault, R>;
22
+ /**
23
+ * Bounded, idempotent work. Delivery is at least once. The permit for this
24
+ * name is not reentrant, so a run must not call its own wakeup's `run`.
25
+ */
26
+ readonly run: Effect.Effect<void | WakeupProgress, unknown, R>;
27
+ readonly retry?: {
28
+ readonly initial?: Duration.Input;
29
+ readonly max?: Duration.Input;
30
+ };
31
+ }
32
+ declare const Wakeup: {
33
+ make: <R>(wakeup: Wakeup<R>) => Wakeup<R>;
34
+ };
35
+ declare const Wakeups_base: Context.ServiceClass<Wakeups, "@streamsy/serve/cloudflare/Wakeups", {
36
+ /**
37
+ * Marks the current request so the host reconciles when the handler
38
+ * returns. Call it before the handler returns. A streamed body that
39
+ * queues work later must call `reconcile` itself.
40
+ */
41
+ readonly touch: Effect.Effect<void>;
42
+ readonly reconcile: Effect.Effect<void>;
43
+ readonly status: Effect.Effect<ReadonlyArray<{
44
+ readonly name: string;
45
+ readonly next: Option.Option<number>;
46
+ readonly failures: number;
47
+ /** The backoff floor. The wakeup does not run before this instant. */
48
+ readonly retryAt: Option.Option<number>;
49
+ }>>;
50
+ }>;
51
+ declare class Wakeups extends Wakeups_base {}
52
+ export { alarmSlotLayer as a, AlarmSlot as i, WakeupProgress as n, Wakeups as r, Wakeup as t };