@streamsy/serve 0.4.0 → 0.6.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.d.ts CHANGED
@@ -1,10 +1,9 @@
1
- import { t as CanonicalValue } from "./contract-BUWJ-Sy2.js";
1
+ import { t as CanonicalValue } from "./contract-CFoMXKuH.js";
2
2
  import { Duration, Effect, Layer, Runtime, Schema } from "effect";
3
3
  import { HttpRouter, HttpServerRequest, HttpServerRespondable, HttpServerResponse } from "effect/http";
4
4
  import { Http, StreamAddress, Streamsy } from "@streamsy/core";
5
5
  import { StreamRoute } from "@streamsy/core/protocol";
6
6
  import { ShutdownSignal } from "@streamsy/core/http";
7
- //#region src/errors.d.ts
8
7
  /** The public error body. `cases` gives one validating constructor per tag. */
9
8
  export declare const PublicErrorSchema: Schema.TaggedUnion<{
10
9
  readonly ContractChanged: Schema.TaggedStruct<"ContractChanged", {
@@ -95,8 +94,6 @@ declare class WireEncodeFailed extends WireEncodeFailed_base {
95
94
  [HttpServerRespondable.symbol](): Effect.Effect<HttpServerResponse.HttpServerResponse, never, never>;
96
95
  }
97
96
  type ServeError = InvalidParams | ProtocolVersionUnsupported | ResumeRejected | ContractChanged | TransportUnavailable | WireEncodeFailed;
98
- //#endregion
99
- //#region src/host.d.ts
100
97
  type DefaultServeRequires<R> = HttpRouter.Request.Only<"Requires", R> | HttpRouter.Request.Only<"GlobalRequires", R>;
101
98
  /** The options of `HttpRouter.serve`, plus the drain bound. */
102
99
  type LayerOptions<R = never, HE = never, HR = DefaultServeRequires<R>> = NonNullable<Parameters<typeof HttpRouter.serve<unknown, unknown, R, HE, HR>>[1]> & {
@@ -155,8 +152,8 @@ declare function mount<P, A, K, RD, RE, const Path extends `/${string}`>(declara
155
152
  }, path: Path & ExactPath<Path, NoInfer<P>>, options?: ReadOptions): import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Error", InvalidParams> | HttpRouter.Request<"Error", ProtocolVersionUnsupported> | HttpRouter.Request<"Error", ResumeRejected> | HttpRouter.Request<"Error", TransportUnavailable> | HttpRouter.Request<"Error", WireEncodeFailed> | HttpRouter.Request.From<"Requires", Exclude<K, HttpRouter.Provided>>>;
156
153
  /** Durable Streams response for one resolved address. */
157
154
  declare function stream<A, K, RD, RE>(address: Streamsy.Address<A, K, RD, RE>, options?: ReadOptions): Effect.Effect<HttpServerResponse.HttpServerResponse, ProtocolVersionUnsupported | ResumeRejected | TransportUnavailable | WireEncodeFailed, K | HttpServerRequest.HttpServerRequest | HttpRouter.RouteContext>;
158
- /** Durable State change response with replay-from-start recovery. */
159
- declare function state<C extends StreamAddress.Collections, K, RD, RE>(address: StreamAddress.StateAddress<C, RD, RE> & Streamsy.Address<StreamAddress.CollectionsChange<C>, K, RD, RE>, options?: ReadOptions): Effect.Effect<HttpServerResponse.HttpServerResponse, ProtocolVersionUnsupported | ResumeRejected | TransportUnavailable | WireEncodeFailed, K | HttpServerRequest.HttpServerRequest | HttpRouter.RouteContext>;
155
+ /** Durable State response with replay-from-start recovery. Stored controls pass through. */
156
+ declare function state<C extends StreamAddress.Collections, K, RD, RE>(address: StreamAddress.StateAddress<C, RD, RE> & Streamsy.Address<StreamAddress.CollectionsEvent<C>, K, RD, RE>, options?: ReadOptions): Effect.Effect<HttpServerResponse.HttpServerResponse, ProtocolVersionUnsupported | ResumeRejected | TransportUnavailable | WireEncodeFailed, K | HttpServerRequest.HttpServerRequest | HttpRouter.RouteContext>;
160
157
  interface ValueOptions {
161
158
  readonly cache?: {
162
159
  readonly visibility: "private" | "public";
@@ -167,5 +164,4 @@ interface ValueOptions {
167
164
  }
168
165
  /** Canonical JSON response for any computation, including mapped projection state. */
169
166
  declare const value: <A, I extends CanonicalValue, E, R, RE>(schema: Schema.Codec<A, I, never, RE>, compute: Effect.Effect<A, E, R>, options?: ValueOptions | undefined) => Effect.Effect<HttpServerResponse.HttpServerResponse, E | ContractChanged | WireEncodeFailed, R | RE | HttpServerRequest.HttpServerRequest | HttpRouter.RouteContext>;
170
- //#endregion
171
167
  export { serve_d_exports as Serve };
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
1
+ import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.js";
2
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 { Cause, Duration, Effect, Exit, Layer, Runtime, Schema } from "effect";
3
+ import { Array, Cause, Duration, Effect, Exit, Layer, Order, Record, Runtime, Schema } from "effect";
4
4
  import { HttpRouter, HttpServerRequest, HttpServerRespondable, HttpServerResponse } from "effect/http";
5
5
  import { Http, Offset } from "@streamsy/core";
6
6
  import { StreamsReader } from "@streamsy/core/protocol";
7
7
  import { ShutdownSignal, layerShutdownSignal } from "@streamsy/core/http";
8
- //#region src/errors.ts
8
+ // src/errors.ts
9
9
  /** The public error body. `cases` gives one validating constructor per tag. */
10
10
  const PublicErrorSchema = Schema.TaggedUnion({
11
11
  InvalidParams: {
@@ -92,8 +92,7 @@ function errorResponse(error, status, headers) {
92
92
  }
93
93
  });
94
94
  }
95
- //#endregion
96
- //#region src/host.ts
95
+ // src/host.ts
97
96
  const drain = (timeout = Duration.seconds(2)) => (serve) => {
98
97
  return Layer.effectDiscard(Effect.gen(function* () {
99
98
  const signal = yield* ShutdownSignal;
@@ -131,8 +130,7 @@ const layer = (routes, options) => HttpRouter.serve(routes, options).pipe(drain(
131
130
  * caused. In a `Layer.launch` main the fiber ends only on failure or a signal.
132
131
  */
133
132
  const teardown = (exit, onExit) => Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause) ? onExit(0) : Runtime.defaultTeardown(exit, onExit);
134
- //#endregion
135
- //#region src/serve.ts
133
+ // src/serve.ts
136
134
  var serve_exports = /* @__PURE__ */ __exportAll({
137
135
  ContractChanged: () => ContractChanged,
138
136
  InvalidParams: () => InvalidParams,
@@ -187,7 +185,7 @@ function mount(declaration, path, options) {
187
185
  function stream(address, options = {}) {
188
186
  return read(address, options, "stream", Effect.succeed(null));
189
187
  }
190
- /** Durable State change response with replay-from-start recovery. */
188
+ /** Durable State response with replay-from-start recovery. Stored controls pass through. */
191
189
  function state(address, options = {}) {
192
190
  return read(address, options, "state", collectionDescriptor(address.collections));
193
191
  }
@@ -198,7 +196,7 @@ function collectionDescriptor(value) {
198
196
  route: matchedRoute.path,
199
197
  detail: "Invalid collection metadata"
200
198
  })));
201
- return Object.fromEntries(Object.entries(collections).map(([type, collection]) => [type, collection.key]));
199
+ return Record.fromEntries(Array.map(Record.toEntries(collections), ([type, collection]) => [type, collection.key]));
202
200
  });
203
201
  }
204
202
  const read = Effect.fn("Serve.read")(function* (address, options, kind, descriptor) {
@@ -215,7 +213,7 @@ const read = Effect.fn("Serve.read")(function* (address, options, kind, descript
215
213
  source,
216
214
  id: address.id,
217
215
  collections,
218
- params: path.split("/").filter((segment) => segment.startsWith(":")).map((segment) => segment.slice(1)).toSorted(),
216
+ params: Array.sort(Array.map(Array.filter(path.split("/"), (segment) => segment.startsWith(":")), (segment) => segment.slice(1)), Order.String),
219
217
  version: 1,
220
218
  recovery: "replay-from-start",
221
219
  contract: options.contract ?? null
@@ -324,7 +322,8 @@ const value = Effect.fn("Serve.value")(function* (schema, compute, options = {})
324
322
  "cache-control": cacheControl,
325
323
  etag
326
324
  };
327
- if (request.headers["if-none-match"]?.split(",").some((tag) => tag.trim() === "*" || tag.trim().replace(/^W\//, "") === etag)) return HttpServerResponse.empty({
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({
328
327
  status: 304,
329
328
  headers: responseHeaders
330
329
  });
@@ -333,5 +332,4 @@ const value = Effect.fn("Serve.value")(function* (schema, compute, options = {})
333
332
  "content-type": "application/json"
334
333
  } });
335
334
  });
336
- //#endregion
337
335
  export { PublicErrorSchema, serve_exports as Serve };
package/dist/node.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { Duration, Layer } from "effect";
2
2
  import { HttpServer } from "effect/http";
3
3
  import { ServerOptions } from "node:http";
4
- //#region src/node.d.ts
5
4
  export declare const DEFAULT_PORT = 3000;
6
5
  export declare const DEFAULT_HOSTNAME = "127.0.0.1";
7
6
  export interface ListenerOptions {
@@ -16,5 +15,4 @@ export interface ListenerOptions {
16
15
  readonly gracefulShutdownTimeout?: Duration.Input;
17
16
  }
18
17
  /** The Node listener for a Streamsy host. */
19
- 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>;
20
- //#endregion
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 CHANGED
@@ -2,7 +2,7 @@ import { Effect, Layer } from "effect";
2
2
  import { HttpServer, HttpServerResponse } from "effect/http";
3
3
  import { createServer } from "node:http";
4
4
  import { NodeHttpServer } from "@effect/platform-node";
5
- //#region src/node.ts
5
+ // src/node.ts
6
6
  const DEFAULT_PORT = 3e3;
7
7
  const DEFAULT_HOSTNAME = "127.0.0.1";
8
8
  const listenOptions = (options) => ({
@@ -54,5 +54,4 @@ const createUnavailableAwareServer = (options) => {
54
54
  };
55
55
  /** The Node listener for a Streamsy host. */
56
56
  const listener = (options = {}) => nodeServer.pipe(Layer.provideMerge(NodeHttpServer.layer(() => createUnavailableAwareServer(options.server ?? {}), listenOptions(options))));
57
- //#endregion
58
57
  export { DEFAULT_HOSTNAME, DEFAULT_PORT, listener };
@@ -0,0 +1,118 @@
1
+ import { i as Wakeups } from "./wakeup-Cy6pvhmx.js";
2
+ import { Context, Effect, Layer, Option, Scope } from "effect";
3
+ import { HttpClientError, HttpRouter, HttpServerRespondable, HttpServerResponse } from "effect/http";
4
+ import { Storage, StorageFault, Streamsy } from "@streamsy/core";
5
+ import { DurableObjectNamespace } from "@cloudflare/workers-types";
6
+ type ObjectServices<R = never> = Storage | Wakeups | R;
7
+ type ObjectApp<R = never> = Layer.Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", ObjectServices<R>> | HttpRouter.Request.From<"Error", HttpServerRespondable.Respondable>>;
8
+ interface Placement {
9
+ readonly name: (streamPath: string) => string;
10
+ }
11
+ /** The parse side of a stream, which is all a placement rule reads. */
12
+ interface Parser<Params> {
13
+ parse(id: string): Option.Option<Params>;
14
+ }
15
+ /**
16
+ * One rule in a stream placement: a stream and the object name its
17
+ * addresses belong to.
18
+ *
19
+ * The owner takes the same parameters its own stream decodes, so the object name
20
+ * and the storage routing agree by construction. `owner` uses method syntax on
21
+ * purpose: it gives the parameter its contextually typed shape at the call site
22
+ * and keeps the rule assignable to the erased table.
23
+ */
24
+ interface OwnerRule<Params> {
25
+ readonly stream: Parser<Params>;
26
+ /** Names the object that owns every stream matching `stream`. */
27
+ owner(params: Params): string;
28
+ }
29
+ /**
30
+ * A rule after its parameter type is erased. `byRoute` reads each rule only
31
+ * through that rule's own `parse`, so the erased parameter is never observed.
32
+ */
33
+ interface ErasedOwnerRule {
34
+ readonly stream: Parser<unknown>;
35
+ owner(params: never): string;
36
+ }
37
+ /**
38
+ * Declare one placement rule with its stream's parameters in scope.
39
+ *
40
+ * The helper exists so the owner parameter is typed and checked. A rule passed
41
+ * to `Placement.byRoute` without it would see the erased parameter set instead.
42
+ */
43
+ declare const rule: <Params>(spec: OwnerRule<Params>) => ErasedOwnerRule;
44
+ declare const Placement: {
45
+ readonly byStream: () => Placement;
46
+ readonly byKey: (key: (streamPath: string) => string) => Placement;
47
+ /**
48
+ * The first matching rule names the object.
49
+ *
50
+ * A path that no rule matches yields `""`, which the router turns into
51
+ * `400 Invalid placement key`. An owner that throws is a `500`. A rule whose
52
+ * stream cannot decode a segment does not own that path, so the table falls
53
+ * through to the next rule.
54
+ */
55
+ readonly byRoute: (rules: ReadonlyArray<ErasedOwnerRule>) => Placement;
56
+ };
57
+ /** Sends one request to the object with this name. */
58
+ type ObjectFetch = (name: string, request: Request) => Effect.Effect<Response, HttpClientError.HttpClientError>;
59
+ interface ObjectStreamsOptions {
60
+ /** The namespace of the owner objects, or a function that sends a request to one object by name. */
61
+ readonly objects: DurableObjectNamespace | ObjectFetch;
62
+ /** Names the owner object of each stream id. Use the same value that the app gives to `router`. */
63
+ readonly placement: Placement;
64
+ /** The mount prefix on the owner object, for example "/streams" or "/__wake". */
65
+ readonly prefix: `/${string}`;
66
+ /** Extra headers on each request. */
67
+ readonly headers?: Readonly<Record<string, string>>;
68
+ }
69
+ /**
70
+ * A `RemoteStreams` layer whose requests go to the object that owns each
71
+ * stream, and never to the network. The owner side is always a Streamsy
72
+ * `Http.routes` mount, so the client declares producer and expected-offset support.
73
+ */
74
+ declare function layer<const Keys extends ReadonlyArray<Streamsy.Any>>(group: Streamsy.Group<Keys>, options: ObjectStreamsOptions): Layer.Layer<Streamsy.Services<Streamsy.Group<Keys>>>;
75
+ declare const ObjectStreams: {
76
+ readonly layer: typeof layer;
77
+ };
78
+ /** What callers may do on a link mount. It applies to wake streams too. */
79
+ type LinkAccess = "read" | "append" | "full";
80
+ interface ObjectLinkOptions<Keys extends ReadonlyArray<Streamsy.Any>> {
81
+ /** The streams that the owner serves to other objects. */
82
+ readonly group: Streamsy.Group<Keys>;
83
+ /** The internal mount. It must start with "/__", so that no package router forwards it. */
84
+ readonly prefix: `/__${string}`;
85
+ /** Names the owner object of each stream id. */
86
+ readonly placement: Placement;
87
+ readonly access: LinkAccess;
88
+ }
89
+ /**
90
+ * The caller side of a link. Return it from the caller object's `links`. The
91
+ * host refuses an object whose own layer also provides one of `streams`.
92
+ */
93
+ interface LinkClient<I> {
94
+ readonly streams: ReadonlyArray<Streamsy.Any>;
95
+ readonly layer: Layer.Layer<I>;
96
+ }
97
+ /** The owner mount, typed as `Http.routes` types it. */
98
+ type LinkRoutes<I> = Layer.Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", Exclude<I, HttpRouter.Provided>>>;
99
+ interface ObjectLink<Keys extends ReadonlyArray<Streamsy.Any>> {
100
+ readonly group: Streamsy.Group<Keys>;
101
+ readonly prefix: `/__${string}`;
102
+ /** The owner side. Add it to the owner object's `app`. */
103
+ readonly routes: LinkRoutes<Streamsy.Services<Streamsy.Group<Keys>>>;
104
+ /**
105
+ * The caller side. `objects` is the owner namespace, or an `ObjectFetch` such
106
+ * as Alchemy's `objectFetch(binding)`. Return the result from the caller's `links`.
107
+ */
108
+ readonly client: (objects: DurableObjectNamespace | ObjectFetch) => LinkClient<Streamsy.Services<Streamsy.Group<Keys>>>;
109
+ }
110
+ /**
111
+ * One value for both sides of an internal mount between objects: the owner
112
+ * mount and the caller client read the same group, prefix and access.
113
+ */
114
+ declare const make: <const Keys extends ReadonlyArray<Streamsy.Any>>(options: ObjectLinkOptions<Keys>) => ObjectLink<Keys>;
115
+ declare const ObjectLink: {
116
+ readonly make: typeof make;
117
+ };
118
+ export { ObjectLinkOptions as a, ObjectStreamsOptions as c, ObjectApp as d, ObjectLink as i, Placement as l, LinkClient as n, ObjectFetch as o, LinkRoutes as r, ObjectStreams as s, LinkAccess as t, rule as u };
@@ -0,0 +1,230 @@
1
+ import { i as makeWakeups, r as Wakeups, t as RequestTouch } from "./wakeup-eIs0xIUh.js";
2
+ import { Array, Context, Effect, Exit, Layer, Option, Predicate, Scope } from "effect";
3
+ import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
4
+ import { Http, StorageFault } from "@streamsy/core";
5
+ import { securityHeaders, streamPath } from "@streamsy/core/http";
6
+ import { RemoteStreams } from "@streamsy/core/remote";
7
+ // src/cloudflare/links.ts
8
+ const clashMessage = (stream) => `${stream.key} is linked to another object, but this object's layer also provides it. Remove it from the storage group.`;
9
+ /**
10
+ * Puts the link layers under the object's own layer and beside it. The own
11
+ * layer sees the linked services at start, and the object sees them at run
12
+ * time. An own layer that also provides a linked stream fails with a
13
+ * `StorageFault`, because its rows would go to a local stream that no
14
+ * client reads.
15
+ */
16
+ function withLinks(own, links) {
17
+ const clients = links ?? [];
18
+ const streams = Array.flatMap(clients, (link) => link.streams);
19
+ const linked = Layer.mergeAll(Layer.empty, ...Array.map(clients, (link) => link.layer));
20
+ const checked = Layer.provide(own, linked).pipe(Layer.tap((local) => Option.match(Array.findFirst(streams, (stream) => Option.isSome(Context.getOption(local, stream))), {
21
+ onNone: () => Effect.void,
22
+ onSome: (stream) => Effect.fail(new StorageFault({
23
+ operation: "streamsy.links",
24
+ message: clashMessage(stream),
25
+ retryable: false
26
+ }))
27
+ })));
28
+ return Layer.merge(checked, linked);
29
+ }
30
+ // src/cloudflare/host-program.ts
31
+ /**
32
+ * Reconciliation belongs to the mutation's finalizer. A request interruption
33
+ * can be pending when the mutation's own uninterruptible region ends, so a
34
+ * second sequential uninterruptible effect is not sufficient. A failed
35
+ * reconciliation is logged and does not replace the mutation's result.
36
+ */
37
+ const withMutationReconciliation = (effect, reconcile) => effect.pipe(Effect.ensuring(reconcile.pipe(Effect.uninterruptible, Effect.catchCause((cause) => Effect.logError("Wakeup reconciliation failed", cause)))));
38
+ // src/cloudflare/object-runtime.ts
39
+ /** Both object hosts install this middleware, including around user routes. */
40
+ function objectApp(app, context) {
41
+ const middleware = HttpRouter.middleware((handler) => Effect.gen(function* () {
42
+ const request = yield* HttpServerRequest.HttpServerRequest;
43
+ const marker = { touched: false };
44
+ const wakeups = Context.get(context, Wakeups);
45
+ return yield* withMutationReconciliation(handler, Effect.suspend(() => [
46
+ "PUT",
47
+ "POST",
48
+ "DELETE"
49
+ ].includes(request.method) || marker.touched ? wakeups.reconcile : Effect.void)).pipe(Effect.catchDefect(() => Effect.succeed(HttpServerResponse.text("Internal server error", {
50
+ status: 500,
51
+ headers: Http.securityHeaders
52
+ }))), Effect.provideService(RequestTouch, marker));
53
+ }).pipe(Effect.provide(context)), { global: true });
54
+ return Layer.merge(app, middleware);
55
+ }
56
+ const unavailable = () => HttpServerResponse.text("Storage unavailable", {
57
+ status: 503,
58
+ headers: {
59
+ ...Http.securityHeaders,
60
+ "retry-after": "1"
61
+ }
62
+ });
63
+ /** One lazy successful acquisition per object owner. Failed attempts close before retry. */
64
+ function acquireObject(layer, compile, wakeups) {
65
+ return Effect.gen(function* () {
66
+ const owner = yield* Scope.Scope;
67
+ return yield* Effect.gen(function* () {
68
+ const attempt = yield* Scope.fork(owner);
69
+ return yield* Effect.gen(function* () {
70
+ const context = yield* Layer.buildWithScope(layer, attempt);
71
+ const scheduler = yield* makeWakeups(context, wakeups).pipe(Effect.provideService(Scope.Scope, attempt));
72
+ return yield* compile(scheduler.context, scheduler.alarm).pipe(Effect.provideService(Scope.Scope, attempt));
73
+ }).pipe(Effect.onExit((exit) => Exit.isFailure(exit) ? Scope.close(attempt, exit) : Effect.void));
74
+ }).pipe(Effect.cachedWithTTL((exit) => Exit.isSuccess(exit) ? "Infinity" : 0));
75
+ });
76
+ }
77
+ /** Request markers are all supplied by this exact object context. */
78
+ function providedApp(app, context) {
79
+ return objectApp(app, context).pipe(HttpRouter.provideRequest(Layer.succeedContext(context)));
80
+ }
81
+ // src/cloudflare/placement.ts
82
+ /**
83
+ * Declare one placement rule with its stream's parameters in scope.
84
+ *
85
+ * The helper exists so the owner parameter is typed and checked. A rule passed
86
+ * to `Placement.byRoute` without it would see the erased parameter set instead.
87
+ */
88
+ const rule = (spec) => spec;
89
+ const Placement = {
90
+ byStream: () => ({ name: (streamPath) => streamPath }),
91
+ byKey: (key) => ({ name: key }),
92
+ /**
93
+ * The first matching rule names the object.
94
+ *
95
+ * A path that no rule matches yields `""`, which the router turns into
96
+ * `400 Invalid placement key`. An owner that throws is a `500`. A rule whose
97
+ * stream cannot decode a segment does not own that path, so the table falls
98
+ * through to the next rule.
99
+ */
100
+ byRoute: (rules) => ({ name: (streamPath) => {
101
+ for (const entry of rules) {
102
+ const params = entry.stream.parse(streamPath);
103
+ if (Option.isNone(params)) continue;
104
+ return entry.owner(params.value);
105
+ }
106
+ return "";
107
+ } })
108
+ };
109
+ // src/cloudflare/router.ts
110
+ const badRequest = (message) => new Response(message, {
111
+ status: 400,
112
+ headers: securityHeaders
113
+ });
114
+ const internalError = () => new Response("Internal server error", {
115
+ status: 500,
116
+ headers: securityHeaders
117
+ });
118
+ const notFound = () => new Response(null, {
119
+ status: 404,
120
+ headers: securityHeaders
121
+ });
122
+ /**
123
+ * Paths whose first segment starts with `__` belong to object links and
124
+ * other internal mounts. No package router forwards them to an object.
125
+ */
126
+ const isReserved = (pathname) => pathname.startsWith("/__");
127
+ const invalidPlacement = () => badRequest("Invalid placement key");
128
+ const resolvePlacement = (placement, streamPath) => {
129
+ try {
130
+ const candidate = placement.name(streamPath);
131
+ const name = String(candidate);
132
+ return candidate === name && name.length > 0 ? {
133
+ ok: true,
134
+ name
135
+ } : {
136
+ ok: false,
137
+ response: invalidPlacement()
138
+ };
139
+ } catch {
140
+ return {
141
+ ok: false,
142
+ response: internalError()
143
+ };
144
+ }
145
+ };
146
+ const router = (options) => {
147
+ const path = streamPath(options.prefix);
148
+ const placement = options.placement ?? Placement.byStream();
149
+ return { fetch(request, env) {
150
+ const url = new URL(request.url);
151
+ if (isReserved(url.pathname)) return notFound();
152
+ const streamPath = path.strip(url.pathname);
153
+ if (!streamPath || streamPath === url.pathname) return badRequest(`Stream path required: ${path.requiredPathPattern()}`);
154
+ const child = resolvePlacement(placement, streamPath);
155
+ if (!child.ok) return child.response;
156
+ const namespace = options.namespace(env);
157
+ return namespace.get(namespace.idFromName(child.name)).fetch(request);
158
+ } };
159
+ };
160
+ // src/cloudflare/object-streams.ts
161
+ /** Translate a host transport error at the Web request boundary. */
162
+ const objectTransportError = (request, cause) => new HttpClientError.HttpClientError({ reason: new HttpClientError.TransportError({
163
+ request: HttpClientRequest.fromWeb(request),
164
+ cause
165
+ }) });
166
+ /** The host is never resolved. It only makes a valid base URL for `RemoteStreams`. */
167
+ const base = "https://streamsy.internal";
168
+ /** A namespace becomes "send to the object with this name". */
169
+ const toObjectFetch = (objects) => Predicate.isFunction(objects) ? objects : (name, request) => Effect.tryPromise({
170
+ try: () => Promise.resolve(objects.get(objects.idFromName(name)).fetch(request)),
171
+ catch: (cause) => objectTransportError(request, cause)
172
+ });
173
+ /** An HTTP client that calls `fetch` in place of the network. */
174
+ const stubClient = (send) => Layer.succeed(HttpClient.HttpClient, HttpClient.make((request, _url, signal) => HttpClientRequest.toWeb(request, { signal }).pipe(Effect.mapError((cause) => new HttpClientError.HttpClientError({ reason: new HttpClientError.InvalidUrlError({
175
+ request,
176
+ cause
177
+ }) })), Effect.flatMap(send), Effect.map((response) => HttpClientResponse.fromWeb(request, response)))));
178
+ /**
179
+ * The `fetch` that `ObjectStreams` gives its HTTP client. A path outside the
180
+ * prefix, or a stream outside the group, answers 404 here and calls no
181
+ * object. A bad placement answers as the public router does.
182
+ */
183
+ const objectRouter = (group, options) => {
184
+ const path = streamPath(options.prefix);
185
+ const send = toObjectFetch(options.objects);
186
+ return Effect.fn("ObjectStreams.route")(function* (request) {
187
+ const pathname = new URL(request.url).pathname;
188
+ const id = path.strip(pathname);
189
+ if (!id || id === pathname || !Array.some(group.streams, (stream) => stream.match(id))) return new Response(null, { status: 404 });
190
+ const owner = resolvePlacement(options.placement, id);
191
+ return owner.ok ? yield* send(owner.name, request) : owner.response;
192
+ });
193
+ };
194
+ function layer(group, options) {
195
+ return RemoteStreams.layer(group, {
196
+ baseUrl: `${base}${options.prefix}`,
197
+ headers: options.headers ?? {},
198
+ capabilities: {
199
+ producer: true,
200
+ expectedOffset: true
201
+ }
202
+ }).pipe(Layer.provide(stubClient(objectRouter(group, options))), Layer.orDie);
203
+ }
204
+ const ObjectStreams = { layer };
205
+ // src/cloudflare/object-link.ts
206
+ /**
207
+ * One value for both sides of an internal mount between objects: the owner
208
+ * mount and the caller client read the same group, prefix and access.
209
+ */
210
+ const make = (options) => {
211
+ if (!/^\/__[^/]/.test(options.prefix)) throw new RangeError(`An object link prefix must start with "/__", got "${options.prefix}"`);
212
+ return {
213
+ group: options.group,
214
+ prefix: options.prefix,
215
+ routes: Http.routes(options.group, {
216
+ prefix: options.prefix,
217
+ access: options.access
218
+ }),
219
+ client: (objects) => ({
220
+ streams: options.group.streams,
221
+ layer: ObjectStreams.layer(options.group, {
222
+ objects,
223
+ placement: options.placement,
224
+ prefix: options.prefix
225
+ })
226
+ })
227
+ };
228
+ };
229
+ const ObjectLink = { make };
230
+ export { resolvePlacement as a, rule as c, unavailable as d, withLinks as f, isReserved as i, acquireObject as l, ObjectStreams as n, router as o, objectTransportError as r, Placement as s, ObjectLink as t, providedApp as u };
@@ -1,4 +1,4 @@
1
- //#region \0rolldown/runtime.js
1
+ // \0rolldown/runtime.js
2
2
  var __defProp = Object.defineProperty;
3
3
  var __exportAll = (all, no_symbols) => {
4
4
  let target = {};
@@ -9,5 +9,4 @@ var __exportAll = (all, no_symbols) => {
9
9
  if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
10
  return target;
11
11
  };
12
- //#endregion
13
12
  export { __exportAll as t };
@@ -1,7 +1,6 @@
1
- import { Context, Duration, Effect, Layer, Option } from "effect";
1
+ import { Context, Effect, Layer, Option, Schedule, Scope } from "effect";
2
2
  import { Storage, StorageFault } from "@streamsy/core";
3
3
  import { DurableObjectStorage } from "@cloudflare/workers-types";
4
- //#region src/cloudflare/alarm.d.ts
5
4
  declare const AlarmSlot_base: Context.ServiceClass<AlarmSlot, "@streamsy/serve/cloudflare/AlarmSlot", {
6
5
  readonly current: Effect.Effect<Option.Option<number>>;
7
6
  readonly arm: (at: number) => Effect.Effect<void>;
@@ -9,8 +8,6 @@ declare const AlarmSlot_base: Context.ServiceClass<AlarmSlot, "@streamsy/serve/c
9
8
  }>;
10
9
  declare class AlarmSlot extends AlarmSlot_base {}
11
10
  declare const alarmSlotLayer: (storage: DurableObjectStorage) => Layer.Layer<AlarmSlot>;
12
- //#endregion
13
- //#region src/cloudflare/wakeup.d.ts
14
11
  /**
15
12
  * A bounded run that stopped at its bound returns `{ more: true }`. The
16
13
  * scheduler then re-arms at once and does not count the pass as a failure.
@@ -27,11 +24,11 @@ interface Wakeup<R = never> {
27
24
  * name is not reentrant, so a run must not call its own wakeup's `run`.
28
25
  */
29
26
  readonly run: Effect.Effect<void | WakeupProgress, unknown, R>;
30
- readonly retry?: {
31
- readonly initial?: Duration.Input;
32
- readonly max?: Duration.Input;
33
- };
27
+ /** Alarm retries use this schedule's delays without sleeping in an alarm. */
28
+ readonly retry?: Schedule.Schedule<unknown>;
34
29
  }
30
+ /** The host acquires effectful wakeups in the same scope as its storage layer. */
31
+ type WakeupSource<R = never> = Wakeup<R | Wakeups> | Effect.Effect<Wakeup<R | Wakeups>, never, R | Scope.Scope>;
35
32
  declare const Wakeup: {
36
33
  make: <R>(wakeup: Wakeup<R>) => Wakeup<R>;
37
34
  };
@@ -52,5 +49,4 @@ declare const Wakeups_base: Context.ServiceClass<Wakeups, "@streamsy/serve/cloud
52
49
  }>>;
53
50
  }>;
54
51
  declare class Wakeups extends Wakeups_base {}
55
- //#endregion
56
- export { alarmSlotLayer as a, AlarmSlot as i, WakeupProgress as n, Wakeups as r, Wakeup as t };
52
+ export { AlarmSlot as a, Wakeups as i, WakeupProgress as n, alarmSlotLayer as o, WakeupSource as r, Wakeup as t };