@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/README.md +36 -3
- package/dist/action/contract.d.ts +1 -3
- package/dist/action/contract.js +3 -3
- package/dist/action/errors.d.ts +3 -6
- package/dist/action/errors.js +1 -2
- package/dist/action/outbox.d.ts +0 -2
- package/dist/action/outbox.js +7 -8
- package/dist/action/runtime.d.ts +2 -4
- package/dist/action/runtime.js +6 -7
- package/dist/action/sql.d.ts +3 -5
- package/dist/action/sql.js +6 -7
- package/dist/alchemy.d.ts +16 -12
- package/dist/alchemy.js +17 -6
- package/dist/bun.d.ts +1 -3
- package/dist/bun.js +1 -2
- package/dist/cloudflare.d.ts +16 -14
- package/dist/cloudflare.js +5 -6
- package/dist/{contract-BUWJ-Sy2.d.ts → contract-CFoMXKuH.d.ts} +0 -2
- package/dist/contract.d.ts +1 -1
- package/dist/contract.js +16 -9
- package/dist/deno.d.ts +1 -3
- package/dist/deno.js +1 -2
- package/dist/index.d.ts +3 -7
- package/dist/index.js +10 -12
- package/dist/node.d.ts +1 -3
- package/dist/node.js +1 -2
- package/dist/object-link-BbxO01Z5.d.ts +118 -0
- package/dist/object-link-HD-tFY4o.js +230 -0
- package/dist/{rolldown-runtime-D7D4PA-g.js → rolldown-runtime-D8jQpDG7.js} +1 -2
- package/dist/{wakeup-BtnMEjLv.d.ts → wakeup-Cy6pvhmx.d.ts} +6 -10
- package/dist/{wakeup-CWKg8pts.js → wakeup-eIs0xIUh.js} +38 -28
- package/package.json +3 -3
- package/dist/placement-BOM3E08P.d.ts +0 -60
- package/dist/router-DviIc1O7.js +0 -130
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { t as CanonicalValue } from "./contract-
|
|
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
|
|
159
|
-
declare function state<C extends StreamAddress.Collections, K, RD, RE>(address: StreamAddress.StateAddress<C, RD, RE> & Streamsy.Address<StreamAddress.
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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("/")
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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 };
|