@swirls/looms 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -2
- package/dist/actor/cell.d.ts +9 -4
- package/dist/actor/cell.js +7 -11
- package/dist/actor/host.d.ts +7 -3
- package/dist/actor/host.js +55 -32
- package/dist/actor/index.d.ts +1 -1
- package/dist/actor/index.js +1 -1
- package/dist/actor/routing.d.ts +12 -1
- package/dist/actor/routing.js +57 -51
- package/dist/agent/agent-session.d.ts +5 -0
- package/dist/agent/agent-session.js +270 -0
- package/dist/agent/definitions-store.d.ts +2 -3
- package/dist/agent/definitions-store.js +2 -4
- package/dist/agent/definitions.d.ts +46 -20
- package/dist/agent/definitions.js +36 -8
- package/dist/agent/effects.d.ts +359 -2
- package/dist/agent/effects.js +60 -34
- package/dist/agent/events.d.ts +175 -19
- package/dist/agent/events.js +45 -10
- package/dist/agent/index.d.ts +1 -0
- package/dist/agent/index.js +1 -0
- package/dist/agent/llm.d.ts +11 -2
- package/dist/agent/llm.js +13 -3
- package/dist/agent/module.d.ts +812 -78
- package/dist/agent/module.js +16 -4
- package/dist/agent/scope.d.ts +174 -18
- package/dist/agent/signals.d.ts +24 -1
- package/dist/agent/signals.js +77 -0
- package/dist/agent/threads.d.ts +3 -33
- package/dist/agent/threads.js +20 -25
- package/dist/agent/tool-schema.js +0 -1
- package/dist/agent/types.d.ts +49 -4
- package/dist/agent/types.js +5 -5
- package/dist/ai-vercel/index.d.ts +1 -0
- package/dist/ai-vercel/index.js +1 -0
- package/dist/ai-vercel/messages.js +22 -7
- package/dist/ai-vercel/vercel-evaluator.d.ts +23 -0
- package/dist/ai-vercel/vercel-evaluator.js +68 -0
- package/dist/ai-vercel/vercel-llm.js +64 -44
- package/dist/approval/effects.d.ts +89 -0
- package/dist/approval/module.d.ts +179 -29
- package/dist/approval/projections.d.ts +0 -14
- package/dist/approval/projections.js +20 -3
- package/dist/approval/signals.d.ts +2 -0
- package/dist/approval/signals.js +18 -2
- package/dist/cli/cli.js +2 -0
- package/dist/cli/commands.js +12 -16
- package/dist/client/client.d.ts +1195 -18
- package/dist/client/client.js +118 -120
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/sse.d.ts +13 -0
- package/dist/client/sse.js +38 -44
- package/dist/cloudflare/alarm-scheduler.d.ts +12 -2
- package/dist/cloudflare/alarm-scheduler.js +34 -7
- package/dist/cloudflare/durable-object.d.ts +10 -2
- package/dist/cloudflare/durable-object.js +59 -28
- package/dist/cloudflare/event-store.d.ts +7 -2
- package/dist/cloudflare/event-store.js +8 -7
- package/dist/cloudflare/index.d.ts +2 -2
- package/dist/cloudflare/index.js +2 -2
- package/dist/cloudflare/router.js +13 -14
- package/dist/core/catalog.d.ts +3 -3
- package/dist/core/catalog.js +2 -5
- package/dist/core/coalesce.js +5 -3
- package/dist/core/compose.d.ts +2 -1
- package/dist/core/compose.js +18 -2
- package/dist/core/definition-store.d.ts +11 -0
- package/dist/core/definition-store.js +6 -0
- package/dist/core/effects.d.ts +127 -15
- package/dist/core/effects.js +89 -17
- package/dist/core/envelope.d.ts +27 -7
- package/dist/core/envelope.js +20 -6
- package/dist/core/event-codec.d.ts +4 -2
- package/dist/core/event-codec.js +10 -5
- package/dist/core/event-index.js +1 -3
- package/dist/core/fold.d.ts +3 -0
- package/dist/core/fold.js +171 -24
- package/dist/core/ids.d.ts +5 -0
- package/dist/core/ids.js +35 -2
- package/dist/core/index.d.ts +5 -1
- package/dist/core/index.js +5 -1
- package/dist/core/kind.d.ts +22 -0
- package/dist/core/kind.js +17 -0
- package/dist/core/limits.d.ts +34 -0
- package/dist/core/limits.js +46 -0
- package/dist/core/match.d.ts +1 -0
- package/dist/core/match.js +10 -1
- package/dist/core/module-scope.d.ts +22 -20
- package/dist/core/module-scope.js +38 -28
- package/dist/core/module.d.ts +16 -5
- package/dist/core/module.js +4 -0
- package/dist/core/projection.d.ts +13 -13
- package/dist/core/projection.js +9 -5
- package/dist/core/protocol.d.ts +68 -11
- package/dist/core/protocol.js +82 -11
- package/dist/core/replay.d.ts +312 -3
- package/dist/core/replay.js +11 -1
- package/dist/core/schema.d.ts +8 -19
- package/dist/core/schema.js +25 -80
- package/dist/core/serialize.js +2 -4
- package/dist/core/snapshot-store.d.ts +2 -1
- package/dist/core/snapshot-store.js +8 -2
- package/dist/core/snapshots.d.ts +5 -1
- package/dist/core/snapshots.js +10 -15
- package/dist/core/sqlite-store.js +56 -11
- package/dist/core/state.d.ts +142 -4
- package/dist/core/state.js +78 -0
- package/dist/core/store.d.ts +38 -3
- package/dist/core/store.js +77 -22
- package/dist/core/thread.d.ts +10 -9
- package/dist/core/types.d.ts +5 -8
- package/dist/core/types.js +28 -24
- package/dist/core/validate-event.js +14 -4
- package/dist/core/worker-callback.d.ts +17 -0
- package/dist/core/worker-callback.js +1 -0
- package/dist/debugger/components/event-stream.js +9 -10
- package/dist/debugger/events/protocol-summarize.js +26 -0
- package/dist/debugger/lib/event-meta.js +1 -2
- package/dist/evaluator/definitions-store.d.ts +11 -0
- package/dist/evaluator/definitions-store.js +8 -0
- package/dist/evaluator/definitions.d.ts +18 -0
- package/dist/evaluator/definitions.js +4 -0
- package/dist/evaluator/effects.d.ts +226 -0
- package/dist/evaluator/effects.js +132 -0
- package/dist/evaluator/evaluator.d.ts +24 -0
- package/dist/evaluator/evaluator.js +67 -0
- package/dist/evaluator/events.d.ts +59 -0
- package/dist/evaluator/events.js +21 -0
- package/dist/evaluator/index.d.ts +11 -0
- package/dist/evaluator/index.js +11 -0
- package/dist/evaluator/module.d.ts +606 -0
- package/dist/evaluator/module.js +25 -0
- package/dist/evaluator/projections.d.ts +50 -0
- package/dist/evaluator/projections.js +20 -0
- package/dist/evaluator/scope.d.ts +59 -0
- package/dist/evaluator/scope.js +7 -0
- package/dist/evaluator/signals.d.ts +3 -0
- package/dist/evaluator/signals.js +17 -0
- package/dist/evaluator/threads.d.ts +62 -0
- package/dist/evaluator/threads.js +117 -0
- package/dist/evaluator/types.d.ts +125 -0
- package/dist/evaluator/types.js +51 -0
- package/dist/projectors/delivery.d.ts +83 -0
- package/dist/projectors/delivery.js +182 -0
- package/dist/projectors/index-model.d.ts +1 -1
- package/dist/projectors/index-model.js +4 -6
- package/dist/projectors/index.d.ts +2 -0
- package/dist/projectors/index.js +2 -0
- package/dist/projectors/memory.d.ts +1 -1
- package/dist/projectors/memory.js +10 -8
- package/dist/projectors/postgres.js +44 -58
- package/dist/projectors/projector.d.ts +17 -0
- package/dist/projectors/projector.js +26 -1
- package/dist/projectors/sqlite-delivery.d.ts +4 -0
- package/dist/projectors/sqlite-delivery.js +147 -0
- package/dist/projectors/sqlite.js +23 -23
- package/dist/projectors/with-projectors.d.ts +7 -2
- package/dist/projectors/with-projectors.js +27 -28
- package/dist/react/notify.js +11 -8
- package/dist/react/react.d.ts +2 -3
- package/dist/react/react.js +14 -30
- package/dist/react/sse.d.ts +1 -1
- package/dist/react/sse.js +1 -1
- package/dist/react/store.d.ts +2 -2
- package/dist/react/store.js +109 -119
- package/dist/runtime/cancellation.d.ts +16 -0
- package/dist/runtime/cancellation.js +129 -0
- package/dist/runtime/deadline-transition-planner.d.ts +31 -0
- package/dist/runtime/deadline-transition-planner.js +139 -0
- package/dist/runtime/dispatch-effect.d.ts +3 -2
- package/dist/runtime/dispatch-effect.js +7 -13
- package/dist/runtime/errors.d.ts +51 -0
- package/dist/runtime/errors.js +39 -0
- package/dist/runtime/{runtime-helpers.d.ts → helpers.d.ts} +24 -4
- package/dist/runtime/{runtime-helpers.js → helpers.js} +56 -19
- package/dist/runtime/index.d.ts +3 -3
- package/dist/runtime/index.js +3 -3
- package/dist/runtime/ingress.d.ts +15 -0
- package/dist/runtime/ingress.js +42 -0
- package/dist/runtime/local-effect-executor.d.ts +21 -0
- package/dist/runtime/local-effect-executor.js +137 -0
- package/dist/runtime/looms.d.ts +22 -4
- package/dist/runtime/looms.js +54 -36
- package/dist/runtime/observer.d.ts +45 -0
- package/dist/runtime/observer.js +11 -0
- package/dist/runtime/repository.d.ts +55 -0
- package/dist/runtime/repository.js +278 -0
- package/dist/runtime/retry-policy.d.ts +16 -0
- package/dist/runtime/retry-policy.js +29 -0
- package/dist/runtime/run-execution-context.d.ts +30 -0
- package/dist/runtime/run-execution-context.js +56 -0
- package/dist/runtime/run-start.d.ts +4 -0
- package/dist/runtime/run-start.js +44 -0
- package/dist/runtime/runtime.d.ts +6 -87
- package/dist/runtime/runtime.js +233 -470
- package/dist/runtime/server/auth.d.ts +40 -0
- package/dist/runtime/server/auth.js +40 -0
- package/dist/runtime/server/fetch-handler.d.ts +14 -0
- package/dist/runtime/server/fetch-handler.js +51 -0
- package/dist/runtime/server/http.d.ts +37 -0
- package/dist/runtime/server/http.js +62 -0
- package/dist/runtime/server/route-table.d.ts +55 -0
- package/dist/runtime/server/route-table.js +74 -0
- package/dist/runtime/server/routes/effects.d.ts +3 -0
- package/dist/runtime/server/routes/effects.js +59 -0
- package/dist/runtime/server/routes/operations.d.ts +3 -0
- package/dist/runtime/server/routes/operations.js +14 -0
- package/dist/runtime/server/routes/runs.d.ts +3 -0
- package/dist/runtime/server/routes/runs.js +192 -0
- package/dist/runtime/server.d.ts +2 -8
- package/dist/runtime/server.js +4 -211
- package/dist/runtime/sse.d.ts +2 -1
- package/dist/runtime/sse.js +24 -17
- package/dist/runtime/types.d.ts +115 -0
- package/dist/runtime/types.js +1 -0
- package/dist/runtime/wake-engine.d.ts +20 -0
- package/dist/runtime/wake-engine.js +311 -0
- package/dist/runtime/wake-scheduler.d.ts +3 -4
- package/dist/runtime/wake-scheduler.js +25 -31
- package/dist/runtime/worker-callbacks.d.ts +16 -0
- package/dist/runtime/worker-callbacks.js +167 -0
- package/dist/runtime/worker-effect-dispatch.d.ts +36 -0
- package/dist/runtime/worker-effect-dispatch.js +126 -0
- package/dist/s2/config.d.ts +1 -1
- package/dist/s2/config.js +1 -1
- package/dist/s2/lite.js +70 -82
- package/dist/s2/pagination.d.ts +15 -0
- package/dist/s2/pagination.js +36 -0
- package/dist/s2/projector.js +6 -5
- package/dist/s2/snapshot-store.js +148 -163
- package/dist/s2/store.d.ts +3 -21
- package/dist/s2/store.js +71 -165
- package/dist/testing/index.d.ts +15 -0
- package/dist/testing/index.js +53 -16
- package/dist/workflow/definitions-store.d.ts +2 -3
- package/dist/workflow/definitions-store.js +2 -2
- package/dist/workflow/definitions.d.ts +78 -22
- package/dist/workflow/definitions.js +80 -14
- package/dist/workflow/effects.d.ts +453 -107
- package/dist/workflow/effects.js +214 -115
- package/dist/workflow/events.d.ts +149 -5
- package/dist/workflow/events.js +27 -5
- package/dist/workflow/graph.d.ts +56 -0
- package/dist/workflow/graph.js +207 -0
- package/dist/workflow/index.d.ts +1 -0
- package/dist/workflow/index.js +1 -0
- package/dist/workflow/module.d.ts +1047 -266
- package/dist/workflow/projections.d.ts +14 -12
- package/dist/workflow/projections.js +30 -8
- package/dist/workflow/scope.d.ts +146 -4
- package/dist/workflow/threads.d.ts +383 -155
- package/dist/workflow/threads.js +308 -110
- package/package.json +7 -2
- package/dist/runtime/events-api.d.ts +0 -4
- package/dist/runtime/events-api.js +0 -84
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* What a route can do. `public` routes (health) are never passed to `authorize`; `read` and
|
|
4
|
+
* `write` cover run state, `worker` covers external worker callbacks, and `operations` covers
|
|
5
|
+
* operator repair endpoints.
|
|
6
|
+
*/
|
|
7
|
+
export type RouteAccess = 'public' | 'read' | 'write' | 'worker' | 'operations';
|
|
8
|
+
export type AuthorizedAccess = Exclude<RouteAccess, 'public'>;
|
|
9
|
+
export interface RouteInfo {
|
|
10
|
+
readonly access: AuthorizedAccess;
|
|
11
|
+
/** Stable route name, such as `runs.signal` or `effects.retry`. */
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly runId?: string;
|
|
14
|
+
}
|
|
15
|
+
export type AuthDecision = {
|
|
16
|
+
readonly allowed: true;
|
|
17
|
+
} | {
|
|
18
|
+
readonly allowed: false;
|
|
19
|
+
readonly status: number;
|
|
20
|
+
readonly error: string;
|
|
21
|
+
};
|
|
22
|
+
export type Authorize = (request: Request, route: RouteInfo) => Effect.Effect<AuthDecision>;
|
|
23
|
+
export declare const authAllowed: AuthDecision;
|
|
24
|
+
export declare function authDenied(status: number, error: string): AuthDecision;
|
|
25
|
+
/**
|
|
26
|
+
* Used when a host supplies no `authorize`: run reads and writes stay open, while worker callbacks
|
|
27
|
+
* and operational endpoints are unavailable until a host opts in.
|
|
28
|
+
*/
|
|
29
|
+
export declare const defaultAuthorize: Authorize;
|
|
30
|
+
export interface BearerTokens {
|
|
31
|
+
readonly read?: string;
|
|
32
|
+
readonly write?: string;
|
|
33
|
+
readonly worker?: string;
|
|
34
|
+
readonly operations?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Requires `Authorization: Bearer <token>` for every access level that has a token. Levels without
|
|
38
|
+
* a token keep the default behavior: reads and writes are open, worker and operations routes 503.
|
|
39
|
+
*/
|
|
40
|
+
export declare function bearerAuth(tokens: BearerTokens): Authorize;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
export const authAllowed = { allowed: true };
|
|
3
|
+
export function authDenied(status, error) {
|
|
4
|
+
return { allowed: false, status, error };
|
|
5
|
+
}
|
|
6
|
+
const unauthorized = authDenied(401, 'Unauthorized');
|
|
7
|
+
function notConfigured(access) {
|
|
8
|
+
return authDenied(503, access === 'worker'
|
|
9
|
+
? 'Worker callbacks are not configured'
|
|
10
|
+
: 'Operational endpoints are not configured');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Used when a host supplies no `authorize`: run reads and writes stay open, while worker callbacks
|
|
14
|
+
* and operational endpoints are unavailable until a host opts in.
|
|
15
|
+
*/
|
|
16
|
+
export const defaultAuthorize = (_request, route) => Effect.succeed(route.access === 'worker' || route.access === 'operations'
|
|
17
|
+
? notConfigured(route.access)
|
|
18
|
+
: authAllowed);
|
|
19
|
+
function constantTimeEquals(actual, expected) {
|
|
20
|
+
const length = Math.max(actual.length, expected.length);
|
|
21
|
+
let difference = actual.length ^ expected.length;
|
|
22
|
+
for (let index = 0; index < length; index += 1) {
|
|
23
|
+
difference |= (actual.charCodeAt(index) || 0) ^ (expected.charCodeAt(index) || 0);
|
|
24
|
+
}
|
|
25
|
+
return difference === 0;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Requires `Authorization: Bearer <token>` for every access level that has a token. Levels without
|
|
29
|
+
* a token keep the default behavior: reads and writes are open, worker and operations routes 503.
|
|
30
|
+
*/
|
|
31
|
+
export function bearerAuth(tokens) {
|
|
32
|
+
return (request, route) => {
|
|
33
|
+
const token = tokens[route.access];
|
|
34
|
+
if (token === undefined) {
|
|
35
|
+
return defaultAuthorize(request, route);
|
|
36
|
+
}
|
|
37
|
+
const header = request.headers.get('authorization') ?? '';
|
|
38
|
+
return Effect.succeed(constantTimeEquals(header, `Bearer ${token}`) ? authAllowed : unauthorized);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EventStore } from '@swirls/looms/core';
|
|
2
|
+
import type { LoomsRuntime } from '../runtime';
|
|
3
|
+
import { type Authorize } from './auth';
|
|
4
|
+
export interface FetchHandlerOptions {
|
|
5
|
+
readonly runtime: LoomsRuntime;
|
|
6
|
+
readonly store: EventStore;
|
|
7
|
+
/**
|
|
8
|
+
* Decides whether a request may use a route. Defaults to open reads and writes with worker and
|
|
9
|
+
* operations routes unavailable; use `bearerAuth` or your own policy to change that.
|
|
10
|
+
*/
|
|
11
|
+
readonly authorize?: Authorize;
|
|
12
|
+
}
|
|
13
|
+
export declare function isLoomsApiPath(path: string): boolean;
|
|
14
|
+
export declare function createFetchHandler(options: FetchHandlerOptions): (req: Request) => Promise<Response | null>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Effect, Predicate } from 'effect';
|
|
2
|
+
import { defaultAuthorize } from './auth.js';
|
|
3
|
+
import { acceptsEventStream, toErrorResponse } from './http.js';
|
|
4
|
+
import { matchRoute } from './route-table.js';
|
|
5
|
+
import { effectRoutes } from './routes/effects.js';
|
|
6
|
+
import { operationRoutes } from './routes/operations.js';
|
|
7
|
+
import { runRoutes } from './routes/runs.js';
|
|
8
|
+
function createRoutes(options) {
|
|
9
|
+
return [...runRoutes(options), ...effectRoutes(options), ...operationRoutes(options)];
|
|
10
|
+
}
|
|
11
|
+
function authorizeRoute(req, matched, authorize) {
|
|
12
|
+
const { access, name } = matched.route;
|
|
13
|
+
if (access === 'public') {
|
|
14
|
+
return Effect.succeed(null);
|
|
15
|
+
}
|
|
16
|
+
const runId = matched.params.runId;
|
|
17
|
+
return authorize(req, Predicate.isString(runId) ? { access, name, runId } : { access, name }).pipe(Effect.map((decision) => decision.allowed
|
|
18
|
+
? null
|
|
19
|
+
: Response.json({ error: decision.error }, { status: decision.status })));
|
|
20
|
+
}
|
|
21
|
+
export function isLoomsApiPath(path) {
|
|
22
|
+
return (path === '/health' ||
|
|
23
|
+
path === '/runs' ||
|
|
24
|
+
path.startsWith('/runs/') ||
|
|
25
|
+
path.startsWith('/operations/'));
|
|
26
|
+
}
|
|
27
|
+
export function createFetchHandler(options) {
|
|
28
|
+
const routes = createRoutes(options);
|
|
29
|
+
const authorize = options.authorize ?? defaultAuthorize;
|
|
30
|
+
return (req) => {
|
|
31
|
+
const url = new URL(req.url);
|
|
32
|
+
if (!isLoomsApiPath(url.pathname)) {
|
|
33
|
+
return Promise.resolve(null);
|
|
34
|
+
}
|
|
35
|
+
const accept = req.headers.get('accept') ?? '';
|
|
36
|
+
if (req.method === 'GET' && accept.includes('text/html') && !acceptsEventStream(req)) {
|
|
37
|
+
return Promise.resolve(null);
|
|
38
|
+
}
|
|
39
|
+
return Effect.runPromise(Effect.gen(function* () {
|
|
40
|
+
const matched = matchRoute(routes, req.method, url.pathname);
|
|
41
|
+
if (!matched) {
|
|
42
|
+
return new Response('Not Found', { status: 404 });
|
|
43
|
+
}
|
|
44
|
+
const denied = yield* authorizeRoute(req, matched, authorize);
|
|
45
|
+
if (denied) {
|
|
46
|
+
return denied;
|
|
47
|
+
}
|
|
48
|
+
return yield* matched.route.handle({ req, url, params: matched.params });
|
|
49
|
+
}).pipe(Effect.catch((error) => Effect.succeed(toErrorResponse(error))), Effect.catchCause((cause) => Effect.logError('[@swirls/looms/runtime] request failed', cause).pipe(Effect.as(Response.json({ error: 'Internal server error' }, { status: 500 }))))));
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Effect, Schema } from 'effect';
|
|
2
|
+
import { EventStoreTag, type EventStore, type JsonValue } from '@swirls/looms/core';
|
|
3
|
+
import { type LoomsRuntime } from '../runtime';
|
|
4
|
+
export interface RouteOptions {
|
|
5
|
+
readonly runtime: LoomsRuntime;
|
|
6
|
+
readonly store: EventStore;
|
|
7
|
+
}
|
|
8
|
+
declare const RequestBodyError_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 & {
|
|
9
|
+
readonly _tag: "RequestBodyError";
|
|
10
|
+
} & Readonly<A>;
|
|
11
|
+
export declare class RequestBodyError extends RequestBodyError_base<{
|
|
12
|
+
readonly cause: unknown;
|
|
13
|
+
readonly message: string;
|
|
14
|
+
}> {
|
|
15
|
+
constructor(cause: unknown);
|
|
16
|
+
}
|
|
17
|
+
declare const RequestQueryError_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 & {
|
|
18
|
+
readonly _tag: "RequestQueryError";
|
|
19
|
+
} & Readonly<A>;
|
|
20
|
+
export declare class RequestQueryError extends RequestQueryError_base<{
|
|
21
|
+
readonly message: string;
|
|
22
|
+
}> {
|
|
23
|
+
}
|
|
24
|
+
export declare const EventInputBodySchema: Schema.Struct<{
|
|
25
|
+
readonly type: Schema.String;
|
|
26
|
+
readonly payload: Schema.Codec<Schema.Json, Schema.Json, never, never>;
|
|
27
|
+
readonly threadId: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
28
|
+
}>;
|
|
29
|
+
export declare function provideStore<A, E>(effect: Effect.Effect<A, E, EventStoreTag>, store: EventStore): Effect.Effect<A, E>;
|
|
30
|
+
export declare const readJson: (req: Request) => Effect.Effect<JsonValue, RequestBodyError>;
|
|
31
|
+
export declare function extractIdempotencyKey(req: Request, bodyKey?: string): string | undefined;
|
|
32
|
+
export declare function acceptsEventStream(req: Request): boolean;
|
|
33
|
+
export declare function readIntegerQuery(url: URL, name: string, options: {
|
|
34
|
+
minimum: number;
|
|
35
|
+
}): Effect.Effect<number | null, RequestQueryError>;
|
|
36
|
+
export declare function toErrorResponse(err: Error): Response;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Data, Effect, Schema } from 'effect';
|
|
2
|
+
import { EventStoreTag, InvalidEventError, InvalidInputError, JsonValueSchema, } from '@swirls/looms/core';
|
|
3
|
+
import { RunOverloadedError, StartRunConflictError } from '../runtime.js';
|
|
4
|
+
export class RequestBodyError extends Data.TaggedError('RequestBodyError') {
|
|
5
|
+
constructor(cause) {
|
|
6
|
+
super({ cause, message: 'Invalid JSON request body' });
|
|
7
|
+
this.name = 'RequestBodyError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class RequestQueryError extends Data.TaggedError('RequestQueryError') {
|
|
11
|
+
}
|
|
12
|
+
export const EventInputBodySchema = Schema.Struct({
|
|
13
|
+
type: Schema.String,
|
|
14
|
+
payload: JsonValueSchema,
|
|
15
|
+
threadId: Schema.optional(Schema.NullOr(Schema.String)),
|
|
16
|
+
});
|
|
17
|
+
export function provideStore(effect, store) {
|
|
18
|
+
return Effect.provideService(effect, EventStoreTag, store);
|
|
19
|
+
}
|
|
20
|
+
export const readJson = (req) => Effect.tryPromise({
|
|
21
|
+
try: () => req.json(),
|
|
22
|
+
catch: (cause) => new RequestBodyError(cause),
|
|
23
|
+
}).pipe(Effect.flatMap(Schema.decodeUnknownEffect(JsonValueSchema)), Effect.mapError((cause) => cause instanceof RequestBodyError ? cause : new RequestBodyError(cause)));
|
|
24
|
+
export function extractIdempotencyKey(req, bodyKey) {
|
|
25
|
+
return (bodyKey ??
|
|
26
|
+
req.headers.get('idempotency-key') ??
|
|
27
|
+
req.headers.get('x-idempotency-key') ??
|
|
28
|
+
undefined);
|
|
29
|
+
}
|
|
30
|
+
export function acceptsEventStream(req) {
|
|
31
|
+
return (req.headers.get('accept') ?? '')
|
|
32
|
+
.split(',')
|
|
33
|
+
.some((value) => value.split(';', 1)[0]?.trim().toLowerCase() === 'text/event-stream');
|
|
34
|
+
}
|
|
35
|
+
export function readIntegerQuery(url, name, options) {
|
|
36
|
+
const raw = url.searchParams.get(name);
|
|
37
|
+
if (raw === null) {
|
|
38
|
+
return Effect.succeed(null);
|
|
39
|
+
}
|
|
40
|
+
const value = Number(raw);
|
|
41
|
+
if (!Number.isSafeInteger(value) || value < options.minimum || raw.trim() === '') {
|
|
42
|
+
return Effect.fail(new RequestQueryError({
|
|
43
|
+
message: `${name} must be an integer greater than or equal to ${options.minimum}`,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
return Effect.succeed(value);
|
|
47
|
+
}
|
|
48
|
+
export function toErrorResponse(err) {
|
|
49
|
+
if (err instanceof RequestBodyError || err instanceof RequestQueryError) {
|
|
50
|
+
return Response.json({ error: err.message }, { status: 400 });
|
|
51
|
+
}
|
|
52
|
+
if (err instanceof InvalidInputError || err instanceof InvalidEventError) {
|
|
53
|
+
return Response.json({ error: err.message, issues: err.issues }, { status: 400 });
|
|
54
|
+
}
|
|
55
|
+
if (err instanceof RunOverloadedError) {
|
|
56
|
+
return Response.json({ error: err.message }, { status: 429 });
|
|
57
|
+
}
|
|
58
|
+
if (err instanceof StartRunConflictError) {
|
|
59
|
+
return Response.json({ error: err.message, mismatches: err.mismatches }, { status: 409 });
|
|
60
|
+
}
|
|
61
|
+
return Response.json({ error: err.message }, { status: 500 });
|
|
62
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Effect } from 'effect';
|
|
2
|
+
import type { RouteAccess } from './auth';
|
|
3
|
+
export type ParamValue = string | number;
|
|
4
|
+
/** Decodes one path segment, or returns null when the segment does not match. */
|
|
5
|
+
export interface ParamDecoder<T extends ParamValue> {
|
|
6
|
+
decode(raw: string): T | null;
|
|
7
|
+
}
|
|
8
|
+
export declare const text: ParamDecoder<string>;
|
|
9
|
+
export declare const integer: ParamDecoder<number>;
|
|
10
|
+
export declare function oneOf<const T extends string>(values: readonly T[]): ParamDecoder<T>;
|
|
11
|
+
type PathParamNames<TPath extends string> = TPath extends `${string}:${infer Param}/${infer Rest}` ? Param | PathParamNames<`/${Rest}`> : TPath extends `${string}:${infer Param}` ? Param : never;
|
|
12
|
+
type ParamDecoders<TPath extends string> = {
|
|
13
|
+
readonly [K in PathParamNames<TPath>]?: ParamDecoder<ParamValue>;
|
|
14
|
+
};
|
|
15
|
+
type DecodedParams<TPath extends string, TDecoders> = {
|
|
16
|
+
readonly [K in PathParamNames<TPath>]: K extends keyof TDecoders ? TDecoders[K] extends ParamDecoder<infer T> ? T : string : string;
|
|
17
|
+
};
|
|
18
|
+
export interface RouteContext<TParams> {
|
|
19
|
+
readonly req: Request;
|
|
20
|
+
readonly url: URL;
|
|
21
|
+
readonly params: TParams;
|
|
22
|
+
}
|
|
23
|
+
export interface RouteDefinition<TPath extends string, TDecoders extends ParamDecoders<TPath>> {
|
|
24
|
+
readonly method: 'GET' | 'POST';
|
|
25
|
+
/** Path pattern such as `/runs/:runId/effects/:effectId`; parameters decode as text by default. */
|
|
26
|
+
readonly path: TPath;
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly access: RouteAccess;
|
|
29
|
+
readonly params?: TDecoders;
|
|
30
|
+
handle(context: RouteContext<DecodedParams<TPath, TDecoders>>): Effect.Effect<Response, Error>;
|
|
31
|
+
}
|
|
32
|
+
type Segment = {
|
|
33
|
+
readonly kind: 'literal';
|
|
34
|
+
readonly value: string;
|
|
35
|
+
} | {
|
|
36
|
+
readonly kind: 'param';
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly decoder: ParamDecoder<ParamValue>;
|
|
39
|
+
};
|
|
40
|
+
export type RouteParams = Readonly<Record<string, ParamValue>>;
|
|
41
|
+
export interface Route {
|
|
42
|
+
readonly method: 'GET' | 'POST';
|
|
43
|
+
readonly name: string;
|
|
44
|
+
readonly access: RouteAccess;
|
|
45
|
+
readonly segments: readonly Segment[];
|
|
46
|
+
handle(context: RouteContext<RouteParams>): Effect.Effect<Response, Error>;
|
|
47
|
+
}
|
|
48
|
+
export declare function route<const TPath extends string, const TDecoders extends ParamDecoders<TPath> = Record<never, never>>(definition: RouteDefinition<TPath, TDecoders>): Route;
|
|
49
|
+
export interface MatchedRoute {
|
|
50
|
+
readonly route: Route;
|
|
51
|
+
readonly params: RouteParams;
|
|
52
|
+
}
|
|
53
|
+
/** First route, in table order, whose method and path match. */
|
|
54
|
+
export declare function matchRoute(routes: readonly Route[], method: string, pathname: string): MatchedRoute | null;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export const text = {
|
|
2
|
+
decode: (raw) => {
|
|
3
|
+
try {
|
|
4
|
+
return decodeURIComponent(raw);
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
export const integer = {
|
|
12
|
+
decode: (raw) => (/^\d+$/.test(raw) ? Number(raw) : null),
|
|
13
|
+
};
|
|
14
|
+
export function oneOf(values) {
|
|
15
|
+
return { decode: (raw) => values.find((value) => value === raw) ?? null };
|
|
16
|
+
}
|
|
17
|
+
export function route(definition) {
|
|
18
|
+
const decoders = definition.params ?? {};
|
|
19
|
+
const segments = definition.path
|
|
20
|
+
.split('/')
|
|
21
|
+
.slice(1)
|
|
22
|
+
.map((part) => {
|
|
23
|
+
if (!part.startsWith(':')) {
|
|
24
|
+
return { kind: 'literal', value: part };
|
|
25
|
+
}
|
|
26
|
+
const name = part.slice(1);
|
|
27
|
+
return { kind: 'param', name, decoder: decoders[name] ?? text };
|
|
28
|
+
});
|
|
29
|
+
// SAFETY: matchRoute only calls a route with every path parameter decoded by its decoders.
|
|
30
|
+
return {
|
|
31
|
+
method: definition.method,
|
|
32
|
+
name: definition.name,
|
|
33
|
+
access: definition.access,
|
|
34
|
+
segments,
|
|
35
|
+
handle: (context) =>
|
|
36
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
37
|
+
definition.handle(context),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function matchSegments(segments, parts) {
|
|
41
|
+
if (segments.length !== parts.length) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const params = {};
|
|
45
|
+
for (const [index, segment] of segments.entries()) {
|
|
46
|
+
const part = parts[index];
|
|
47
|
+
if (segment.kind === 'literal') {
|
|
48
|
+
if (segment.value !== part) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const value = segment.decoder.decode(part);
|
|
54
|
+
if (value === null) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
params[segment.name] = value;
|
|
58
|
+
}
|
|
59
|
+
return params;
|
|
60
|
+
}
|
|
61
|
+
/** First route, in table order, whose method and path match. */
|
|
62
|
+
export function matchRoute(routes, method, pathname) {
|
|
63
|
+
const parts = pathname.split('/').slice(1);
|
|
64
|
+
for (const candidate of routes) {
|
|
65
|
+
if (candidate.method !== method) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const params = matchSegments(candidate.segments, parts);
|
|
69
|
+
if (params) {
|
|
70
|
+
return { route: candidate, params };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Effect, Schema } from 'effect';
|
|
2
|
+
import { EventInputBodySchema, provideStore, readJson } from '../http.js';
|
|
3
|
+
import { integer, oneOf, route } from '../route-table.js';
|
|
4
|
+
const WORKER_CALLBACK_KINDS = [
|
|
5
|
+
'started',
|
|
6
|
+
'heartbeat',
|
|
7
|
+
'complete',
|
|
8
|
+
'fail',
|
|
9
|
+
'cancelled',
|
|
10
|
+
];
|
|
11
|
+
const WorkerCallbackBodySchema = Schema.Struct({
|
|
12
|
+
events: Schema.optional(Schema.Array(EventInputBodySchema)),
|
|
13
|
+
error: Schema.optional(Schema.String),
|
|
14
|
+
});
|
|
15
|
+
function workerCallbackInput(params, body) {
|
|
16
|
+
const callback = { effectId: params.effectId, attempt: params.attempt };
|
|
17
|
+
switch (params.action) {
|
|
18
|
+
case 'complete':
|
|
19
|
+
return { ...callback, kind: 'complete', events: body.events ?? [] };
|
|
20
|
+
case 'fail':
|
|
21
|
+
return body.error ? { ...callback, kind: 'fail', error: body.error } : null;
|
|
22
|
+
default:
|
|
23
|
+
return { ...callback, kind: params.action };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function effectRoutes({ runtime, store }) {
|
|
27
|
+
return [
|
|
28
|
+
route({
|
|
29
|
+
method: 'GET',
|
|
30
|
+
path: '/runs/:runId/effects',
|
|
31
|
+
name: 'effects.list',
|
|
32
|
+
access: 'read',
|
|
33
|
+
handle: ({ params }) => provideStore(runtime.inspectRun(params.runId), store).pipe(Effect.map((status) => Response.json({ runId: params.runId, effects: status.effects }))),
|
|
34
|
+
}),
|
|
35
|
+
route({
|
|
36
|
+
method: 'POST',
|
|
37
|
+
path: '/runs/:runId/effects/:effectId/retry',
|
|
38
|
+
name: 'effects.retry',
|
|
39
|
+
access: 'operations',
|
|
40
|
+
handle: ({ params }) => provideStore(runtime.retryEffect(params.runId, params.effectId), store).pipe(Effect.map((state) => Response.json({ runId: params.runId, effectId: params.effectId, state }))),
|
|
41
|
+
}),
|
|
42
|
+
route({
|
|
43
|
+
method: 'POST',
|
|
44
|
+
path: '/runs/:runId/effects/:effectId/:attempt/:action',
|
|
45
|
+
name: 'effects.workerCallback',
|
|
46
|
+
access: 'worker',
|
|
47
|
+
params: { attempt: integer, action: oneOf(WORKER_CALLBACK_KINDS) },
|
|
48
|
+
handle: ({ req, params }) => Effect.gen(function* () {
|
|
49
|
+
const body = yield* readJson(req).pipe(Effect.flatMap(Schema.decodeUnknownEffect(WorkerCallbackBodySchema)));
|
|
50
|
+
const input = workerCallbackInput(params, body);
|
|
51
|
+
if (!input) {
|
|
52
|
+
return Response.json({ error: 'error required' }, { status: 400 });
|
|
53
|
+
}
|
|
54
|
+
const state = yield* provideStore(runtime.workerCallback(params.runId, input), store);
|
|
55
|
+
return Response.json({ runId: params.runId, state });
|
|
56
|
+
}),
|
|
57
|
+
}),
|
|
58
|
+
];
|
|
59
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { provideStore } from '../http.js';
|
|
3
|
+
import { route } from '../route-table.js';
|
|
4
|
+
export function operationRoutes({ runtime, store }) {
|
|
5
|
+
return [
|
|
6
|
+
route({
|
|
7
|
+
method: 'POST',
|
|
8
|
+
path: '/operations/deadlines/rescan',
|
|
9
|
+
name: 'operations.rescanDeadlines',
|
|
10
|
+
access: 'operations',
|
|
11
|
+
handle: () => provideStore(runtime.recoverDeadlines, store).pipe(Effect.map((scheduled) => Response.json({ scheduled }))),
|
|
12
|
+
}),
|
|
13
|
+
];
|
|
14
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Effect, Schema } from 'effect';
|
|
2
|
+
import { createRunId, EventStoreTag, JsonValueSchema, stringifyJson, } from '@swirls/looms/core';
|
|
3
|
+
import { createEventStreamResponse } from '../../sse.js';
|
|
4
|
+
import { acceptsEventStream, EventInputBodySchema, extractIdempotencyKey, provideStore, readIntegerQuery, readJson, RequestQueryError, } from '../http.js';
|
|
5
|
+
import { route } from '../route-table.js';
|
|
6
|
+
const StartRunBodySchema = Schema.Struct({
|
|
7
|
+
kind: Schema.optional(Schema.String),
|
|
8
|
+
definitionName: Schema.optional(Schema.String),
|
|
9
|
+
definitionVersion: Schema.optional(Schema.String),
|
|
10
|
+
name: Schema.optional(Schema.String),
|
|
11
|
+
input: Schema.optional(JsonValueSchema),
|
|
12
|
+
runId: Schema.optional(Schema.String),
|
|
13
|
+
idempotencyKey: Schema.optional(Schema.String),
|
|
14
|
+
});
|
|
15
|
+
const SignalBodySchema = Schema.Struct({
|
|
16
|
+
events: Schema.optional(Schema.Array(EventInputBodySchema)),
|
|
17
|
+
type: Schema.optional(Schema.String),
|
|
18
|
+
payload: Schema.optional(JsonValueSchema),
|
|
19
|
+
threadId: Schema.optional(Schema.NullOr(Schema.String)),
|
|
20
|
+
idempotencyKey: Schema.optional(Schema.String),
|
|
21
|
+
});
|
|
22
|
+
function wantsRunStream(req, url) {
|
|
23
|
+
return url.searchParams.get('stream') === 'true' || acceptsEventStream(req);
|
|
24
|
+
}
|
|
25
|
+
function wantsEventStream(req, url) {
|
|
26
|
+
const live = url.searchParams.get('live');
|
|
27
|
+
return live === 'true' || live === '1' || acceptsEventStream(req);
|
|
28
|
+
}
|
|
29
|
+
function readEventStreamFromSeq(req, url) {
|
|
30
|
+
const lastEventId = req.headers.get('last-event-id');
|
|
31
|
+
if (lastEventId !== null && lastEventId.trim() !== '') {
|
|
32
|
+
const value = Number(lastEventId);
|
|
33
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
34
|
+
return Effect.fail(new RequestQueryError({
|
|
35
|
+
message: 'Last-Event-ID must be a non-negative integer',
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
return Effect.succeed(value + 1);
|
|
39
|
+
}
|
|
40
|
+
return readIntegerQuery(url, 'fromSeq', { minimum: 1 }).pipe(Effect.map((fromSeq) => fromSeq ?? 1));
|
|
41
|
+
}
|
|
42
|
+
function streamStartRun(req, runtime, store, args) {
|
|
43
|
+
return createEventStreamResponse({
|
|
44
|
+
signal: req.signal,
|
|
45
|
+
request: req,
|
|
46
|
+
store,
|
|
47
|
+
runId: args.runId,
|
|
48
|
+
fromSeq: 1,
|
|
49
|
+
onStart: (write, close) => runtime
|
|
50
|
+
.startRun({
|
|
51
|
+
kind: args.kind,
|
|
52
|
+
definitionName: args.definitionName,
|
|
53
|
+
definitionVersion: args.definitionVersion,
|
|
54
|
+
input: args.input,
|
|
55
|
+
runId: args.runId,
|
|
56
|
+
idempotencyKey: args.idempotencyKey,
|
|
57
|
+
})
|
|
58
|
+
.pipe(Effect.provideService(EventStoreTag, store), Effect.tap((result) => Effect.sync(() => write(`event: done\ndata: ${stringifyJson(result)}\n\n`))), Effect.catch((error) => Effect.sync(() => write(`event: error\ndata: ${stringifyJson({ error: error.message })}\n\n`))), Effect.ensuring(Effect.sync(close)), Effect.asVoid),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export function runRoutes({ runtime, store }) {
|
|
62
|
+
return [
|
|
63
|
+
route({
|
|
64
|
+
method: 'GET',
|
|
65
|
+
path: '/health',
|
|
66
|
+
name: 'health',
|
|
67
|
+
access: 'public',
|
|
68
|
+
handle: () => Effect.succeed(Response.json({ ok: true })),
|
|
69
|
+
}),
|
|
70
|
+
route({
|
|
71
|
+
method: 'GET',
|
|
72
|
+
path: '/runs',
|
|
73
|
+
name: 'runs.list',
|
|
74
|
+
access: 'read',
|
|
75
|
+
handle: () => provideStore(runtime.listRuns, store).pipe(Effect.map((runIds) => Response.json({ runIds }))),
|
|
76
|
+
}),
|
|
77
|
+
route({
|
|
78
|
+
method: 'POST',
|
|
79
|
+
path: '/runs',
|
|
80
|
+
name: 'runs.start',
|
|
81
|
+
access: 'write',
|
|
82
|
+
handle: ({ req, url }) => Effect.gen(function* () {
|
|
83
|
+
const body = yield* readJson(req).pipe(Effect.flatMap(Schema.decodeUnknownEffect(StartRunBodySchema)));
|
|
84
|
+
const definitionName = body.definitionName ?? body.name;
|
|
85
|
+
const kind = body.kind;
|
|
86
|
+
if (!definitionName || !kind) {
|
|
87
|
+
return Response.json({ error: 'kind and definitionName required' }, { status: 400 });
|
|
88
|
+
}
|
|
89
|
+
const runId = body.runId ?? createRunId();
|
|
90
|
+
const idempotencyKey = extractIdempotencyKey(req, body.idempotencyKey);
|
|
91
|
+
const args = {
|
|
92
|
+
kind,
|
|
93
|
+
definitionName,
|
|
94
|
+
definitionVersion: body.definitionVersion,
|
|
95
|
+
input: body.input ?? null,
|
|
96
|
+
runId,
|
|
97
|
+
idempotencyKey,
|
|
98
|
+
};
|
|
99
|
+
if (wantsRunStream(req, url)) {
|
|
100
|
+
return streamStartRun(req, runtime, store, args);
|
|
101
|
+
}
|
|
102
|
+
const result = yield* provideStore(runtime.startRun(args), store);
|
|
103
|
+
return Response.json(result);
|
|
104
|
+
}),
|
|
105
|
+
}),
|
|
106
|
+
route({
|
|
107
|
+
method: 'GET',
|
|
108
|
+
path: '/runs/:runId/replay',
|
|
109
|
+
name: 'runs.replay',
|
|
110
|
+
access: 'read',
|
|
111
|
+
handle: ({ url, params }) => provideStore(runtime.replayTo(params.runId, Number(url.searchParams.get('seq') ?? '0')), store).pipe(Effect.map((step) => Response.json({ runId: params.runId, step }))),
|
|
112
|
+
}),
|
|
113
|
+
route({
|
|
114
|
+
method: 'GET',
|
|
115
|
+
path: '/runs/:runId/status',
|
|
116
|
+
name: 'runs.status',
|
|
117
|
+
access: 'read',
|
|
118
|
+
handle: ({ params }) => provideStore(runtime.inspectRun(params.runId), store).pipe(Effect.map((status) => Response.json(status))),
|
|
119
|
+
}),
|
|
120
|
+
route({
|
|
121
|
+
method: 'GET',
|
|
122
|
+
path: '/runs/:runId/projections/:projectionName',
|
|
123
|
+
name: 'runs.projection',
|
|
124
|
+
access: 'read',
|
|
125
|
+
handle: ({ params }) => {
|
|
126
|
+
const definition = runtime.registry.projections.get(params.projectionName);
|
|
127
|
+
if (!definition) {
|
|
128
|
+
return Effect.succeed(Response.json({ error: `Unknown projection: ${params.projectionName}` }, { status: 404 }));
|
|
129
|
+
}
|
|
130
|
+
return provideStore(runtime.project(params.runId, definition), store).pipe(Effect.map((value) => Response.json({ runId: params.runId, name: params.projectionName, value })));
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
route({
|
|
134
|
+
method: 'GET',
|
|
135
|
+
path: '/runs/:runId/events',
|
|
136
|
+
name: 'runs.events',
|
|
137
|
+
access: 'read',
|
|
138
|
+
handle: ({ req, url, params }) => Effect.gen(function* () {
|
|
139
|
+
if (wantsEventStream(req, url)) {
|
|
140
|
+
return createEventStreamResponse({
|
|
141
|
+
signal: req.signal,
|
|
142
|
+
request: req,
|
|
143
|
+
store,
|
|
144
|
+
runId: params.runId,
|
|
145
|
+
fromSeq: yield* readEventStreamFromSeq(req, url),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const fromSeq = (yield* readIntegerQuery(url, 'fromSeq', { minimum: 1 })) ?? undefined;
|
|
149
|
+
const limit = (yield* readIntegerQuery(url, 'limit', { minimum: 1 })) ?? undefined;
|
|
150
|
+
const events = yield* provideStore(runtime.getEvents(params.runId, { fromSeq, limit }), store);
|
|
151
|
+
return Response.json({ runId: params.runId, events }, { headers: { vary: 'accept' } });
|
|
152
|
+
}),
|
|
153
|
+
}),
|
|
154
|
+
route({
|
|
155
|
+
method: 'GET',
|
|
156
|
+
path: '/runs/:runId/threads',
|
|
157
|
+
name: 'runs.threads',
|
|
158
|
+
access: 'read',
|
|
159
|
+
handle: ({ params }) => provideStore(runtime.getRun(params.runId), store).pipe(Effect.map((state) => Response.json({ runId: params.runId, threads: state.threads }))),
|
|
160
|
+
}),
|
|
161
|
+
route({
|
|
162
|
+
method: 'GET',
|
|
163
|
+
path: '/runs/:runId',
|
|
164
|
+
name: 'runs.get',
|
|
165
|
+
access: 'read',
|
|
166
|
+
handle: ({ params }) => provideStore(runtime.getRun(params.runId), store).pipe(Effect.map((state) => Response.json({ runId: params.runId, state }))),
|
|
167
|
+
}),
|
|
168
|
+
route({
|
|
169
|
+
method: 'POST',
|
|
170
|
+
path: '/runs/:runId/events',
|
|
171
|
+
name: 'runs.signal',
|
|
172
|
+
access: 'write',
|
|
173
|
+
handle: ({ req, params }) => Effect.gen(function* () {
|
|
174
|
+
const body = yield* readJson(req).pipe(Effect.flatMap(Schema.decodeUnknownEffect(SignalBodySchema)));
|
|
175
|
+
const events = body.events ??
|
|
176
|
+
(body.type
|
|
177
|
+
? [{ type: body.type, payload: body.payload ?? null, threadId: body.threadId }]
|
|
178
|
+
: []);
|
|
179
|
+
const idempotencyKey = extractIdempotencyKey(req, body.idempotencyKey);
|
|
180
|
+
const state = yield* provideStore(runtime.signal(params.runId, events, { idempotencyKey }), store);
|
|
181
|
+
return Response.json({ runId: params.runId, state });
|
|
182
|
+
}),
|
|
183
|
+
}),
|
|
184
|
+
route({
|
|
185
|
+
method: 'POST',
|
|
186
|
+
path: '/runs/:runId/wake',
|
|
187
|
+
name: 'runs.wake',
|
|
188
|
+
access: 'write',
|
|
189
|
+
handle: ({ params }) => provideStore(runtime.wake(params.runId), store).pipe(Effect.map((state) => Response.json({ runId: params.runId, state }))),
|
|
190
|
+
}),
|
|
191
|
+
];
|
|
192
|
+
}
|