@swirls/looms 0.3.0 → 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/CHANGELOG.md +64 -0
- package/dist/actor/cell.d.ts +21 -2
- package/dist/actor/cell.js +15 -3
- package/dist/actor/host.d.ts +10 -2
- package/dist/actor/host.js +27 -4
- package/dist/actor/index.d.ts +2 -1
- package/dist/actor/index.js +1 -1
- package/dist/actor/routing.d.ts +3 -0
- package/dist/actor/routing.js +13 -2
- package/dist/agent/debugger/agent-chat.js +3 -30
- package/dist/agent/debugger/streaming-text.d.ts +6 -0
- package/dist/agent/debugger/streaming-text.js +33 -0
- package/dist/agent/definitions-store.d.ts +1 -1
- package/dist/agent/definitions-store.js +1 -1
- package/dist/agent/definitions.d.ts +48 -0
- package/dist/agent/definitions.js +44 -0
- package/dist/agent/effects.d.ts +184 -0
- package/dist/agent/effects.js +172 -40
- package/dist/agent/events.d.ts +89 -0
- package/dist/agent/events.js +25 -0
- package/dist/agent/module.d.ts +819 -407
- package/dist/agent/module.js +5 -1
- package/dist/agent/scope.d.ts +88 -0
- package/dist/agent/threads.js +39 -3
- package/dist/agent/types.d.ts +4 -0
- package/dist/approval/effects.d.ts +40 -0
- package/dist/approval/module.d.ts +80 -0
- package/dist/client/client.d.ts +468 -119
- package/dist/client/client.js +214 -21
- package/dist/client/index.d.ts +1 -1
- package/dist/client/sse.d.ts +1 -1
- package/dist/client/sse.js +2 -2
- package/dist/cloudflare/durable-object.d.ts +66 -2
- package/dist/cloudflare/durable-object.js +96 -1
- package/dist/cloudflare/event-store.d.ts +7 -3
- package/dist/cloudflare/event-store.js +16 -3
- package/dist/cloudflare/index.d.ts +7 -1
- package/dist/cloudflare/index.js +7 -1
- package/dist/cloudflare/memory-queue.d.ts +18 -0
- package/dist/cloudflare/memory-queue.js +53 -0
- package/dist/cloudflare/queue-consumer.d.ts +52 -0
- package/dist/cloudflare/queue-consumer.js +102 -0
- package/dist/cloudflare/queue-message.d.ts +31 -0
- package/dist/cloudflare/queue-message.js +33 -0
- package/dist/cloudflare/queue-pull.d.ts +62 -0
- package/dist/cloudflare/queue-pull.js +130 -0
- package/dist/cloudflare/queue-worker.d.ts +39 -0
- package/dist/cloudflare/queue-worker.js +62 -0
- package/dist/cloudflare/r2-blob-store.d.ts +33 -0
- package/dist/cloudflare/r2-blob-store.js +60 -0
- package/dist/cloudflare/router.d.ts +4 -1
- package/dist/cloudflare/router.js +8 -3
- package/dist/cloudflare/websocket.d.ts +27 -0
- package/dist/cloudflare/websocket.js +215 -0
- package/dist/core/attempts.d.ts +8 -0
- package/dist/core/attempts.js +27 -0
- package/dist/core/blob-offload.d.ts +21 -0
- package/dist/core/blob-offload.js +143 -0
- package/dist/core/blob-store.d.ts +45 -0
- package/dist/core/blob-store.js +164 -0
- package/dist/core/blobs.d.ts +37 -0
- package/dist/core/blobs.js +160 -0
- package/dist/core/definition-resolver.d.ts +40 -0
- package/dist/core/definition-resolver.js +129 -0
- package/dist/core/definition-store.d.ts +6 -0
- package/dist/core/definition-store.js +37 -1
- package/dist/core/effects.d.ts +86 -3
- package/dist/core/effects.js +99 -3
- package/dist/core/envelope.d.ts +9 -0
- package/dist/core/envelope.js +4 -0
- package/dist/core/event-codec.js +3 -0
- package/dist/core/fold.js +83 -1
- package/dist/core/ids.d.ts +2 -0
- package/dist/core/ids.js +4 -0
- package/dist/core/index.d.ts +10 -0
- package/dist/core/index.js +9 -0
- package/dist/core/launcher.d.ts +97 -0
- package/dist/core/launcher.js +67 -0
- package/dist/core/module-scope.d.ts +7 -3
- package/dist/core/module-scope.js +3 -0
- package/dist/core/module.d.ts +10 -0
- package/dist/core/projection.d.ts +5 -4
- package/dist/core/projection.js +4 -0
- package/dist/core/protocol.d.ts +36 -1
- package/dist/core/protocol.js +35 -0
- package/dist/core/published.d.ts +2 -0
- package/dist/core/published.js +4 -1
- package/dist/{react → core}/register.d.ts +9 -8
- package/dist/core/replay.d.ts +96 -0
- package/dist/core/run-context.d.ts +33 -0
- package/dist/core/run-context.js +23 -0
- package/dist/core/sqlite-store.js +57 -5
- package/dist/core/state.d.ts +60 -0
- package/dist/core/state.js +19 -0
- package/dist/core/store.d.ts +6 -0
- package/dist/core/store.js +7 -0
- package/dist/core/subscription.d.ts +97 -0
- package/dist/core/subscription.js +224 -0
- package/dist/core/thread.d.ts +7 -0
- package/dist/core/thread.js +1 -0
- package/dist/core/trace.d.ts +21 -0
- package/dist/core/trace.js +45 -0
- package/dist/core/worker-callback.d.ts +25 -1
- package/dist/debugger/app/assets/{index-CgDzWJg7.css → index-B7GdT1UA.css} +1 -1
- package/dist/debugger/app/assets/index-C31SnCuN.js +22 -0
- package/dist/debugger/app/assets/routes-DPA3O5AF.js +47 -0
- package/dist/debugger/app/index.html +2 -2
- package/dist/debugger/components/event-inspector.d.ts +3 -1
- package/dist/debugger/components/event-inspector.js +25 -4
- package/dist/debugger/components/event-stream.d.ts +4 -1
- package/dist/debugger/components/event-stream.js +2 -2
- package/dist/debugger/components/run-tree.d.ts +5 -2
- package/dist/debugger/components/run-tree.js +10 -4
- package/dist/debugger/context.d.ts +8 -1
- package/dist/debugger/context.js +15 -3
- package/dist/debugger/events/protocol-summarize.js +18 -3
- package/dist/debugger/index.d.ts +1 -1
- package/dist/debugger/index.js +1 -1
- package/dist/debugger/shell/debugger-shell.js +1 -1
- package/dist/debugger/shell/events-pane.js +4 -3
- package/dist/debugger/styles.css +1 -0
- package/dist/evaluator/definitions-store.d.ts +3 -4
- package/dist/evaluator/definitions-store.js +3 -3
- package/dist/evaluator/definitions.d.ts +4 -1
- package/dist/evaluator/definitions.js +5 -2
- package/dist/evaluator/effects.d.ts +41 -0
- package/dist/evaluator/effects.js +6 -2
- package/dist/evaluator/module.d.ts +84 -0
- package/dist/evaluator/module.js +7 -3
- package/dist/evaluator/threads.d.ts +2 -0
- package/dist/evaluator/threads.js +4 -0
- package/dist/projectors/event-log.d.ts +87 -0
- package/dist/projectors/event-log.js +396 -0
- package/dist/projectors/index-model.d.ts +12 -0
- package/dist/projectors/index-model.js +12 -3
- package/dist/projectors/memory.js +7 -0
- package/dist/projectors/postgres.d.ts +1 -0
- package/dist/projectors/postgres.js +32 -15
- package/dist/projectors/schema.d.ts +5 -0
- package/dist/projectors/schema.js +26 -1
- package/dist/projectors/sqlite.js +47 -19
- package/dist/projectors/with-projectors.d.ts +7 -1
- package/dist/projectors/with-projectors.js +13 -3
- package/dist/react/derived.d.ts +26 -8
- package/dist/react/derived.js +23 -26
- package/dist/react/index.d.ts +3 -3
- package/dist/react/index.js +2 -2
- package/dist/react/materialize.js +5 -1
- package/dist/react/react.d.ts +16 -9
- package/dist/react/react.js +47 -23
- package/dist/react/store.d.ts +14 -6
- package/dist/react/store.js +104 -12
- package/dist/react/tables.d.ts +4 -1
- package/dist/runtime/cancellation.d.ts +2 -1
- package/dist/runtime/cancellation.js +35 -2
- package/dist/runtime/child-run.d.ts +10 -0
- package/dist/runtime/child-run.js +58 -0
- package/dist/runtime/deadline-transition-planner.js +11 -2
- package/dist/runtime/dispatch-effect.d.ts +2 -2
- package/dist/runtime/dispatch-effect.js +43 -19
- package/dist/runtime/errors.d.ts +14 -1
- package/dist/runtime/errors.js +10 -0
- package/dist/runtime/filesystem-blob-store.d.ts +6 -0
- package/dist/runtime/filesystem-blob-store.js +59 -0
- package/dist/runtime/helpers.d.ts +15 -6
- package/dist/runtime/helpers.js +46 -6
- package/dist/runtime/index.d.ts +4 -2
- package/dist/runtime/index.js +3 -1
- package/dist/runtime/local-effect-executor.d.ts +11 -2
- package/dist/runtime/local-effect-executor.js +80 -4
- package/dist/runtime/looms.d.ts +38 -4
- package/dist/runtime/looms.js +17 -3
- package/dist/runtime/repository.d.ts +5 -4
- package/dist/runtime/repository.js +5 -2
- package/dist/runtime/retry-policy.d.ts +5 -0
- package/dist/runtime/retry-policy.js +10 -2
- package/dist/runtime/run-start.d.ts +1 -1
- package/dist/runtime/run-start.js +10 -0
- package/dist/runtime/runtime.d.ts +2 -2
- package/dist/runtime/runtime.js +191 -68
- package/dist/runtime/server/auth.d.ts +3 -0
- package/dist/runtime/server/fetch-handler.d.ts +2 -2
- package/dist/runtime/server/fetch-handler.js +35 -4
- package/dist/runtime/server/grant.d.ts +49 -0
- package/dist/runtime/server/grant.js +177 -0
- package/dist/runtime/server/routes/effects.js +47 -5
- package/dist/runtime/server/routes/runs.js +6 -1
- package/dist/runtime/server/run-route.d.ts +18 -0
- package/dist/runtime/server/run-route.js +26 -0
- package/dist/runtime/server.d.ts +3 -1
- package/dist/runtime/server.js +3 -1
- package/dist/runtime/sse.js +26 -4
- package/dist/runtime/subscription.d.ts +8 -0
- package/dist/runtime/subscription.js +23 -0
- package/dist/runtime/types.d.ts +87 -8
- package/dist/runtime/wake-engine.d.ts +4 -2
- package/dist/runtime/wake-engine.js +57 -6
- package/dist/runtime/worker-callbacks.d.ts +12 -2
- package/dist/runtime/worker-callbacks.js +102 -72
- package/dist/runtime/worker-effect-dispatch.d.ts +14 -5
- package/dist/runtime/worker-effect-dispatch.js +84 -18
- package/dist/schedule/debugger/index.d.ts +4 -0
- package/dist/schedule/debugger/index.js +11 -0
- package/dist/schedule/debugger/schedule-view.d.ts +3 -0
- package/dist/schedule/debugger/schedule-view.js +68 -0
- package/dist/schedule/debugger/summarize.d.ts +2 -0
- package/dist/schedule/debugger/summarize.js +43 -0
- package/dist/schedule/definitions-store.d.ts +10 -0
- package/dist/schedule/definitions-store.js +8 -0
- package/dist/schedule/definitions.d.ts +57 -0
- package/dist/schedule/definitions.js +30 -0
- package/dist/schedule/effects.d.ts +756 -0
- package/dist/schedule/effects.js +244 -0
- package/dist/schedule/ensure.d.ts +55 -0
- package/dist/schedule/ensure.js +109 -0
- package/dist/schedule/events.d.ts +116 -0
- package/dist/schedule/events.js +49 -0
- package/dist/schedule/index.d.ts +11 -0
- package/dist/schedule/index.js +11 -0
- package/dist/schedule/module.d.ts +1474 -0
- package/dist/schedule/module.js +22 -0
- package/dist/schedule/projections.d.ts +55 -0
- package/dist/schedule/projections.js +88 -0
- package/dist/schedule/scope.d.ts +121 -0
- package/dist/schedule/scope.js +7 -0
- package/dist/schedule/signals.d.ts +25 -0
- package/dist/schedule/signals.js +33 -0
- package/dist/schedule/specs.d.ts +59 -0
- package/dist/schedule/specs.js +231 -0
- package/dist/schedule/threads.d.ts +25 -0
- package/dist/schedule/threads.js +259 -0
- package/dist/testing/corpus-module.d.ts +34 -0
- package/dist/testing/corpus-module.js +87 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +5 -6
- package/dist/testing/replay-corpus.d.ts +22 -0
- package/dist/testing/replay-corpus.js +50 -0
- package/dist/workflow/definitions-store.d.ts +1 -1
- package/dist/workflow/definitions-store.js +1 -1
- package/dist/workflow/definitions.d.ts +14 -0
- package/dist/workflow/effects.d.ts +122 -0
- package/dist/workflow/effects.js +16 -6
- package/dist/workflow/events.d.ts +42 -0
- package/dist/workflow/events.js +4 -0
- package/dist/workflow/module.d.ts +345 -32
- package/dist/workflow/module.js +7 -3
- package/dist/workflow/scope.d.ts +42 -0
- package/dist/workflow/threads.d.ts +72 -0
- package/dist/workflow/threads.js +106 -38
- package/package.json +11 -2
- package/dist/debugger/app/assets/index-BevQYa-U.js +0 -22
- package/dist/debugger/app/assets/routes-B17Om93i.js +0 -47
- /package/dist/{react → core}/register.js +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { blobStoreFromBytes, BlobStoreError } from '@swirls/looms/core';
|
|
3
|
+
const PREFIX = 'looms/blobs/';
|
|
4
|
+
/**
|
|
5
|
+
* Content-addressed blob store in R2 under `looms/blobs/`. Every run's Durable Object can
|
|
6
|
+
* share one bucket: each run writes its own reference markers, and a body is deleted only
|
|
7
|
+
* after no marker for it remains.
|
|
8
|
+
*/
|
|
9
|
+
export function r2BlobStore(bucket) {
|
|
10
|
+
return blobStoreFromBytes(r2BlobBytes(bucket));
|
|
11
|
+
}
|
|
12
|
+
function r2BlobBytes(bucket) {
|
|
13
|
+
const keyFor = (key) => `${PREFIX}${key}`;
|
|
14
|
+
return {
|
|
15
|
+
read: (key) => Effect.tryPromise({
|
|
16
|
+
try: () => bucket.get(keyFor(key)),
|
|
17
|
+
catch: (cause) => new BlobStoreError(`Unable to read blob key ${key}`, cause),
|
|
18
|
+
}).pipe(Effect.flatMap((object) => {
|
|
19
|
+
if (!object) {
|
|
20
|
+
return Effect.succeed(null);
|
|
21
|
+
}
|
|
22
|
+
return Effect.tryPromise({
|
|
23
|
+
try: () => object.arrayBuffer(),
|
|
24
|
+
catch: (cause) => new BlobStoreError(`Unable to read blob key ${key}`, cause),
|
|
25
|
+
}).pipe(Effect.map((buffer) => new Uint8Array(buffer)));
|
|
26
|
+
})),
|
|
27
|
+
write: (key, body) => Effect.tryPromise({
|
|
28
|
+
try: () => bucket.put(keyFor(key), body),
|
|
29
|
+
catch: (cause) => new BlobStoreError(`Unable to write blob key ${key}`, cause),
|
|
30
|
+
}).pipe(Effect.asVoid),
|
|
31
|
+
remove: (key) => Effect.tryPromise({
|
|
32
|
+
try: () => bucket.delete(keyFor(key)),
|
|
33
|
+
catch: (cause) => new BlobStoreError(`Unable to delete blob key ${key}`, cause),
|
|
34
|
+
}).pipe(Effect.asVoid),
|
|
35
|
+
exists: (key) => Effect.tryPromise({
|
|
36
|
+
try: () => bucket.head(keyFor(key)),
|
|
37
|
+
catch: (cause) => new BlobStoreError(`Unable to check blob key ${key}`, cause),
|
|
38
|
+
}).pipe(Effect.map((object) => object !== null)),
|
|
39
|
+
list: (prefix) => listPrefixed(bucket, `${PREFIX}${prefix}`),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function listPrefixed(bucket, prefix) {
|
|
43
|
+
return Effect.gen(function* () {
|
|
44
|
+
const keys = [];
|
|
45
|
+
let cursor;
|
|
46
|
+
for (;;) {
|
|
47
|
+
const page = yield* Effect.tryPromise({
|
|
48
|
+
try: () => cursor === undefined ? bucket.list({ prefix }) : bucket.list({ prefix, cursor }),
|
|
49
|
+
catch: (cause) => new BlobStoreError(`Unable to list blob keys ${prefix}`, cause),
|
|
50
|
+
});
|
|
51
|
+
for (const object of page.objects) {
|
|
52
|
+
keys.push(object.key.slice(PREFIX.length));
|
|
53
|
+
}
|
|
54
|
+
if (!page.truncated || page.cursor === undefined || page.cursor === cursor) {
|
|
55
|
+
return keys;
|
|
56
|
+
}
|
|
57
|
+
cursor = page.cursor;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AnyRuntimeModule } from '@swirls/looms/core';
|
|
1
2
|
export interface DurableObjectStubLike {
|
|
2
3
|
fetch(request: Request): Promise<Response>;
|
|
3
4
|
}
|
|
@@ -9,4 +10,6 @@ export interface DurableObjectNamespaceLike<T extends DurableObjectStubLike = Du
|
|
|
9
10
|
* Handles `/health` locally, returns 501 for global run lists and forks, and forwards
|
|
10
11
|
* all other run and event-stream requests to `namespace.getByName(runId)`.
|
|
11
12
|
*/
|
|
12
|
-
export declare function routeToDurableObject(namespace: DurableObjectNamespaceLike, req: Request
|
|
13
|
+
export declare function routeToDurableObject(namespace: DurableObjectNamespaceLike, req: Request, options?: {
|
|
14
|
+
readonly modules?: readonly AnyRuntimeModule[] | (() => readonly AnyRuntimeModule[]);
|
|
15
|
+
}): Promise<Response | null>;
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { crossRunResponse, resolveRunTarget, RunTargetError } from '@swirls/looms/actor';
|
|
1
|
+
import { crossRunResponse, definitionsResponse, resolveRunTarget, RunTargetError, } from '@swirls/looms/actor';
|
|
2
2
|
/**
|
|
3
3
|
* Routes incoming HTTP requests to the appropriate Durable Object actor cell.
|
|
4
4
|
* Handles `/health` locally, returns 501 for global run lists and forks, and forwards
|
|
5
5
|
* all other run and event-stream requests to `namespace.getByName(runId)`.
|
|
6
6
|
*/
|
|
7
|
-
export function routeToDurableObject(namespace, req) {
|
|
8
|
-
|
|
7
|
+
export function routeToDurableObject(namespace, req, options) {
|
|
8
|
+
const url = new URL(req.url);
|
|
9
|
+
if (url.pathname === '/health') {
|
|
9
10
|
return Promise.resolve(Response.json({ ok: true }));
|
|
10
11
|
}
|
|
12
|
+
const definitions = definitionsResponse(req, options?.modules);
|
|
13
|
+
if (definitions) {
|
|
14
|
+
return Promise.resolve(definitions);
|
|
15
|
+
}
|
|
11
16
|
const unsupported = crossRunResponse(req, 'the Durable Object backend');
|
|
12
17
|
if (unsupported) {
|
|
13
18
|
return Promise.resolve(unsupported);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { type EventStore } from '@swirls/looms/core';
|
|
3
|
+
import { type Authorize, type LoomsRuntime } from '@swirls/looms/runtime';
|
|
4
|
+
export interface HibernatingSocketState {
|
|
5
|
+
acceptWebSocket(ws: WebSocket, tags?: string[]): void;
|
|
6
|
+
getWebSockets?(tag?: string): readonly WebSocket[];
|
|
7
|
+
setWebSocketAutoResponse?(pair: WebSocketRequestResponsePair): void;
|
|
8
|
+
}
|
|
9
|
+
export interface OpenRunSocketOptions {
|
|
10
|
+
readonly ctx: HibernatingSocketState;
|
|
11
|
+
readonly runtime: LoomsRuntime;
|
|
12
|
+
readonly store: EventStore;
|
|
13
|
+
readonly request: Request;
|
|
14
|
+
readonly runId: string;
|
|
15
|
+
readonly authorize?: Authorize;
|
|
16
|
+
readonly createPair?: () => {
|
|
17
|
+
readonly client: WebSocket;
|
|
18
|
+
readonly server: WebSocket;
|
|
19
|
+
} | undefined;
|
|
20
|
+
}
|
|
21
|
+
export declare function isRunSocketUpgrade(request: Request): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Wakes on append, not on a socket subscription, so a hibernated object can sleep
|
|
24
|
+
* while its WebSockets stay accepted.
|
|
25
|
+
*/
|
|
26
|
+
export declare function attachSocketFanout(ctx: HibernatingSocketState, store: EventStore, runId: string): EventStore;
|
|
27
|
+
export declare function openRunSocket(options: OpenRunSocketOptions): Effect.Effect<Response>;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Effect, Option, Predicate } from 'effect';
|
|
2
|
+
import { attachmentFromFilter, fanoutSubscription, openSubscription, subscriptionFilter, subscriptionFromSeq, } from '@swirls/looms/core';
|
|
3
|
+
import { authResponse, defaultAuthorize, grantSubprotocol, loadSubscriptionOpening, readSubscriptionBounds, runRouteInfo, } from '@swirls/looms/runtime';
|
|
4
|
+
const EVENTS_PATH = /^\/runs\/([^/]+)\/events\/?$/;
|
|
5
|
+
const autoResponseInstalled = new WeakSet();
|
|
6
|
+
export function isRunSocketUpgrade(request) {
|
|
7
|
+
if (request.method !== 'GET') {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const upgrade = request.headers.get('upgrade');
|
|
11
|
+
if (upgrade === null || upgrade.toLowerCase() !== 'websocket') {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return runEventsPath(new URL(request.url)) !== undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Wakes on append, not on a socket subscription, so a hibernated object can sleep
|
|
18
|
+
* while its WebSockets stay accepted.
|
|
19
|
+
*/
|
|
20
|
+
export function attachSocketFanout(ctx, store, runId) {
|
|
21
|
+
return tapAppend(store, (events) => readSubscriptionBounds(store, runId).pipe(Effect.map((bounds) => {
|
|
22
|
+
fanoutSubscription(acceptedSockets(ctx, runId), events, { head: bounds.head });
|
|
23
|
+
}), Effect.orElseSucceed(() => {
|
|
24
|
+
fanoutSubscription(acceptedSockets(ctx, runId), events, { head: 1 });
|
|
25
|
+
})));
|
|
26
|
+
}
|
|
27
|
+
export function openRunSocket(options) {
|
|
28
|
+
const url = new URL(options.request.url);
|
|
29
|
+
const requested = runEventsPath(url);
|
|
30
|
+
if (requested !== options.runId) {
|
|
31
|
+
return Effect.succeed(Response.json({ error: 'Run not found' }, { status: 404 }));
|
|
32
|
+
}
|
|
33
|
+
const authorize = options.authorize ?? defaultAuthorize;
|
|
34
|
+
return Effect.gen(function* () {
|
|
35
|
+
const route = yield* runRouteInfo(options.runtime, options.store, {
|
|
36
|
+
access: 'read',
|
|
37
|
+
name: 'runs.events',
|
|
38
|
+
runId: options.runId,
|
|
39
|
+
}).pipe(Effect.option);
|
|
40
|
+
if (Option.isNone(route)) {
|
|
41
|
+
return Response.json({ error: 'Event log is unavailable' }, { status: 500 });
|
|
42
|
+
}
|
|
43
|
+
const decision = yield* authorize(options.request, route.value);
|
|
44
|
+
const denied = authResponse(decision);
|
|
45
|
+
if (denied) {
|
|
46
|
+
return denied;
|
|
47
|
+
}
|
|
48
|
+
const cursor = subscriptionFromSeq(url, options.request.headers.get('last-event-id'));
|
|
49
|
+
if (!cursor.ok) {
|
|
50
|
+
return Response.json({ error: cursor.message }, { status: 400 });
|
|
51
|
+
}
|
|
52
|
+
const createPair = options.createPair ?? createWebSocketPair;
|
|
53
|
+
const pair = createPair();
|
|
54
|
+
if (!pair) {
|
|
55
|
+
return new Response('WebSocket upgrade is not available', { status: 426 });
|
|
56
|
+
}
|
|
57
|
+
installAutoResponse(options.ctx);
|
|
58
|
+
options.ctx.acceptWebSocket(pair.server, [options.runId]);
|
|
59
|
+
const filter = subscriptionFilter(url);
|
|
60
|
+
const attachment = attachmentFromFilter(filter, cursor.fromSeq, null);
|
|
61
|
+
const socket = wrapSocket(pair.server);
|
|
62
|
+
socket.setAttachment(attachment);
|
|
63
|
+
const loaded = yield* loadSubscriptionOpening(options.store, options.runId, cursor.fromSeq).pipe(Effect.option);
|
|
64
|
+
if (Option.isNone(loaded)) {
|
|
65
|
+
return Response.json({ error: 'Event log is unavailable' }, { status: 500 });
|
|
66
|
+
}
|
|
67
|
+
const current = socket.attachment() ?? attachment;
|
|
68
|
+
const next = openSubscription(socket, current, loaded.value);
|
|
69
|
+
if (next) {
|
|
70
|
+
socket.setAttachment(next);
|
|
71
|
+
}
|
|
72
|
+
const protocol = grantSubprotocol(options.request);
|
|
73
|
+
return new Response(null, {
|
|
74
|
+
status: 101,
|
|
75
|
+
webSocket: pair.client,
|
|
76
|
+
headers: protocol ? { 'sec-websocket-protocol': protocol } : undefined,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function tapAppend(store, onAppend) {
|
|
81
|
+
return {
|
|
82
|
+
...store,
|
|
83
|
+
append: (runId, events, appendOptions) => Effect.gen(function* () {
|
|
84
|
+
const result = yield* store.append(runId, events, appendOptions);
|
|
85
|
+
const first = result.sequences[0];
|
|
86
|
+
if (first === undefined) {
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
yield* Effect.gen(function* () {
|
|
90
|
+
const appended = yield* store.read(runId, {
|
|
91
|
+
fromSeq: first,
|
|
92
|
+
limit: result.sequences.length,
|
|
93
|
+
});
|
|
94
|
+
if (appended.length === 0) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
yield* onAppend(appended);
|
|
98
|
+
}).pipe(Effect.ignore);
|
|
99
|
+
return result;
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function acceptedSockets(ctx, runId) {
|
|
104
|
+
if (!ctx.getWebSockets) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
return ctx.getWebSockets(runId).map(wrapSocket);
|
|
108
|
+
}
|
|
109
|
+
function wrapSocket(ws) {
|
|
110
|
+
return {
|
|
111
|
+
send: (data) => {
|
|
112
|
+
ws.send(data);
|
|
113
|
+
},
|
|
114
|
+
close: (code, reason) => {
|
|
115
|
+
ws.close(code, reason);
|
|
116
|
+
},
|
|
117
|
+
get bufferedAmount() {
|
|
118
|
+
return Predicate.isNumber(ws.bufferedAmount) ? ws.bufferedAmount : 0;
|
|
119
|
+
},
|
|
120
|
+
attachment: () => readAttachment(ws),
|
|
121
|
+
setAttachment: (attachment) => {
|
|
122
|
+
writeAttachment(ws, attachment);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function readAttachment(ws) {
|
|
127
|
+
if (!('deserializeAttachment' in ws)) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
// oxlint-disable-next-line typescript/unbound-method -- hibernation methods are own properties of the platform socket
|
|
131
|
+
const read = ws.deserializeAttachment;
|
|
132
|
+
if (!Predicate.isFunction(read)) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const value = read.call(ws);
|
|
136
|
+
if (!Predicate.isObject(value) || !('cursor' in value) || !Predicate.isNumber(value.cursor)) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
const principal = 'principal' in value && (value.principal === null || Predicate.isString(value.principal))
|
|
140
|
+
? value.principal
|
|
141
|
+
: null;
|
|
142
|
+
const threadId = 'threadId' in value && Predicate.isString(value.threadId) ? value.threadId : null;
|
|
143
|
+
const includeEphemeral = !('includeEphemeral' in value) || value.includeEphemeral !== false;
|
|
144
|
+
return {
|
|
145
|
+
principal,
|
|
146
|
+
cursor: value.cursor,
|
|
147
|
+
threadId,
|
|
148
|
+
includeEphemeral,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function writeAttachment(ws, attachment) {
|
|
152
|
+
if (!('serializeAttachment' in ws)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// oxlint-disable-next-line typescript/unbound-method -- hibernation methods are own properties of the platform socket
|
|
156
|
+
const write = ws.serializeAttachment;
|
|
157
|
+
if (!Predicate.isFunction(write)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
write.call(ws, attachment);
|
|
161
|
+
}
|
|
162
|
+
function installAutoResponse(ctx) {
|
|
163
|
+
if (autoResponseInstalled.has(ctx) || !ctx.setWebSocketAutoResponse) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair('ping', 'pong'));
|
|
168
|
+
autoResponseInstalled.add(ctx);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Hosts without the hibernation helper still accept the socket.
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function createWebSocketPair() {
|
|
175
|
+
// SAFETY: Workers adds WebSocketPair to the global object. Other hosts leave it absent.
|
|
176
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
177
|
+
const candidate = globalThis.WebSocketPair;
|
|
178
|
+
if (!Predicate.isFunction(candidate)) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
let pair;
|
|
182
|
+
try {
|
|
183
|
+
pair = Reflect.construct(candidate, []);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
if (!isSocketPair(pair)) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
return { client: pair[0], server: pair[1] };
|
|
192
|
+
}
|
|
193
|
+
function isSocket(value) {
|
|
194
|
+
return (Predicate.isObject(value) &&
|
|
195
|
+
'send' in value &&
|
|
196
|
+
Predicate.isFunction(value.send) &&
|
|
197
|
+
'close' in value &&
|
|
198
|
+
Predicate.isFunction(value.close));
|
|
199
|
+
}
|
|
200
|
+
function isSocketPair(value) {
|
|
201
|
+
return Predicate.isObject(value) && isSocket(value[0]) && isSocket(value[1]);
|
|
202
|
+
}
|
|
203
|
+
function runEventsPath(url) {
|
|
204
|
+
const match = EVENTS_PATH.exec(url.pathname);
|
|
205
|
+
const raw = match?.[1];
|
|
206
|
+
if (!raw) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
return decodeURIComponent(raw);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AnyEventEnvelope } from './register';
|
|
2
|
+
/** The effect a `runtime.effect.retry.scheduled` event retries, or `undefined` for other events. */
|
|
3
|
+
export declare function retriedEffectId(event: AnyEventEnvelope): string | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Drops ephemeral events from effect attempts that a later retry in `events` superseded. Durable
|
|
6
|
+
* events and ephemeral events from the current attempt are kept.
|
|
7
|
+
*/
|
|
8
|
+
export declare function dropSupersededAttempts<E extends AnyEventEnvelope>(events: readonly E[]): readonly E[];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
import { protocolCatalog } from './protocol.js';
|
|
3
|
+
const RETRY_SCHEDULED = 'runtime.effect.retry.scheduled';
|
|
4
|
+
const isRetryPayload = Schema.is(protocolCatalog.entries['effect.retry.scheduled']);
|
|
5
|
+
/** The effect a `runtime.effect.retry.scheduled` event retries, or `undefined` for other events. */
|
|
6
|
+
export function retriedEffectId(event) {
|
|
7
|
+
return event.type === RETRY_SCHEDULED && isRetryPayload(event.payload)
|
|
8
|
+
? event.payload.effectId
|
|
9
|
+
: undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Drops ephemeral events from effect attempts that a later retry in `events` superseded. Durable
|
|
13
|
+
* events and ephemeral events from the current attempt are kept.
|
|
14
|
+
*/
|
|
15
|
+
export function dropSupersededAttempts(events) {
|
|
16
|
+
const lastRetry = new Map();
|
|
17
|
+
events.forEach((event, index) => {
|
|
18
|
+
const effectId = retriedEffectId(event);
|
|
19
|
+
if (effectId !== undefined) {
|
|
20
|
+
lastRetry.set(effectId, index);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
if (lastRetry.size === 0) {
|
|
24
|
+
return events;
|
|
25
|
+
}
|
|
26
|
+
return events.filter((event, index) => !event.ephemeral || !event.effectId || (lastRetry.get(event.effectId) ?? -1) < index);
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type BlobStore } from './blob-store';
|
|
2
|
+
import { type SnapshotStore } from './snapshot-store';
|
|
3
|
+
import { type EventStore } from './store';
|
|
4
|
+
export interface BlobOffloadOptions {
|
|
5
|
+
/** Snapshot store to offload and resolve. Trim keeps digests referenced by its latest snapshot. */
|
|
6
|
+
readonly snapshots?: SnapshotStore;
|
|
7
|
+
/** UTF-8 JSON size above which a string is stored by hash. Default 64 KiB. */
|
|
8
|
+
readonly thresholdBytes?: number;
|
|
9
|
+
/** Serialized event size that claims the whole payload. Default 1 MiB. */
|
|
10
|
+
readonly maxEventBytes?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Store oversized JSON outside the event log.
|
|
14
|
+
*
|
|
15
|
+
* Append replaces values over the threshold with a content-addressed reference before the
|
|
16
|
+
* event byte limit runs. `read`, `readStream`, and `subscribe` resolve those references, so
|
|
17
|
+
* folds, projections, and callers see the original values. The stored log keeps the references.
|
|
18
|
+
* When `snapshots` is set, snapshot save and load do the same, and trim deletes a body only
|
|
19
|
+
* once no retained event and no latest snapshot in any run still needs it.
|
|
20
|
+
*/
|
|
21
|
+
export declare function withBlobOffload(store: EventStore, blobs: BlobStore, options?: BlobOffloadOptions): EventStore;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Effect, Option, Schema, Stream } from 'effect';
|
|
2
|
+
import { BlobStoreError } from './blob-store.js';
|
|
3
|
+
import { DEFAULT_BLOB_THRESHOLD_BYTES, claimJson, collectBlobDigests, expandBlobDigests, offloadJson, resolveJson, } from './blobs.js';
|
|
4
|
+
import { DEFAULT_MAX_EVENT_BYTES, utf8JsonBytes } from './limits.js';
|
|
5
|
+
import { SnapshotStoreError, withSnapshotStore } from './snapshot-store.js';
|
|
6
|
+
import { RunStateSchema } from './state.js';
|
|
7
|
+
import { EventStoreError } from './store.js';
|
|
8
|
+
const decodeJsonText = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json));
|
|
9
|
+
const decodeRunState = Schema.decodeUnknownEffect(RunStateSchema);
|
|
10
|
+
/**
|
|
11
|
+
* Store oversized JSON outside the event log.
|
|
12
|
+
*
|
|
13
|
+
* Append replaces values over the threshold with a content-addressed reference before the
|
|
14
|
+
* event byte limit runs. `read`, `readStream`, and `subscribe` resolve those references, so
|
|
15
|
+
* folds, projections, and callers see the original values. The stored log keeps the references.
|
|
16
|
+
* When `snapshots` is set, snapshot save and load do the same, and trim deletes a body only
|
|
17
|
+
* once no retained event and no latest snapshot in any run still needs it.
|
|
18
|
+
*/
|
|
19
|
+
export function withBlobOffload(store, blobs, options) {
|
|
20
|
+
const thresholdBytes = options?.thresholdBytes ?? DEFAULT_BLOB_THRESHOLD_BYTES;
|
|
21
|
+
const maxEventBytes = options?.maxEventBytes ?? DEFAULT_MAX_EVENT_BYTES;
|
|
22
|
+
const snapshots = options?.snapshots;
|
|
23
|
+
const wrappedSnapshots = snapshots
|
|
24
|
+
? wrapSnapshots(snapshots, store, blobs, thresholdBytes)
|
|
25
|
+
: undefined;
|
|
26
|
+
const release = (runId) => keepDigests(store, snapshots, blobs, runId).pipe(Effect.flatMap((keep) => blobs.replaceRefs(runId, keep)), Effect.mapError(asEventError));
|
|
27
|
+
const wrapped = {
|
|
28
|
+
append: (runId, events, appendOptions) => Effect.gen(function* () {
|
|
29
|
+
const stored = yield* Effect.forEach(events, (event) => offloadAppendable(runId, event, blobs, thresholdBytes, maxEventBytes)).pipe(Effect.mapError(asEventError));
|
|
30
|
+
const seeds = digestsOf(stored.map((event) => event.payload));
|
|
31
|
+
const expanded = yield* expandBlobDigests(blobs, seeds).pipe(Effect.mapError(asEventError));
|
|
32
|
+
yield* blobs.retain(runId, [...expanded]).pipe(Effect.mapError(asEventError));
|
|
33
|
+
return yield* store.append(runId, stored, appendOptions);
|
|
34
|
+
}),
|
|
35
|
+
read: (runId, readOptions) => store
|
|
36
|
+
.read(runId, readOptions)
|
|
37
|
+
.pipe(Effect.flatMap((events) => Effect.forEach(events, (event) => resolveEnvelope(event, blobs).pipe(Effect.mapError(asEventError))))),
|
|
38
|
+
readStream: store.readStream
|
|
39
|
+
? (runId, readOptions) => store.readStream(runId, readOptions).pipe(Stream.mapEffect((event) => resolveEnvelope(event, blobs).pipe(Effect.mapError(asEventError))))
|
|
40
|
+
: undefined,
|
|
41
|
+
tail: (runId) => store.tail(runId),
|
|
42
|
+
bounds: store.bounds ? (runId) => store.bounds(runId) : undefined,
|
|
43
|
+
trim: store.trim
|
|
44
|
+
? (runId, beforeSeq) => store.trim(runId, beforeSeq).pipe(Effect.andThen(release(runId)))
|
|
45
|
+
: undefined,
|
|
46
|
+
subscribe: (runId, subscribeOptions) => store
|
|
47
|
+
.subscribe(runId, subscribeOptions)
|
|
48
|
+
.pipe(Stream.mapEffect((event) => resolveEnvelope(event, blobs).pipe(Effect.mapError(asEventError)))),
|
|
49
|
+
listRuns: store.listRuns,
|
|
50
|
+
};
|
|
51
|
+
if (!wrappedSnapshots) {
|
|
52
|
+
return wrapped;
|
|
53
|
+
}
|
|
54
|
+
return withSnapshotStore(wrapped, wrappedSnapshots);
|
|
55
|
+
}
|
|
56
|
+
function offloadAppendable(runId, event, blobs, thresholdBytes, maxEventBytes) {
|
|
57
|
+
return Effect.gen(function* () {
|
|
58
|
+
const payload = yield* offloadJson(event.payload, blobs, { thresholdBytes });
|
|
59
|
+
const offloaded = { ...event, payload };
|
|
60
|
+
if (eventBytes(runId, offloaded) <= maxEventBytes) {
|
|
61
|
+
return offloaded;
|
|
62
|
+
}
|
|
63
|
+
const ref = yield* claimJson(payload, blobs);
|
|
64
|
+
return { ...event, payload: ref };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function eventBytes(runId, event) {
|
|
68
|
+
return utf8JsonBytes({ ...event, runId, seq: event.seq ?? 0 });
|
|
69
|
+
}
|
|
70
|
+
function resolveEnvelope(event, blobs) {
|
|
71
|
+
return resolveJson(event.payload, blobs).pipe(Effect.map((payload) => ({ ...event, payload })));
|
|
72
|
+
}
|
|
73
|
+
function digestsOf(values) {
|
|
74
|
+
const seeds = new Set();
|
|
75
|
+
for (const value of values) {
|
|
76
|
+
collectBlobDigests(value, seeds);
|
|
77
|
+
}
|
|
78
|
+
return seeds;
|
|
79
|
+
}
|
|
80
|
+
function keepDigests(store, snapshots, blobs, runId) {
|
|
81
|
+
return Effect.gen(function* () {
|
|
82
|
+
const events = yield* store.read(runId);
|
|
83
|
+
const seeds = digestsOf(events.map((event) => event.payload));
|
|
84
|
+
if (snapshots) {
|
|
85
|
+
const latest = yield* snapshots
|
|
86
|
+
.loadLatest(runId)
|
|
87
|
+
.pipe(Effect.mapError((cause) => new EventStoreError(cause.message, cause)));
|
|
88
|
+
if (Option.isSome(latest)) {
|
|
89
|
+
const encoded = yield* jsonValue(latest.value.state).pipe(Effect.mapError((cause) => new EventStoreError(cause.message, cause)));
|
|
90
|
+
collectBlobDigests(encoded, seeds);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return yield* expandBlobDigests(blobs, seeds);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function wrapSnapshots(snapshots, store, blobs, thresholdBytes) {
|
|
97
|
+
const release = (runId) => keepDigests(store, snapshots, blobs, runId).pipe(Effect.flatMap((keep) => blobs.replaceRefs(runId, keep)), Effect.mapError((cause) => new SnapshotStoreError(cause.message, cause)));
|
|
98
|
+
return {
|
|
99
|
+
loadLatest: (runId) => Effect.gen(function* () {
|
|
100
|
+
const loaded = yield* snapshots.loadLatest(runId);
|
|
101
|
+
if (Option.isNone(loaded)) {
|
|
102
|
+
return loaded;
|
|
103
|
+
}
|
|
104
|
+
const encoded = yield* jsonValue(loaded.value.state).pipe(Effect.mapError(asSnapshotError));
|
|
105
|
+
const resolved = yield* resolveJson(encoded, blobs).pipe(Effect.mapError(asSnapshotError));
|
|
106
|
+
const state = yield* decodeRunState(resolved).pipe(Effect.mapError((cause) => new SnapshotStoreError('snapshot state did not match RunState', cause)));
|
|
107
|
+
return Option.some({ ...loaded.value, state });
|
|
108
|
+
}),
|
|
109
|
+
save: (snapshot) => Effect.gen(function* () {
|
|
110
|
+
const encoded = yield* jsonValue(snapshot.state).pipe(Effect.mapError(asSnapshotError));
|
|
111
|
+
const offloaded = yield* offloadJson(encoded, blobs, {
|
|
112
|
+
thresholdBytes,
|
|
113
|
+
claimRoot: false,
|
|
114
|
+
}).pipe(Effect.mapError(asSnapshotError));
|
|
115
|
+
const state = yield* decodeRunState(offloaded).pipe(Effect.mapError((cause) => new SnapshotStoreError('snapshot state did not match RunState', cause)));
|
|
116
|
+
const seeds = digestsOf([offloaded]);
|
|
117
|
+
const expanded = yield* expandBlobDigests(blobs, seeds).pipe(Effect.mapError(asSnapshotError));
|
|
118
|
+
yield* blobs.retain(snapshot.runId, [...expanded]).pipe(Effect.mapError(asSnapshotError));
|
|
119
|
+
return yield* snapshots.save({
|
|
120
|
+
runId: snapshot.runId,
|
|
121
|
+
cursor: snapshot.cursor,
|
|
122
|
+
stateHash: snapshot.stateHash,
|
|
123
|
+
takenAt: snapshot.takenAt,
|
|
124
|
+
state,
|
|
125
|
+
});
|
|
126
|
+
}),
|
|
127
|
+
listCursors: snapshots.listCursors ? (runId) => snapshots.listCursors(runId) : undefined,
|
|
128
|
+
prune: snapshots.prune
|
|
129
|
+
? (runId, keepLatest) => snapshots.prune(runId, keepLatest).pipe(Effect.andThen(release(runId)))
|
|
130
|
+
: undefined,
|
|
131
|
+
saveHeader: (header) => snapshots.saveHeader(header),
|
|
132
|
+
listHeaders: snapshots.listHeaders,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function jsonValue(state) {
|
|
136
|
+
return decodeJsonText(JSON.stringify(state)).pipe(Effect.mapError((cause) => new BlobStoreError('snapshot state is not JSON', cause)));
|
|
137
|
+
}
|
|
138
|
+
function asEventError(cause) {
|
|
139
|
+
return new EventStoreError(cause.message, cause);
|
|
140
|
+
}
|
|
141
|
+
function asSnapshotError(cause) {
|
|
142
|
+
return new SnapshotStoreError(cause.message, cause);
|
|
143
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
declare const BlobStoreError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
3
|
+
readonly _tag: "BlobStoreError";
|
|
4
|
+
} & Readonly<A>;
|
|
5
|
+
export declare class BlobStoreError extends BlobStoreError_base<{
|
|
6
|
+
readonly message: string;
|
|
7
|
+
readonly cause?: unknown;
|
|
8
|
+
}> {
|
|
9
|
+
readonly cause?: unknown;
|
|
10
|
+
constructor(message: string, cause?: unknown);
|
|
11
|
+
}
|
|
12
|
+
declare const BlobNotFoundError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
13
|
+
readonly _tag: "BlobNotFoundError";
|
|
14
|
+
} & Readonly<A>;
|
|
15
|
+
export declare class BlobNotFoundError extends BlobNotFoundError_base<{
|
|
16
|
+
readonly digest: string;
|
|
17
|
+
readonly message: string;
|
|
18
|
+
}> {
|
|
19
|
+
constructor(digest: string);
|
|
20
|
+
}
|
|
21
|
+
export type BlobStoreFailure = BlobStoreError | BlobNotFoundError;
|
|
22
|
+
/**
|
|
23
|
+
* Content-addressed bytes. `put` is idempotent: the same digest stores one body.
|
|
24
|
+
* `retain` and `replaceRefs` record which digests a run still needs. A body is deleted only
|
|
25
|
+
* when no run references it. Many runs may share one store, each writing its own references.
|
|
26
|
+
*/
|
|
27
|
+
export interface BlobStore {
|
|
28
|
+
readonly put: (digest: string, body: Uint8Array) => Effect.Effect<void, BlobStoreError>;
|
|
29
|
+
readonly get: (digest: string) => Effect.Effect<Uint8Array, BlobStoreFailure>;
|
|
30
|
+
readonly retain: (runId: string, digests: ReadonlyArray<string>) => Effect.Effect<void, BlobStoreError>;
|
|
31
|
+
readonly replaceRefs: (runId: string, keep: ReadonlySet<string>) => Effect.Effect<void, BlobStoreError>;
|
|
32
|
+
}
|
|
33
|
+
export declare function isSha256Digest(value: string): boolean;
|
|
34
|
+
/** Byte operations behind {@link blobStoreFromBytes}. Keys are store-relative, not run ids. */
|
|
35
|
+
export interface BlobBytes {
|
|
36
|
+
readonly read: (key: string) => Effect.Effect<Uint8Array | null, BlobStoreError>;
|
|
37
|
+
readonly write: (key: string, body: Uint8Array) => Effect.Effect<void, BlobStoreError>;
|
|
38
|
+
readonly remove: (key: string) => Effect.Effect<void, BlobStoreError>;
|
|
39
|
+
readonly exists: (key: string) => Effect.Effect<boolean, BlobStoreError>;
|
|
40
|
+
readonly list: (prefix: string) => Effect.Effect<ReadonlyArray<string>, BlobStoreError>;
|
|
41
|
+
}
|
|
42
|
+
export declare function memoryBlobStore(): BlobStore;
|
|
43
|
+
export declare function blobStoreFromBytes(bytes: BlobBytes): BlobStore;
|
|
44
|
+
export declare function sha256Hex(body: Uint8Array): Effect.Effect<string, BlobStoreError>;
|
|
45
|
+
export {};
|