@swirls/looms 0.0.1
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/LICENSE +190 -0
- package/README.md +9 -0
- package/dist/actor/cell.d.ts +27 -0
- package/dist/actor/cell.js +41 -0
- package/dist/actor/host.d.ts +27 -0
- package/dist/actor/host.js +82 -0
- package/dist/actor/index.d.ts +3 -0
- package/dist/actor/index.js +3 -0
- package/dist/actor/routing.d.ts +15 -0
- package/dist/actor/routing.js +80 -0
- package/dist/agent/definitions-store.d.ts +11 -0
- package/dist/agent/definitions-store.js +10 -0
- package/dist/agent/definitions.d.ts +152 -0
- package/dist/agent/definitions.js +74 -0
- package/dist/agent/effects.d.ts +425 -0
- package/dist/agent/effects.js +318 -0
- package/dist/agent/events.d.ts +170 -0
- package/dist/agent/events.js +61 -0
- package/dist/agent/index.d.ts +13 -0
- package/dist/agent/index.js +13 -0
- package/dist/agent/llm.d.ts +35 -0
- package/dist/agent/llm.js +48 -0
- package/dist/agent/module.d.ts +1037 -0
- package/dist/agent/module.js +27 -0
- package/dist/agent/projections.d.ts +17 -0
- package/dist/agent/projections.js +81 -0
- package/dist/agent/scope.d.ts +171 -0
- package/dist/agent/scope.js +7 -0
- package/dist/agent/signals.d.ts +17 -0
- package/dist/agent/signals.js +19 -0
- package/dist/agent/stop-when.d.ts +12 -0
- package/dist/agent/stop-when.js +6 -0
- package/dist/agent/threads.d.ts +34 -0
- package/dist/agent/threads.js +407 -0
- package/dist/agent/tool-schema.d.ts +21 -0
- package/dist/agent/tool-schema.js +67 -0
- package/dist/agent/types.d.ts +87 -0
- package/dist/agent/types.js +32 -0
- package/dist/ai-vercel/index.d.ts +2 -0
- package/dist/ai-vercel/index.js +2 -0
- package/dist/ai-vercel/messages.d.ts +9 -0
- package/dist/ai-vercel/messages.js +68 -0
- package/dist/ai-vercel/vercel-llm.d.ts +8 -0
- package/dist/ai-vercel/vercel-llm.js +98 -0
- package/dist/approval/effects.d.ts +133 -0
- package/dist/approval/effects.js +13 -0
- package/dist/approval/events.d.ts +52 -0
- package/dist/approval/events.js +28 -0
- package/dist/approval/index.d.ts +6 -0
- package/dist/approval/index.js +6 -0
- package/dist/approval/module.d.ts +342 -0
- package/dist/approval/module.js +10 -0
- package/dist/approval/projections.d.ts +42 -0
- package/dist/approval/projections.js +57 -0
- package/dist/approval/scope.d.ts +45 -0
- package/dist/approval/scope.js +7 -0
- package/dist/approval/signals.d.ts +19 -0
- package/dist/approval/signals.js +34 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +6 -0
- package/dist/cli/commands.d.ts +13 -0
- package/dist/cli/commands.js +133 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/client/client.d.ts +69 -0
- package/dist/client/client.js +179 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +2 -0
- package/dist/client/sse.d.ts +10 -0
- package/dist/client/sse.js +103 -0
- package/dist/cloudflare/alarm-scheduler.d.ts +9 -0
- package/dist/cloudflare/alarm-scheduler.js +13 -0
- package/dist/cloudflare/durable-object.d.ts +24 -0
- package/dist/cloudflare/durable-object.js +43 -0
- package/dist/cloudflare/event-store.d.ts +10 -0
- package/dist/cloudflare/event-store.js +18 -0
- package/dist/cloudflare/index.d.ts +4 -0
- package/dist/cloudflare/index.js +4 -0
- package/dist/cloudflare/router.d.ts +12 -0
- package/dist/cloudflare/router.js +26 -0
- package/dist/core/bun-sqlite.d.ts +13 -0
- package/dist/core/bun-sqlite.js +19 -0
- package/dist/core/catalog.d.ts +49 -0
- package/dist/core/catalog.js +39 -0
- package/dist/core/coalesce.d.ts +21 -0
- package/dist/core/coalesce.js +178 -0
- package/dist/core/compose.d.ts +59 -0
- package/dist/core/compose.js +97 -0
- package/dist/core/cursor.d.ts +16 -0
- package/dist/core/cursor.js +46 -0
- package/dist/core/effects.d.ts +110 -0
- package/dist/core/effects.js +99 -0
- package/dist/core/envelope.d.ts +79 -0
- package/dist/core/envelope.js +68 -0
- package/dist/core/event-codec.d.ts +8 -0
- package/dist/core/event-codec.js +82 -0
- package/dist/core/event-index.d.ts +28 -0
- package/dist/core/event-index.js +86 -0
- package/dist/core/fold.d.ts +19 -0
- package/dist/core/fold.js +362 -0
- package/dist/core/ids.d.ts +11 -0
- package/dist/core/ids.js +47 -0
- package/dist/core/index.d.ts +26 -0
- package/dist/core/index.js +25 -0
- package/dist/core/match.d.ts +8 -0
- package/dist/core/match.js +58 -0
- package/dist/core/module-scope.d.ts +99 -0
- package/dist/core/module-scope.js +72 -0
- package/dist/core/module.d.ts +74 -0
- package/dist/core/module.js +3 -0
- package/dist/core/projection.d.ts +83 -0
- package/dist/core/projection.js +241 -0
- package/dist/core/protocol.d.ts +99 -0
- package/dist/core/protocol.js +101 -0
- package/dist/core/replay.d.ts +13 -0
- package/dist/core/replay.js +34 -0
- package/dist/core/schema.d.ts +35 -0
- package/dist/core/schema.js +130 -0
- package/dist/core/serialize.d.ts +14 -0
- package/dist/core/serialize.js +42 -0
- package/dist/core/snapshot-store.d.ts +39 -0
- package/dist/core/snapshot-store.js +53 -0
- package/dist/core/snapshots.d.ts +26 -0
- package/dist/core/snapshots.js +100 -0
- package/dist/core/sqlite-store.d.ts +31 -0
- package/dist/core/sqlite-store.js +328 -0
- package/dist/core/state.d.ts +43 -0
- package/dist/core/state.js +31 -0
- package/dist/core/store.d.ts +146 -0
- package/dist/core/store.js +229 -0
- package/dist/core/thread.d.ts +41 -0
- package/dist/core/thread.js +13 -0
- package/dist/core/types.d.ts +27 -0
- package/dist/core/types.js +42 -0
- package/dist/core/validate-event.d.ts +19 -0
- package/dist/core/validate-event.js +42 -0
- package/dist/debugger/components/debugger-split.d.ts +23 -0
- package/dist/debugger/components/debugger-split.js +25 -0
- package/dist/debugger/components/event-inspector.d.ts +5 -0
- package/dist/debugger/components/event-inspector.js +38 -0
- package/dist/debugger/components/event-stream.d.ts +13 -0
- package/dist/debugger/components/event-stream.js +119 -0
- package/dist/debugger/components/json-view.d.ts +5 -0
- package/dist/debugger/components/json-view.js +5 -0
- package/dist/debugger/components/run-tree.d.ts +14 -0
- package/dist/debugger/components/run-tree.js +38 -0
- package/dist/debugger/components/status-dot.d.ts +3 -0
- package/dist/debugger/components/status-dot.js +5 -0
- package/dist/debugger/contracts.d.ts +22 -0
- package/dist/debugger/contracts.js +1 -0
- package/dist/debugger/events/catalog.d.ts +6 -0
- package/dist/debugger/events/catalog.js +27 -0
- package/dist/debugger/events/domain-summarize.d.ts +3 -0
- package/dist/debugger/events/domain-summarize.js +107 -0
- package/dist/debugger/events/family.d.ts +8 -0
- package/dist/debugger/events/family.js +44 -0
- package/dist/debugger/events/protocol-summarize.d.ts +3 -0
- package/dist/debugger/events/protocol-summarize.js +70 -0
- package/dist/debugger/index.d.ts +17 -0
- package/dist/debugger/index.js +16 -0
- package/dist/debugger/lib/cn.d.ts +5 -0
- package/dist/debugger/lib/cn.js +17 -0
- package/dist/debugger/lib/event-counts.d.ts +5 -0
- package/dist/debugger/lib/event-counts.js +23 -0
- package/dist/debugger/lib/event-meta.d.ts +8 -0
- package/dist/debugger/lib/event-meta.js +20 -0
- package/dist/debugger/lib/project-run.d.ts +9 -0
- package/dist/debugger/lib/project-run.js +14 -0
- package/dist/debugger/lib/status.d.ts +3 -0
- package/dist/debugger/lib/status.js +31 -0
- package/dist/debugger/lib/visible-events.d.ts +8 -0
- package/dist/debugger/lib/visible-events.js +21 -0
- package/dist/debugger/styles.css +71 -0
- package/dist/debugger/ui/checkbox.d.ts +4 -0
- package/dist/debugger/ui/checkbox.js +9 -0
- package/dist/debugger/ui/input.d.ts +3 -0
- package/dist/debugger/ui/input.js +7 -0
- package/dist/debugger/ui/scroll-area.d.ts +9 -0
- package/dist/debugger/ui/scroll-area.js +10 -0
- package/dist/debugger/ui/tabs.d.ts +7 -0
- package/dist/debugger/ui/tabs.js +17 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/projectors/index-model.d.ts +56 -0
- package/dist/projectors/index-model.js +117 -0
- package/dist/projectors/index.d.ts +4 -0
- package/dist/projectors/index.js +3 -0
- package/dist/projectors/memory.d.ts +5 -0
- package/dist/projectors/memory.js +77 -0
- package/dist/projectors/postgres.d.ts +9 -0
- package/dist/projectors/postgres.js +108 -0
- package/dist/projectors/projector.d.ts +11 -0
- package/dist/projectors/projector.js +1 -0
- package/dist/projectors/schema.d.ts +5 -0
- package/dist/projectors/schema.js +43 -0
- package/dist/projectors/sqlite.d.ts +9 -0
- package/dist/projectors/sqlite.js +103 -0
- package/dist/projectors/with-projectors.d.ts +17 -0
- package/dist/projectors/with-projectors.js +55 -0
- package/dist/react/derived.d.ts +19 -0
- package/dist/react/derived.js +50 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.js +5 -0
- package/dist/react/materialize.d.ts +3 -0
- package/dist/react/materialize.js +86 -0
- package/dist/react/notify.d.ts +18 -0
- package/dist/react/notify.js +80 -0
- package/dist/react/react.d.ts +61 -0
- package/dist/react/react.js +187 -0
- package/dist/react/register.d.ts +31 -0
- package/dist/react/register.js +1 -0
- package/dist/react/sse.d.ts +1 -0
- package/dist/react/sse.js +1 -0
- package/dist/react/store.d.ts +56 -0
- package/dist/react/store.js +340 -0
- package/dist/react/tables.d.ts +21 -0
- package/dist/react/tables.js +6 -0
- package/dist/runtime/dispatch-effect.d.ts +3 -0
- package/dist/runtime/dispatch-effect.js +113 -0
- package/dist/runtime/events-api.d.ts +4 -0
- package/dist/runtime/events-api.js +84 -0
- package/dist/runtime/index.d.ts +5 -0
- package/dist/runtime/index.js +5 -0
- package/dist/runtime/looms.d.ts +74 -0
- package/dist/runtime/looms.js +109 -0
- package/dist/runtime/runtime-helpers.d.ts +51 -0
- package/dist/runtime/runtime-helpers.js +230 -0
- package/dist/runtime/runtime.d.ts +91 -0
- package/dist/runtime/runtime.js +531 -0
- package/dist/runtime/server.d.ts +17 -0
- package/dist/runtime/server.js +226 -0
- package/dist/runtime/sse.d.ts +15 -0
- package/dist/runtime/sse.js +96 -0
- package/dist/runtime/wake-scheduler.d.ts +8 -0
- package/dist/runtime/wake-scheduler.js +38 -0
- package/dist/s2/config.d.ts +27 -0
- package/dist/s2/config.js +54 -0
- package/dist/s2/index.d.ts +6 -0
- package/dist/s2/index.js +6 -0
- package/dist/s2/lite.d.ts +26 -0
- package/dist/s2/lite.js +150 -0
- package/dist/s2/projector.d.ts +8 -0
- package/dist/s2/projector.js +28 -0
- package/dist/s2/serialize.d.ts +1 -0
- package/dist/s2/serialize.js +1 -0
- package/dist/s2/snapshot-store.d.ts +17 -0
- package/dist/s2/snapshot-store.js +320 -0
- package/dist/s2/store.d.ts +35 -0
- package/dist/s2/store.js +337 -0
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.js +63 -0
- package/dist/workflow/definitions-store.d.ts +11 -0
- package/dist/workflow/definitions-store.js +8 -0
- package/dist/workflow/definitions.d.ts +82 -0
- package/dist/workflow/definitions.js +32 -0
- package/dist/workflow/effects.d.ts +305 -0
- package/dist/workflow/effects.js +208 -0
- package/dist/workflow/events.d.ts +60 -0
- package/dist/workflow/events.js +39 -0
- package/dist/workflow/index.d.ts +8 -0
- package/dist/workflow/index.js +8 -0
- package/dist/workflow/module.d.ts +926 -0
- package/dist/workflow/module.js +19 -0
- package/dist/workflow/projections.d.ts +30 -0
- package/dist/workflow/projections.js +43 -0
- package/dist/workflow/scope.d.ts +61 -0
- package/dist/workflow/scope.js +7 -0
- package/dist/workflow/threads.d.ts +171 -0
- package/dist/workflow/threads.js +287 -0
- package/package.json +183 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { resolveRunTarget } from '@swirls/looms/actor';
|
|
2
|
+
/**
|
|
3
|
+
* Routes incoming HTTP requests to the appropriate Durable Object actor cell.
|
|
4
|
+
* Handles `/health` locally, returns 501 for global `GET /runs`, and forwards
|
|
5
|
+
* all other run and event-stream requests to `namespace.getByName(runId)`.
|
|
6
|
+
*/
|
|
7
|
+
export async function routeToDurableObject(namespace, req) {
|
|
8
|
+
const url = new URL(req.url);
|
|
9
|
+
if (url.pathname === '/health') {
|
|
10
|
+
return Response.json({ ok: true });
|
|
11
|
+
}
|
|
12
|
+
if (req.method === 'GET' && url.pathname === '/runs') {
|
|
13
|
+
return Response.json({
|
|
14
|
+
error: 'Global run listing is not supported in the Durable Object backend without a global index',
|
|
15
|
+
}, { status: 501 });
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const { runId, request } = await resolveRunTarget(req);
|
|
19
|
+
const stub = namespace.getByName(runId);
|
|
20
|
+
return stub.fetch(request);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Paths without a resolvable runId (e.g. GET /) are not handled by this router.
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { type SqliteExec } from './sqlite-store';
|
|
3
|
+
import type { EventStore } from './store';
|
|
4
|
+
export interface BunSqliteEventStoreOptions {
|
|
5
|
+
/** File path or `:memory:`. Default `:memory:`. */
|
|
6
|
+
readonly path?: string;
|
|
7
|
+
readonly db?: Database;
|
|
8
|
+
}
|
|
9
|
+
export declare function bunSqliteExec(db: Database): SqliteExec;
|
|
10
|
+
/**
|
|
11
|
+
* Embedded EventStore backed by Bun's native SQLite engine (`bun:sqlite`).
|
|
12
|
+
*/
|
|
13
|
+
export declare function bunSqliteEventStore(options?: BunSqliteEventStoreOptions): EventStore;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { sqliteEventStore } from './sqlite-store.js';
|
|
3
|
+
export function bunSqliteExec(db) {
|
|
4
|
+
return {
|
|
5
|
+
run(sql, params = []) {
|
|
6
|
+
db.run(sql, [...params]);
|
|
7
|
+
},
|
|
8
|
+
rows(sql, params = []) {
|
|
9
|
+
return db.query(sql).all(...params);
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Embedded EventStore backed by Bun's native SQLite engine (`bun:sqlite`).
|
|
15
|
+
*/
|
|
16
|
+
export function bunSqliteEventStore(options = {}) {
|
|
17
|
+
const db = options.db ?? new Database(options.path ?? ':memory:');
|
|
18
|
+
return sqliteEventStore({ exec: bunSqliteExec(db) });
|
|
19
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import type { Schema } from 'effect';
|
|
3
|
+
import { type EventEnvelope, type EventInput, type TypedEvent } from './envelope';
|
|
4
|
+
import { type JsonValue } from './types';
|
|
5
|
+
declare const catalogPayloadBrand: unique symbol;
|
|
6
|
+
/** Type-only catalog entry. Carries a payload type without a runtime schema. */
|
|
7
|
+
export type CatalogEventSpec<TPayload = object> = {
|
|
8
|
+
readonly [catalogPayloadBrand]?: TPayload;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Declare a catalog event's payload type when you do not have a Zod / Standard Schema.
|
|
12
|
+
*
|
|
13
|
+
* `payload<{ amount: number }>()` is a compile-time marker — not a cast of `{}`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function payload<TPayload>(): CatalogEventSpec<TPayload>;
|
|
16
|
+
export type CatalogEntry = CatalogEventSpec | Schema.Top | StandardSchemaV1;
|
|
17
|
+
export type CatalogEntries = {
|
|
18
|
+
readonly [key: string]: CatalogEntry;
|
|
19
|
+
};
|
|
20
|
+
export type InferPayload<T> = T extends CatalogEventSpec<infer P> ? P : T extends Schema.Top ? Schema.Schema.Type<T> : T extends StandardSchemaV1<infer _In, infer Out> ? Out : JsonValue;
|
|
21
|
+
export interface EventCatalog<TNamespace extends string = string, TEntries extends CatalogEntries = CatalogEntries> {
|
|
22
|
+
readonly namespace: TNamespace;
|
|
23
|
+
readonly entries: TEntries;
|
|
24
|
+
event<K extends keyof TEntries & string>(key: K, payload: InferPayload<TEntries[K]>, meta: Omit<EventInput, 'type' | 'payload'> & {
|
|
25
|
+
runId: string;
|
|
26
|
+
}): EventEnvelope<`${TNamespace}.${K}`, InferPayload<TEntries[K]>>;
|
|
27
|
+
input<K extends keyof TEntries & string>(key: K, payload: InferPayload<TEntries[K]>, meta?: Omit<EventInput, 'type' | 'payload'>): Omit<EventInput, 'type' | 'payload'> & {
|
|
28
|
+
readonly type: `${TNamespace}.${K}`;
|
|
29
|
+
readonly payload: InferPayload<TEntries[K]>;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export type CatalogEvent<TNamespace extends string, TEntries extends CatalogEntries> = {
|
|
33
|
+
[K in keyof TEntries & string]: TypedEvent<`${TNamespace}.${K}`, InferPayload<TEntries[K]>>;
|
|
34
|
+
}[keyof TEntries & string];
|
|
35
|
+
export type EventsOfCatalog<T> = T extends EventCatalog<infer N, infer E> ? CatalogEvent<N, E> : never;
|
|
36
|
+
export type EventTypeOf<T> = T extends EventCatalog<infer N, infer E> ? `${N}.${keyof E & string}` : T extends EventEnvelope<infer Type, any> ? Type : string;
|
|
37
|
+
export type EventInputOf<T> = T extends EventCatalog<infer N, infer E> ? {
|
|
38
|
+
[K in keyof E & string]: Omit<EventInput, 'type' | 'payload'> & {
|
|
39
|
+
readonly type: `${N}.${K}`;
|
|
40
|
+
readonly payload: InferPayload<E[K]>;
|
|
41
|
+
};
|
|
42
|
+
}[keyof E & string] : T extends EventEnvelope<infer Type, infer Payload> ? Omit<EventInput, 'type' | 'payload'> & {
|
|
43
|
+
readonly type: Type;
|
|
44
|
+
readonly payload: Payload;
|
|
45
|
+
} : EventInput;
|
|
46
|
+
export declare function eventType<TNamespace extends string, K extends string>(namespace: TNamespace, key: K): `${TNamespace}.${K}`;
|
|
47
|
+
export declare function defineEventCatalog<TNamespace extends string, TEntries extends CatalogEntries>(namespace: TNamespace, entries: TEntries): EventCatalog<TNamespace, TEntries>;
|
|
48
|
+
export declare function isCatalogEvent<TNamespace extends string>(event: EventEnvelope, catalog: EventCatalog<TNamespace>): event is EventEnvelope<`${TNamespace}.${string}`>;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createEvent } from './envelope.js';
|
|
2
|
+
import { asJson } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Declare a catalog event's payload type when you do not have a Zod / Standard Schema.
|
|
5
|
+
*
|
|
6
|
+
* `payload<{ amount: number }>()` is a compile-time marker — not a cast of `{}`.
|
|
7
|
+
*/
|
|
8
|
+
export function payload() {
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
export function eventType(namespace, key) {
|
|
12
|
+
return `${namespace}.${key}`;
|
|
13
|
+
}
|
|
14
|
+
export function defineEventCatalog(namespace, entries) {
|
|
15
|
+
return {
|
|
16
|
+
namespace,
|
|
17
|
+
entries,
|
|
18
|
+
event(key, eventPayload, meta) {
|
|
19
|
+
const { runId, ...rest } = meta;
|
|
20
|
+
// SAFETY: createEvent returns EventEnvelope with inferred payload type.
|
|
21
|
+
return createEvent(runId, {
|
|
22
|
+
...rest,
|
|
23
|
+
type: eventType(namespace, key),
|
|
24
|
+
payload: asJson(eventPayload),
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
input(key, eventPayload, meta) {
|
|
28
|
+
return {
|
|
29
|
+
...meta,
|
|
30
|
+
type: eventType(namespace, key),
|
|
31
|
+
// SAFETY: asJson returns typed payload for catalog event.
|
|
32
|
+
payload: asJson(eventPayload),
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function isCatalogEvent(event, catalog) {
|
|
38
|
+
return event.type.startsWith(`${catalog.namespace}.`);
|
|
39
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-key coalescing append buffer.
|
|
3
|
+
* - `push` enqueues and succeeds without waiting for the store ack (unless over `maxPending`).
|
|
4
|
+
* - At most one flush is in flight per key; arrivals during a flush form the next batch.
|
|
5
|
+
* - Buffer order is flush order. Independent keys flush concurrently.
|
|
6
|
+
* - A failed flush poisons the key: later `push` calls fail, `drain` still succeeds.
|
|
7
|
+
*/
|
|
8
|
+
import { Effect } from 'effect';
|
|
9
|
+
export interface CoalescingAppenderOptions {
|
|
10
|
+
/** When the buffer exceeds this size, `push` waits for the next flush. Default 1024. */
|
|
11
|
+
readonly maxPending?: number;
|
|
12
|
+
/** Extra delay before the first flush of an idle key. `0` (default) yields to the scheduler. */
|
|
13
|
+
readonly lingerMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function createCoalescingAppender<T, E = Error>(flush: (key: string, batch: readonly T[]) => Effect.Effect<void, E>, options?: CoalescingAppenderOptions): {
|
|
16
|
+
push: (key: string, event: T) => Effect.Effect<void, E>;
|
|
17
|
+
drain: (key: string) => Effect.Effect<void>;
|
|
18
|
+
clear: (key?: string) => void;
|
|
19
|
+
size: () => number;
|
|
20
|
+
};
|
|
21
|
+
export type CoalescingAppender<T, E = Error> = ReturnType<typeof createCoalescingAppender<T, E>>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-key coalescing append buffer.
|
|
3
|
+
* - `push` enqueues and succeeds without waiting for the store ack (unless over `maxPending`).
|
|
4
|
+
* - At most one flush is in flight per key; arrivals during a flush form the next batch.
|
|
5
|
+
* - Buffer order is flush order. Independent keys flush concurrently.
|
|
6
|
+
* - A failed flush poisons the key: later `push` calls fail, `drain` still succeeds.
|
|
7
|
+
*/
|
|
8
|
+
import { Deferred, Effect, Exit, Ref } from 'effect';
|
|
9
|
+
function createState() {
|
|
10
|
+
return {
|
|
11
|
+
buffer: [],
|
|
12
|
+
flushing: false,
|
|
13
|
+
scheduled: false,
|
|
14
|
+
poison: undefined,
|
|
15
|
+
nextFlush: Deferred.makeUnsafe(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const completeFlush = (state, cause) => Effect.gen(function* () {
|
|
19
|
+
const current = state.nextFlush;
|
|
20
|
+
state.nextFlush = Deferred.makeUnsafe();
|
|
21
|
+
if (cause) {
|
|
22
|
+
yield* Deferred.failCause(current, cause);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
yield* Deferred.succeed(current, undefined);
|
|
26
|
+
});
|
|
27
|
+
export function createCoalescingAppender(flush, options) {
|
|
28
|
+
const maxPending = options?.maxPending ?? 1024;
|
|
29
|
+
const lingerMs = options?.lingerMs ?? 0;
|
|
30
|
+
const states = Ref.makeUnsafe(new Map());
|
|
31
|
+
const evictIfIdle = (key) => Ref.update(states, (map) => {
|
|
32
|
+
const state = map.get(key);
|
|
33
|
+
if (!state ||
|
|
34
|
+
state.buffer.length > 0 ||
|
|
35
|
+
state.flushing ||
|
|
36
|
+
state.scheduled ||
|
|
37
|
+
state.poison !== undefined) {
|
|
38
|
+
return map;
|
|
39
|
+
}
|
|
40
|
+
const next = new Map(map);
|
|
41
|
+
next.delete(key);
|
|
42
|
+
return next;
|
|
43
|
+
});
|
|
44
|
+
const take = (key) => Ref.modify(states, (map) => {
|
|
45
|
+
const state = map.get(key);
|
|
46
|
+
if (!state) {
|
|
47
|
+
return [{ kind: 'gone' }, map];
|
|
48
|
+
}
|
|
49
|
+
if (state.buffer.length === 0) {
|
|
50
|
+
state.flushing = false;
|
|
51
|
+
state.scheduled = false;
|
|
52
|
+
return [{ kind: 'idle', state }, map];
|
|
53
|
+
}
|
|
54
|
+
const batch = state.buffer;
|
|
55
|
+
state.buffer = [];
|
|
56
|
+
state.flushing = true;
|
|
57
|
+
state.scheduled = false;
|
|
58
|
+
return [{ kind: 'batch', batch, state }, map];
|
|
59
|
+
});
|
|
60
|
+
const stillCurrent = (key, state) => Ref.modify(states, (map) => [map.get(key) === state, map]);
|
|
61
|
+
const runFlush = (key) => Effect.gen(function* () {
|
|
62
|
+
while (true) {
|
|
63
|
+
const taken = yield* take(key);
|
|
64
|
+
switch (taken.kind) {
|
|
65
|
+
case 'gone':
|
|
66
|
+
return;
|
|
67
|
+
case 'idle':
|
|
68
|
+
yield* completeFlush(taken.state);
|
|
69
|
+
yield* evictIfIdle(key);
|
|
70
|
+
return;
|
|
71
|
+
case 'batch': {
|
|
72
|
+
const exit = yield* Effect.exit(flush(key, taken.batch));
|
|
73
|
+
if (Exit.isFailure(exit)) {
|
|
74
|
+
const current = yield* stillCurrent(key, taken.state);
|
|
75
|
+
if (current) {
|
|
76
|
+
taken.state.poison = exit.cause;
|
|
77
|
+
taken.state.buffer = [];
|
|
78
|
+
taken.state.flushing = false;
|
|
79
|
+
taken.state.scheduled = false;
|
|
80
|
+
yield* completeFlush(taken.state, exit.cause);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const current = yield* stillCurrent(key, taken.state);
|
|
85
|
+
if (!current) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
yield* completeFlush(taken.state);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
default: {
|
|
92
|
+
const exhaustiveCheck = taken;
|
|
93
|
+
return exhaustiveCheck;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
const scheduleFlush = (key) => Effect.sync(() => {
|
|
99
|
+
Effect.runFork((lingerMs > 0 ? Effect.sleep(lingerMs) : Effect.yieldNow).pipe(Effect.andThen(runFlush(key))));
|
|
100
|
+
});
|
|
101
|
+
const enqueue = (key, event) => Ref.modify(states, (map) => {
|
|
102
|
+
const existing = map.get(key);
|
|
103
|
+
if (existing?.poison !== undefined) {
|
|
104
|
+
return [{ kind: 'poisoned', cause: existing.poison }, map];
|
|
105
|
+
}
|
|
106
|
+
const next = existing ? map : new Map(map);
|
|
107
|
+
const state = existing ?? createState();
|
|
108
|
+
if (!existing) {
|
|
109
|
+
next.set(key, state);
|
|
110
|
+
}
|
|
111
|
+
state.buffer.push(event);
|
|
112
|
+
const shouldSchedule = !state.flushing && !state.scheduled;
|
|
113
|
+
if (shouldSchedule) {
|
|
114
|
+
state.scheduled = true;
|
|
115
|
+
}
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
kind: 'enqueued',
|
|
119
|
+
shouldSchedule,
|
|
120
|
+
pending: state.buffer.length,
|
|
121
|
+
deferred: state.nextFlush,
|
|
122
|
+
},
|
|
123
|
+
next,
|
|
124
|
+
];
|
|
125
|
+
});
|
|
126
|
+
const push = (key, event) => Effect.gen(function* () {
|
|
127
|
+
const result = yield* enqueue(key, event);
|
|
128
|
+
switch (result.kind) {
|
|
129
|
+
case 'poisoned':
|
|
130
|
+
return yield* Effect.failCause(result.cause);
|
|
131
|
+
case 'enqueued': {
|
|
132
|
+
if (result.shouldSchedule) {
|
|
133
|
+
yield* scheduleFlush(key);
|
|
134
|
+
}
|
|
135
|
+
if (result.pending > maxPending) {
|
|
136
|
+
yield* Deferred.await(result.deferred);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
default: {
|
|
141
|
+
const exhaustiveCheck = result;
|
|
142
|
+
return exhaustiveCheck;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
const drain = (key) => Effect.gen(function* () {
|
|
147
|
+
while (true) {
|
|
148
|
+
const wait = yield* Ref.modify(states, (map) => {
|
|
149
|
+
const state = map.get(key);
|
|
150
|
+
if (!state ||
|
|
151
|
+
state.poison !== undefined ||
|
|
152
|
+
(state.buffer.length === 0 && !state.flushing && !state.scheduled)) {
|
|
153
|
+
return [{ kind: 'done' }, map];
|
|
154
|
+
}
|
|
155
|
+
return [{ kind: 'wait', deferred: state.nextFlush }, map];
|
|
156
|
+
});
|
|
157
|
+
if (wait.kind === 'done') {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
yield* Deferred.await(wait.deferred).pipe(Effect.ignoreCause);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
const clear = (key) => {
|
|
164
|
+
Effect.runSync(Ref.update(states, (map) => {
|
|
165
|
+
if (key === undefined) {
|
|
166
|
+
return new Map();
|
|
167
|
+
}
|
|
168
|
+
if (!map.has(key)) {
|
|
169
|
+
return map;
|
|
170
|
+
}
|
|
171
|
+
const next = new Map(map);
|
|
172
|
+
next.delete(key);
|
|
173
|
+
return next;
|
|
174
|
+
}));
|
|
175
|
+
};
|
|
176
|
+
const size = () => Effect.runSync(Ref.get(states)).size;
|
|
177
|
+
return { push, drain, clear, size };
|
|
178
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import type { CatalogEvent, EventCatalog } from './catalog';
|
|
3
|
+
import type { EffectDefinition } from './effects';
|
|
4
|
+
import type { EventEnvelope } from './envelope';
|
|
5
|
+
import type { FoldRegistry } from './fold';
|
|
6
|
+
import type { AnyRuntimeModule, EffectMiddleware, RuntimeModule, RegisteredDefinition } from './module';
|
|
7
|
+
import type { ProjectionDefinition } from './projection';
|
|
8
|
+
import { protocolCatalog } from './protocol';
|
|
9
|
+
import type { ThreadDefinition } from './thread';
|
|
10
|
+
declare const ModuleCompositionError_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 & {
|
|
11
|
+
readonly _tag: "ModuleCompositionError";
|
|
12
|
+
} & Readonly<A>;
|
|
13
|
+
export declare class ModuleCompositionError extends ModuleCompositionError_base<{
|
|
14
|
+
readonly message: string;
|
|
15
|
+
}> {
|
|
16
|
+
constructor(text: string);
|
|
17
|
+
}
|
|
18
|
+
export interface ComposedRegistry extends FoldRegistry {
|
|
19
|
+
readonly definitions: readonly RegisteredDefinition[];
|
|
20
|
+
readonly modules: readonly AnyRuntimeModule[];
|
|
21
|
+
readonly effects: ReadonlyMap<string, EffectDefinition>;
|
|
22
|
+
readonly projections: ReadonlyMap<string, ProjectionDefinition>;
|
|
23
|
+
readonly catalogs: readonly EventCatalog[];
|
|
24
|
+
readonly middleware: readonly EffectMiddleware[];
|
|
25
|
+
}
|
|
26
|
+
export declare function composeModules(modules: readonly AnyRuntimeModule[]): ComposedRegistry;
|
|
27
|
+
export declare function composeModulesEffect(modules: readonly AnyRuntimeModule[]): Effect.Effect<ComposedRegistry, ModuleCompositionError>;
|
|
28
|
+
export type ProtocolEvents = CatalogEvent<'runtime', typeof protocolCatalog.entries>;
|
|
29
|
+
export type ModuleEvents<T> = T extends {
|
|
30
|
+
readonly events?: infer C;
|
|
31
|
+
} ? C extends EventCatalog<infer N, infer E> ? CatalogEvent<N, E> : never : never;
|
|
32
|
+
export type EventsOf<T> = T extends (...args: never[]) => infer R ? EventsOf<R> : T extends (...args: any[]) => infer R ? EventsOf<R> : T extends {
|
|
33
|
+
readonly modules: infer M;
|
|
34
|
+
} ? EventsOf<M> : T extends readonly (infer Item)[] ? Item extends AnyRuntimeModule ? ProtocolEvents | ModuleEvents<Item> : Item extends EventCatalog<infer N, infer E> ? ProtocolEvents | CatalogEvent<N, E> : EventEnvelope : T extends RuntimeModule ? ProtocolEvents | ModuleEvents<T> : T extends EventCatalog<infer N, infer E> ? CatalogEvent<N, E> : EventEnvelope;
|
|
35
|
+
export type EffectsOf<T> = T extends {
|
|
36
|
+
readonly modules: infer M;
|
|
37
|
+
} ? M extends readonly AnyRuntimeModule[] ? ModuleEffects<M[number]> : string : T extends RuntimeModule ? ModuleEffects<T> : string;
|
|
38
|
+
type ModuleEffects<T> = T extends {
|
|
39
|
+
readonly effects: infer E;
|
|
40
|
+
} ? E extends {
|
|
41
|
+
readonly [key: string]: EffectDefinition;
|
|
42
|
+
} ? E[keyof E]['type'] : never : never;
|
|
43
|
+
export type ThreadsOf<T> = T extends {
|
|
44
|
+
readonly modules: infer M;
|
|
45
|
+
} ? M extends readonly AnyRuntimeModule[] ? ModuleKinds<M[number]> : string : T extends RuntimeModule ? ModuleKinds<T> : string;
|
|
46
|
+
type ModuleKinds<T> = T extends {
|
|
47
|
+
readonly threads: infer E;
|
|
48
|
+
} ? E extends {
|
|
49
|
+
readonly [key: string]: ThreadDefinition;
|
|
50
|
+
} ? keyof E & string : never : never;
|
|
51
|
+
export type ProjectionsOf<T> = T extends {
|
|
52
|
+
readonly modules: infer M;
|
|
53
|
+
} ? M extends readonly AnyRuntimeModule[] ? ModuleProjectionNames<M[number]> : string : T extends RuntimeModule ? ModuleProjectionNames<T> : string;
|
|
54
|
+
type ModuleProjectionNames<T> = T extends {
|
|
55
|
+
readonly projections: infer P;
|
|
56
|
+
} ? P extends {
|
|
57
|
+
readonly [key: string]: ProjectionDefinition;
|
|
58
|
+
} ? keyof P & string : never : never;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Data, Effect } from 'effect';
|
|
2
|
+
import { protocolCatalog } from './protocol.js';
|
|
3
|
+
export class ModuleCompositionError extends Data.TaggedError('ModuleCompositionError') {
|
|
4
|
+
constructor(text) {
|
|
5
|
+
super({ message: text });
|
|
6
|
+
this.name = 'ModuleCompositionError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function composeModules(modules) {
|
|
10
|
+
const definitions = [];
|
|
11
|
+
const definitionKeys = new Set();
|
|
12
|
+
const namespaces = new Set();
|
|
13
|
+
const threads = new Map();
|
|
14
|
+
const effects = new Map();
|
|
15
|
+
const projections = new Map();
|
|
16
|
+
const middleware = [];
|
|
17
|
+
const catalogs = [protocolCatalog];
|
|
18
|
+
for (const module of modules) {
|
|
19
|
+
if (namespaces.has(module.namespace)) {
|
|
20
|
+
throw new ModuleCompositionError(`Duplicate module namespace: ${module.namespace}`);
|
|
21
|
+
}
|
|
22
|
+
namespaces.add(module.namespace);
|
|
23
|
+
if (module.dependencies) {
|
|
24
|
+
for (const dep of module.dependencies) {
|
|
25
|
+
if (!namespaces.has(dep.namespace) && dep.namespace !== 'runtime') {
|
|
26
|
+
const present = modules.some((item) => item.namespace === dep.namespace);
|
|
27
|
+
if (!present) {
|
|
28
|
+
throw new ModuleCompositionError(`Module ${module.namespace} depends on missing module ${dep.namespace}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (module.events && !catalogs.some((c) => c.namespace === module.events.namespace)) {
|
|
34
|
+
catalogs.push(module.events);
|
|
35
|
+
}
|
|
36
|
+
if (module.observes) {
|
|
37
|
+
for (const obs of module.observes) {
|
|
38
|
+
if (!catalogs.some((c) => c.namespace === obs.namespace)) {
|
|
39
|
+
catalogs.push(obs);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const definition of module.definitions ?? []) {
|
|
44
|
+
const key = `${definition.kind}:${definition.name}`;
|
|
45
|
+
if (definitionKeys.has(key)) {
|
|
46
|
+
throw new ModuleCompositionError(`Duplicate definition: ${key}`);
|
|
47
|
+
}
|
|
48
|
+
if (!module.threads || !Object.hasOwn(module.threads, definition.kind)) {
|
|
49
|
+
throw new ModuleCompositionError(`Module ${module.namespace} registers definition ${key} but does not implement thread kind ${definition.kind}`);
|
|
50
|
+
}
|
|
51
|
+
definitionKeys.add(key);
|
|
52
|
+
definitions.push({
|
|
53
|
+
kind: definition.kind,
|
|
54
|
+
name: definition.name,
|
|
55
|
+
input: definition.input,
|
|
56
|
+
value: definition,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const moduleThreads = module.threads;
|
|
60
|
+
if (moduleThreads) {
|
|
61
|
+
for (const [kind, definition] of Object.entries(moduleThreads)) {
|
|
62
|
+
if (threads.has(kind)) {
|
|
63
|
+
throw new ModuleCompositionError(`Duplicate thread kind: ${kind}`);
|
|
64
|
+
}
|
|
65
|
+
threads.set(kind, definition);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (module.effects) {
|
|
69
|
+
for (const definition of Object.values(module.effects)) {
|
|
70
|
+
if (effects.has(definition.type)) {
|
|
71
|
+
throw new ModuleCompositionError(`Duplicate effect type: ${definition.type}`);
|
|
72
|
+
}
|
|
73
|
+
effects.set(definition.type, definition);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (module.middleware) {
|
|
77
|
+
middleware.push(...module.middleware);
|
|
78
|
+
}
|
|
79
|
+
if (module.projections) {
|
|
80
|
+
for (const definition of Object.values(module.projections)) {
|
|
81
|
+
if (projections.has(definition.name)) {
|
|
82
|
+
throw new ModuleCompositionError(`Duplicate projection: ${definition.name}`);
|
|
83
|
+
}
|
|
84
|
+
projections.set(definition.name, definition);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { definitions, modules, threads, effects, projections, catalogs, middleware };
|
|
89
|
+
}
|
|
90
|
+
export function composeModulesEffect(modules) {
|
|
91
|
+
return Effect.try({
|
|
92
|
+
try: () => composeModules(modules),
|
|
93
|
+
catch: (cause) => cause instanceof ModuleCompositionError
|
|
94
|
+
? cause
|
|
95
|
+
: new ModuleCompositionError(cause instanceof Error ? cause.message : String(cause)),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type AppendableEvent, type EventEnvelope } from './envelope';
|
|
2
|
+
import { type FoldRegistry } from './fold';
|
|
3
|
+
import { type RunState } from './state';
|
|
4
|
+
import type { AppendResult } from './store';
|
|
5
|
+
export interface RunCursor {
|
|
6
|
+
readonly state: RunState;
|
|
7
|
+
readonly seq: number;
|
|
8
|
+
readonly durableSinceSnapshot: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function emptyCursor(runId: string): RunCursor;
|
|
11
|
+
/**
|
|
12
|
+
* Folds events into the cursor. Ephemeral events are skipped by `foldEvent`
|
|
13
|
+
* (no RunState change) but still advance `seq`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function advanceCursor(cursor: RunCursor, events: readonly EventEnvelope[], registry: FoldRegistry): RunCursor;
|
|
16
|
+
export declare function assignSequences(batch: readonly AppendableEvent[], result: AppendResult): EventEnvelope[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { withAssignedSeq } from './envelope.js';
|
|
2
|
+
import { foldEvent } from './fold.js';
|
|
3
|
+
import { emptyRunState } from './state.js';
|
|
4
|
+
export function emptyCursor(runId) {
|
|
5
|
+
return {
|
|
6
|
+
state: emptyRunState(runId),
|
|
7
|
+
seq: 0,
|
|
8
|
+
durableSinceSnapshot: 0,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Folds events into the cursor. Ephemeral events are skipped by `foldEvent`
|
|
13
|
+
* (no RunState change) but still advance `seq`.
|
|
14
|
+
*/
|
|
15
|
+
export function advanceCursor(cursor, events, registry) {
|
|
16
|
+
let state = cursor.state;
|
|
17
|
+
let seq = cursor.seq;
|
|
18
|
+
let durableSinceSnapshot = cursor.durableSinceSnapshot;
|
|
19
|
+
for (const event of events) {
|
|
20
|
+
if (event.seq > seq) {
|
|
21
|
+
seq = event.seq;
|
|
22
|
+
}
|
|
23
|
+
if (!event.ephemeral) {
|
|
24
|
+
if (event.type === 'runtime.snapshot.taken') {
|
|
25
|
+
durableSinceSnapshot = 0;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
durableSinceSnapshot += 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
state = foldEvent(state, event, registry);
|
|
32
|
+
}
|
|
33
|
+
return { state, seq, durableSinceSnapshot };
|
|
34
|
+
}
|
|
35
|
+
export function assignSequences(batch, result) {
|
|
36
|
+
if (batch.length !== result.sequences.length) {
|
|
37
|
+
throw new Error(`assignSequences length mismatch: batch has ${batch.length} events but append returned ${result.sequences.length} sequences`);
|
|
38
|
+
}
|
|
39
|
+
return batch.map((partial, index) => {
|
|
40
|
+
const seq = result.sequences[index];
|
|
41
|
+
if (seq === undefined) {
|
|
42
|
+
throw new Error(`assignSequences missing sequence at index ${index}`);
|
|
43
|
+
}
|
|
44
|
+
return withAssignedSeq(partial, partial.runId, seq);
|
|
45
|
+
});
|
|
46
|
+
}
|