@swirls/looms 0.0.2 → 0.2.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 +24 -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 +57 -33
- 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/debugger/agent-chat.d.ts +2 -0
- package/dist/agent/debugger/agent-chat.js +176 -0
- package/dist/agent/debugger/index.d.ts +5 -0
- package/dist/agent/debugger/index.js +19 -0
- package/dist/agent/debugger/summarize.d.ts +2 -0
- package/dist/agent/debugger/summarize.js +51 -0
- package/dist/agent/debugger/token-usage.d.ts +3 -0
- package/dist/agent/debugger/token-usage.js +11 -0
- package/dist/agent/definitions-store.d.ts +2 -3
- package/dist/agent/definitions-store.js +2 -4
- package/dist/agent/definitions.d.ts +47 -29
- package/dist/agent/definitions.js +39 -12
- 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 +26 -1
- package/dist/agent/signals.js +79 -0
- package/dist/agent/threads.d.ts +3 -33
- package/dist/agent/threads.js +20 -25
- package/dist/agent/tool-schema.d.ts +1 -17
- package/dist/agent/tool-schema.js +3 -43
- 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 +36 -0
- package/dist/ai-vercel/vercel-evaluator.js +73 -0
- package/dist/ai-vercel/vercel-llm.js +64 -44
- package/dist/approval/debugger/approvals-view.d.ts +3 -0
- package/dist/approval/debugger/approvals-view.js +16 -0
- package/dist/approval/debugger/index.d.ts +4 -0
- package/dist/approval/debugger/index.js +11 -0
- package/dist/approval/debugger/summarize.d.ts +2 -0
- package/dist/approval/debugger/summarize.js +17 -0
- 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 +1213 -18
- package/dist/client/client.js +120 -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.d.ts +1 -1
- package/dist/cloudflare/router.js +15 -16
- 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 +19 -2
- package/dist/core/debugger-host.d.ts +16 -0
- package/dist/core/debugger-host.js +1 -0
- 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 +7 -1
- package/dist/core/index.js +6 -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 +18 -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/published.d.ts +43 -0
- package/dist/core/published.js +44 -0
- package/dist/core/replay.d.ts +312 -3
- package/dist/core/replay.js +11 -1
- package/dist/core/schema.d.ts +11 -20
- package/dist/core/schema.js +63 -80
- package/dist/core/serialize.js +2 -4
- package/dist/core/snapshot-store.d.ts +6 -1
- package/dist/core/snapshot-store.js +15 -2
- package/dist/core/snapshots.d.ts +5 -1
- package/dist/core/snapshots.js +10 -15
- package/dist/core/sqlite-store.js +95 -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/app/assets/index-B3Ah8WAE.css +2 -0
- package/dist/debugger/app/assets/index-Qb371t_O.js +22 -0
- package/dist/debugger/app/assets/rolldown-runtime-CbXtAM7H.js +1 -0
- package/dist/debugger/app/assets/routes-CjtEHgPQ.js +47 -0
- package/dist/debugger/app/index.html +14 -0
- package/dist/debugger/components/composer.d.ts +17 -0
- package/dist/debugger/components/composer.js +25 -0
- package/dist/debugger/components/debugger-split.d.ts +2 -1
- package/dist/debugger/components/debugger-split.js +2 -2
- package/dist/debugger/components/event-inspector.d.ts +2 -1
- package/dist/debugger/components/event-inspector.js +25 -16
- package/dist/debugger/components/event-stream.d.ts +2 -1
- package/dist/debugger/components/event-stream.js +14 -74
- package/dist/debugger/components/follow-list.d.ts +19 -0
- package/dist/debugger/components/follow-list.js +96 -0
- package/dist/debugger/components/json-tree.d.ts +11 -0
- package/dist/debugger/components/json-tree.js +120 -0
- package/dist/debugger/components/json-view.js +2 -2
- package/dist/debugger/components/run-tree.js +53 -20
- package/dist/debugger/components/status-dot.d.ts +4 -0
- package/dist/debugger/components/status-dot.js +6 -2
- package/dist/debugger/context.d.ts +14 -0
- package/dist/debugger/context.js +17 -0
- package/dist/debugger/contracts.d.ts +7 -4
- package/dist/debugger/events/builtin-families.d.ts +3 -0
- package/dist/debugger/events/builtin-families.js +9 -0
- package/dist/debugger/events/catalog.d.ts +3 -4
- package/dist/debugger/events/catalog.js +27 -17
- package/dist/debugger/events/protocol-summarize.d.ts +1 -2
- package/dist/debugger/events/protocol-summarize.js +42 -24
- package/dist/debugger/index.d.ts +20 -6
- package/dist/debugger/index.js +19 -5
- package/dist/debugger/lib/cn.d.ts +1 -0
- package/dist/debugger/lib/cn.js +3 -0
- package/dist/debugger/lib/event-meta.d.ts +2 -3
- package/dist/debugger/lib/event-meta.js +6 -12
- package/dist/debugger/lib/json.d.ts +25 -0
- package/dist/debugger/lib/json.js +87 -0
- package/dist/debugger/lib/payload.d.ts +7 -0
- package/dist/debugger/lib/payload.js +11 -0
- package/dist/debugger/lib/time.d.ts +3 -0
- package/dist/debugger/lib/time.js +24 -0
- package/dist/debugger/plugin.d.ts +36 -0
- package/dist/debugger/plugin.js +14 -0
- package/dist/debugger/server/index.d.ts +14 -0
- package/dist/debugger/server/index.js +114 -0
- package/dist/debugger/shell/debugger-shell.d.ts +15 -0
- package/dist/debugger/shell/debugger-shell.js +43 -0
- package/dist/debugger/shell/definition-key.d.ts +3 -0
- package/dist/debugger/shell/definition-key.js +4 -0
- package/dist/debugger/shell/events-pane.d.ts +12 -0
- package/dist/debugger/shell/events-pane.js +25 -0
- package/dist/debugger/shell/projections-panel.d.ts +4 -0
- package/dist/debugger/shell/projections-panel.js +59 -0
- package/dist/debugger/shell/run-controls.d.ts +4 -0
- package/dist/debugger/shell/run-controls.js +72 -0
- package/dist/debugger/shell/run-search.d.ts +6 -0
- package/dist/debugger/shell/run-search.js +102 -0
- package/dist/debugger/shell/run-type-menu.d.ts +6 -0
- package/dist/debugger/shell/run-type-menu.js +11 -0
- package/dist/debugger/shell/schema-form.d.ts +31 -0
- package/dist/debugger/shell/schema-form.js +117 -0
- package/dist/debugger/shell/start-form.d.ts +3 -0
- package/dist/debugger/shell/start-form.js +75 -0
- package/dist/debugger/shell/use-start-run.d.ts +14 -0
- package/dist/debugger/shell/use-start-run.js +32 -0
- package/dist/debugger/shell/workspace.d.ts +6 -0
- package/dist/debugger/shell/workspace.js +15 -0
- package/dist/debugger/styles.css +111 -27
- package/dist/debugger/ui/button.d.ts +24 -0
- package/dist/debugger/ui/button.js +24 -0
- package/dist/debugger/ui/command.d.ts +9 -0
- package/dist/debugger/ui/command.js +24 -0
- package/dist/debugger/ui/dropdown-menu.d.ts +13 -0
- package/dist/debugger/ui/dropdown-menu.js +37 -0
- package/dist/debugger/ui/panel.d.ts +14 -0
- package/dist/debugger/ui/panel.js +16 -0
- package/dist/debugger/ui/popover.d.ts +7 -0
- package/dist/debugger/ui/popover.js +17 -0
- package/dist/debugger/ui/resizable.d.ts +7 -0
- package/dist/debugger/ui/resizable.js +14 -0
- package/dist/debugger/ui/scroll-area.d.ts +2 -1
- package/dist/debugger/ui/scroll-area.js +5 -3
- package/dist/debugger/ui/select.d.ts +10 -0
- package/dist/debugger/ui/select.js +21 -0
- package/dist/debugger/ui/tabs.js +2 -2
- package/dist/debugger/ui/textarea.d.ts +3 -0
- package/dist/debugger/ui/textarea.js +7 -0
- 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 +3 -3
- 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 +30 -4
- package/dist/runtime/looms.js +57 -37
- 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 +293 -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 +248 -470
- package/dist/runtime/server/auth.d.ts +41 -0
- package/dist/runtime/server/auth.js +45 -0
- package/dist/runtime/server/debugger-host.d.ts +11 -0
- package/dist/runtime/server/debugger-host.js +32 -0
- package/dist/runtime/server/fetch-handler.d.ts +18 -0
- package/dist/runtime/server/fetch-handler.js +62 -0
- package/dist/runtime/server/http.d.ts +38 -0
- package/dist/runtime/server/http.js +66 -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/definitions.d.ts +3 -0
- package/dist/runtime/server/routes/definitions.js +20 -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 +199 -0
- package/dist/runtime/server.d.ts +4 -8
- package/dist/runtime/server.js +6 -211
- package/dist/runtime/sse.d.ts +2 -1
- package/dist/runtime/sse.js +24 -17
- package/dist/runtime/types.d.ts +117 -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 +188 -161
- 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/debugger/index.d.ts +4 -0
- package/dist/workflow/debugger/index.js +11 -0
- package/dist/workflow/debugger/nodes-view.d.ts +3 -0
- package/dist/workflow/debugger/nodes-view.js +13 -0
- package/dist/workflow/debugger/summarize.d.ts +2 -0
- package/dist/workflow/debugger/summarize.js +30 -0
- 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 +26 -2
- package/dist/debugger/events/domain-summarize.d.ts +0 -3
- package/dist/debugger/events/domain-summarize.js +0 -107
- package/dist/debugger/events/family.d.ts +0 -8
- package/dist/debugger/events/family.js +0 -44
- package/dist/runtime/events-api.d.ts +0 -4
- package/dist/runtime/events-api.js +0 -84
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Data, Effect, Semaphore } from 'effect';
|
|
2
|
+
import { runProjectorEffect } from './projector.js';
|
|
3
|
+
export class ProjectorCursorError extends Data.TaggedError('ProjectorCursorError') {
|
|
4
|
+
}
|
|
5
|
+
export class UnknownProjectorError extends Data.TaggedError('UnknownProjectorError') {
|
|
6
|
+
}
|
|
7
|
+
function projectorIdentity(projector) {
|
|
8
|
+
return `${projector.name}@${projector.version}`;
|
|
9
|
+
}
|
|
10
|
+
class ProjectorDeliveryError extends Data.TaggedError('ProjectorDeliveryError') {
|
|
11
|
+
}
|
|
12
|
+
function normalizeError(cause) {
|
|
13
|
+
return new ProjectorDeliveryError({
|
|
14
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function createProjectorDelivery(source, projectors, cursors, options = {}) {
|
|
18
|
+
const batchSize = options.batchSize ?? 100;
|
|
19
|
+
const initialRetryMs = options.initialRetryMs ?? 1_000;
|
|
20
|
+
const maxRetryMs = options.maxRetryMs ?? 60_000;
|
|
21
|
+
const maxAttempts = options.maxAttempts ?? 8;
|
|
22
|
+
const now = options.now ?? Date.now;
|
|
23
|
+
const initialized = new Map();
|
|
24
|
+
const exclusive = new Map();
|
|
25
|
+
const observe = (event) => {
|
|
26
|
+
try {
|
|
27
|
+
Promise.resolve(options.observer?.observe(event)).catch(() => undefined);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Delivery correctness must not depend on telemetry.
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const initialize = (projector) => {
|
|
34
|
+
const key = projectorIdentity(projector);
|
|
35
|
+
let pending = initialized.get(key);
|
|
36
|
+
if (!pending) {
|
|
37
|
+
pending = Promise.resolve(projector.init?.()).catch((cause) => {
|
|
38
|
+
initialized.delete(key);
|
|
39
|
+
throw cause;
|
|
40
|
+
});
|
|
41
|
+
initialized.set(key, pending);
|
|
42
|
+
}
|
|
43
|
+
return pending;
|
|
44
|
+
};
|
|
45
|
+
const scheduleRetry = (at) => options.scheduleRetry?.(at) ?? Effect.void;
|
|
46
|
+
const recordFailure = (runId, projector, status, failedSeq, error) => Effect.gen(function* () {
|
|
47
|
+
const attempts = status.attempts + 1;
|
|
48
|
+
const deadLettered = attempts >= maxAttempts;
|
|
49
|
+
const nextRetryAt = deadLettered
|
|
50
|
+
? null
|
|
51
|
+
: now() + Math.min(maxRetryMs, initialRetryMs * 2 ** Math.max(0, attempts - 1));
|
|
52
|
+
yield* cursors.fail(runId, projector, {
|
|
53
|
+
attempts,
|
|
54
|
+
failedSeq,
|
|
55
|
+
nextRetryAt,
|
|
56
|
+
error: error.message,
|
|
57
|
+
now: now(),
|
|
58
|
+
});
|
|
59
|
+
const sourceTail = yield* source.tail(runId).pipe(Effect.orElseSucceed(() => status.cursor));
|
|
60
|
+
observe({
|
|
61
|
+
type: deadLettered ? 'projector.dead-letter' : 'projector.retry',
|
|
62
|
+
runId,
|
|
63
|
+
projector: projectorIdentity(projector),
|
|
64
|
+
at: now(),
|
|
65
|
+
cursor: status.cursor,
|
|
66
|
+
lag: Math.max(0, sourceTail - status.cursor),
|
|
67
|
+
attempts,
|
|
68
|
+
error: error.message,
|
|
69
|
+
});
|
|
70
|
+
if (nextRetryAt !== null) {
|
|
71
|
+
yield* scheduleRetry(nextRetryAt);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
const deliverProjector = (runId, projector, force) => Effect.gen(function* () {
|
|
75
|
+
let status = yield* cursors.get(runId, projector);
|
|
76
|
+
const currentTime = now();
|
|
77
|
+
if (status.state === 'dead-letter' ||
|
|
78
|
+
(!force && status.nextRetryAt !== null && status.nextRetryAt > currentTime)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const initialization = yield* Effect.tryPromise({
|
|
82
|
+
try: () => initialize(projector),
|
|
83
|
+
catch: normalizeError,
|
|
84
|
+
}).pipe(Effect.exit);
|
|
85
|
+
if (initialization._tag === 'Failure') {
|
|
86
|
+
yield* recordFailure(runId, projector, status, status.cursor + 1, normalizeError(initialization.cause));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
while (true) {
|
|
90
|
+
const events = yield* source.read(runId, {
|
|
91
|
+
fromSeq: status.cursor + 1,
|
|
92
|
+
limit: batchSize,
|
|
93
|
+
});
|
|
94
|
+
const first = events[0];
|
|
95
|
+
const last = events.at(-1);
|
|
96
|
+
if (first === undefined || last === undefined) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const projected = yield* runProjectorEffect(projectorIdentity(projector), () => projector.project(events)).pipe(Effect.exit);
|
|
100
|
+
if (projected._tag === 'Failure') {
|
|
101
|
+
yield* recordFailure(runId, projector, status, first.seq, normalizeError(projected.cause));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const advancedAt = now();
|
|
105
|
+
yield* cursors.advance(runId, projector, last.seq, advancedAt);
|
|
106
|
+
status = {
|
|
107
|
+
...status,
|
|
108
|
+
cursor: last.seq,
|
|
109
|
+
state: 'idle',
|
|
110
|
+
attempts: 0,
|
|
111
|
+
failedSeq: null,
|
|
112
|
+
nextRetryAt: null,
|
|
113
|
+
lastError: null,
|
|
114
|
+
};
|
|
115
|
+
const sourceTail = yield* source.tail(runId).pipe(Effect.orElseSucceed(() => last.seq));
|
|
116
|
+
observe({
|
|
117
|
+
type: 'projector.lag',
|
|
118
|
+
runId,
|
|
119
|
+
projector: projectorIdentity(projector),
|
|
120
|
+
at: advancedAt,
|
|
121
|
+
cursor: last.seq,
|
|
122
|
+
lag: Math.max(0, sourceTail - last.seq),
|
|
123
|
+
attempts: 0,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
/** One delivery at a time per run and projector; independent projectors still run in parallel. */
|
|
128
|
+
const deliverExclusive = (runId, projector, force) => {
|
|
129
|
+
const key = `${runId}:${projectorIdentity(projector)}`;
|
|
130
|
+
let semaphore = exclusive.get(key);
|
|
131
|
+
if (!semaphore) {
|
|
132
|
+
semaphore = Semaphore.makeUnsafe(1);
|
|
133
|
+
exclusive.set(key, semaphore);
|
|
134
|
+
}
|
|
135
|
+
return Semaphore.withPermits(semaphore, 1, deliverProjector(runId, projector, force));
|
|
136
|
+
};
|
|
137
|
+
const findProjector = (name, version) => projectors.find((candidate) => candidate.name === name && (version === undefined || candidate.version === version));
|
|
138
|
+
return {
|
|
139
|
+
deliver: (runId, runOptions) => Effect.forEach(projectors, (projector) => deliverExclusive(runId, projector, runOptions?.force ?? false), { concurrency: 'unbounded', discard: true }),
|
|
140
|
+
status: (runId) => cursors.list(runId),
|
|
141
|
+
requeue: (runId, projectorName, version) => {
|
|
142
|
+
const projector = findProjector(projectorName, version);
|
|
143
|
+
if (!projector) {
|
|
144
|
+
return Effect.fail(new UnknownProjectorError({
|
|
145
|
+
message: `Unknown projector ${projectorName}${version === undefined ? '' : `@${version}`}`,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
return cursors
|
|
149
|
+
.requeue(runId, projector, now())
|
|
150
|
+
.pipe(Effect.andThen(deliverExclusive(runId, projector, true)));
|
|
151
|
+
},
|
|
152
|
+
nextRetryAt: (runId) => cursors.list(runId).pipe(Effect.map((statuses) => {
|
|
153
|
+
const deadlines = statuses
|
|
154
|
+
.map((status) => status.nextRetryAt)
|
|
155
|
+
.filter((deadline) => deadline !== null);
|
|
156
|
+
return deadlines.length === 0 ? null : Math.min(...deadlines);
|
|
157
|
+
})),
|
|
158
|
+
inspect: (runId) => cursors.list(runId).pipe(Effect.flatMap((statuses) => Effect.forEach(statuses, (status) => source.tail(status.runId).pipe(Effect.map((sourceTail) => ({
|
|
159
|
+
...status,
|
|
160
|
+
sourceTail,
|
|
161
|
+
lag: Math.max(0, sourceTail - status.cursor),
|
|
162
|
+
}))), { concurrency: 'unbounded' }))),
|
|
163
|
+
recover: (runId) => Effect.gen(function* () {
|
|
164
|
+
const statuses = yield* cursors.list(runId);
|
|
165
|
+
let recovered = 0;
|
|
166
|
+
for (const status of statuses) {
|
|
167
|
+
if (status.state === 'dead-letter' || status.nextRetryAt === null) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
yield* scheduleRetry(status.nextRetryAt);
|
|
171
|
+
if (status.nextRetryAt <= now()) {
|
|
172
|
+
const projector = findProjector(status.projector, status.version);
|
|
173
|
+
if (projector) {
|
|
174
|
+
yield* deliverExclusive(status.runId, projector, true);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
recovered += 1;
|
|
178
|
+
}
|
|
179
|
+
return recovered;
|
|
180
|
+
}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -53,4 +53,4 @@ export interface IndexProjector extends Projector {
|
|
|
53
53
|
listReviews: (actorId?: string) => Promise<ReviewIndexRow[]>;
|
|
54
54
|
}
|
|
55
55
|
export declare function indexOpsFor(event: EventEnvelope): IndexOp[];
|
|
56
|
-
export declare function createIndexProjector(name: string, backend: IndexBackend): IndexProjector;
|
|
56
|
+
export declare function createIndexProjector(name: string, backend: IndexBackend, version?: string): IndexProjector;
|
|
@@ -99,17 +99,15 @@ export function indexOpsFor(event) {
|
|
|
99
99
|
return [];
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
export function createIndexProjector(name, backend) {
|
|
102
|
+
export function createIndexProjector(name, backend, version = '1') {
|
|
103
103
|
return {
|
|
104
104
|
name,
|
|
105
|
+
version,
|
|
105
106
|
init: backend.init,
|
|
106
107
|
dispose: backend.dispose,
|
|
107
|
-
project:
|
|
108
|
+
project: (events) => {
|
|
108
109
|
const ops = events.flatMap(indexOpsFor);
|
|
109
|
-
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
await backend.applyOps(ops);
|
|
110
|
+
return ops.length === 0 ? Promise.resolve() : backend.applyOps(ops);
|
|
113
111
|
},
|
|
114
112
|
getActor: (actorId) => backend.getActor(actorId),
|
|
115
113
|
listReviews: (actorId) => backend.listReviews(actorId),
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type { Projector, ProjectorErrorHandler } from './projector';
|
|
2
|
+
export { createProjectorDelivery, ProjectorCursorError, UnknownProjectorError, type ProjectorCursorStore, type ProjectorDeliveryFailure, type ProjectorDelivery, type ProjectorDeliveryOptions, type ProjectorDeliveryState, type ProjectorDeliveryStatus, type ProjectorObservation, type ProjectorObserver, type ProjectorOperationalStatus, } from './delivery';
|
|
3
|
+
export { sqliteProjectorCursorStore, SQLITE_PROJECTOR_DELIVERY_SCHEMA } from './sqlite-delivery';
|
|
2
4
|
export { withProjectors, projectEvents, ProjectorsStoreLive, type WithProjectorsOptions, } from './with-projectors';
|
|
3
5
|
export { indexOpsFor, createIndexProjector, type IndexOp, type IndexBackend, type IndexProjector, type ActorIndexRow, type ReviewIndexRow, } from './index-model';
|
|
4
6
|
export { memory, noop } from './memory';
|
package/dist/projectors/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { createProjectorDelivery, ProjectorCursorError, UnknownProjectorError, } from './delivery.js';
|
|
2
|
+
export { sqliteProjectorCursorStore, SQLITE_PROJECTOR_DELIVERY_SCHEMA } from './sqlite-delivery.js';
|
|
1
3
|
export { withProjectors, projectEvents, ProjectorsStoreLive, } from './with-projectors.js';
|
|
2
4
|
export { indexOpsFor, createIndexProjector, } from './index-model.js';
|
|
3
5
|
export { memory, noop } from './memory.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type IndexProjector } from './index-model';
|
|
2
|
-
import type
|
|
2
|
+
import { type Projector } from './projector';
|
|
3
3
|
export declare function memory(): IndexProjector;
|
|
4
4
|
/** No-op projector — safe default when indexing is disabled. */
|
|
5
5
|
export declare function noop(): Projector;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createIndexProjector, } from './index-model.js';
|
|
2
|
+
import { runProjectorSync } from './projector.js';
|
|
2
3
|
function applyOp(actors, reviews, op) {
|
|
3
4
|
switch (op.type) {
|
|
4
5
|
case 'upsertActor':
|
|
@@ -51,20 +52,20 @@ export function memory() {
|
|
|
51
52
|
const actors = new Map();
|
|
52
53
|
const reviews = new Map();
|
|
53
54
|
const backend = {
|
|
54
|
-
applyOps:
|
|
55
|
+
applyOps: (ops) => runProjectorSync('memory', () => {
|
|
55
56
|
for (const op of ops) {
|
|
56
57
|
applyOp(actors, reviews, op);
|
|
57
58
|
}
|
|
58
|
-
},
|
|
59
|
-
getActor:
|
|
60
|
-
listReviews:
|
|
59
|
+
}),
|
|
60
|
+
getActor: (actorId) => runProjectorSync('memory', () => actors.get(actorId) ?? null),
|
|
61
|
+
listReviews: (actorId) => runProjectorSync('memory', () => {
|
|
61
62
|
const rows = [...reviews.values()];
|
|
62
63
|
return actorId ? rows.filter((row) => row.actorId === actorId) : rows;
|
|
63
|
-
},
|
|
64
|
-
dispose:
|
|
64
|
+
}),
|
|
65
|
+
dispose: () => runProjectorSync('memory', () => {
|
|
65
66
|
actors.clear();
|
|
66
67
|
reviews.clear();
|
|
67
|
-
},
|
|
68
|
+
}),
|
|
68
69
|
};
|
|
69
70
|
return createIndexProjector('memory', backend);
|
|
70
71
|
}
|
|
@@ -72,6 +73,7 @@ export function memory() {
|
|
|
72
73
|
export function noop() {
|
|
73
74
|
return {
|
|
74
75
|
name: 'noop',
|
|
75
|
-
|
|
76
|
+
version: '1',
|
|
77
|
+
project: () => Promise.resolve(),
|
|
76
78
|
};
|
|
77
79
|
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
import { DateTime, Effect } from 'effect';
|
|
1
2
|
import createPostgresClient, {} from 'postgres';
|
|
2
3
|
import { createIndexProjector, } from './index-model.js';
|
|
4
|
+
import { runProjectorPromise } from './projector.js';
|
|
3
5
|
import { POSTGRES_SCHEMA_SQL } from './schema.js';
|
|
4
6
|
export { POSTGRES_SCHEMA_SQL } from './schema.js';
|
|
5
7
|
function ownsClient(options) {
|
|
6
8
|
return 'url' in options;
|
|
7
9
|
}
|
|
8
|
-
|
|
10
|
+
function applyOp(sql, op) {
|
|
11
|
+
const date = DateTime.toDateUtc(DateTime.makeUnsafe(op.updatedAt));
|
|
9
12
|
switch (op.type) {
|
|
10
13
|
case 'upsertActor':
|
|
11
|
-
|
|
14
|
+
return sql `
|
|
12
15
|
INSERT INTO looms_actors (actor_id, kind, status, definition_name, parent_actor_id, updated_at)
|
|
13
|
-
VALUES (${op.actorId}, ${op.kind}, ${op.status}, ${op.definitionName}, ${op.parentActorId}, ${
|
|
16
|
+
VALUES (${op.actorId}, ${op.kind}, ${op.status}, ${op.definitionName}, ${op.parentActorId}, ${date})
|
|
14
17
|
ON CONFLICT (actor_id) DO UPDATE SET
|
|
15
18
|
kind = EXCLUDED.kind,
|
|
16
19
|
status = EXCLUDED.status,
|
|
@@ -18,31 +21,27 @@ async function applyOp(sql, op) {
|
|
|
18
21
|
parent_actor_id = EXCLUDED.parent_actor_id,
|
|
19
22
|
updated_at = EXCLUDED.updated_at
|
|
20
23
|
`;
|
|
21
|
-
return;
|
|
22
24
|
case 'setActorStatus':
|
|
23
|
-
|
|
25
|
+
return sql `
|
|
24
26
|
INSERT INTO looms_actors (actor_id, status, updated_at)
|
|
25
|
-
VALUES (${op.actorId}, ${op.status}, ${
|
|
27
|
+
VALUES (${op.actorId}, ${op.status}, ${date})
|
|
26
28
|
ON CONFLICT (actor_id) DO UPDATE SET
|
|
27
29
|
status = EXCLUDED.status,
|
|
28
30
|
updated_at = EXCLUDED.updated_at
|
|
29
31
|
`;
|
|
30
|
-
return;
|
|
31
32
|
case 'upsertReview':
|
|
32
|
-
|
|
33
|
+
return sql `
|
|
33
34
|
INSERT INTO looms_reviews (review_id, actor_id, status, title, updated_at)
|
|
34
|
-
VALUES (${op.reviewId}, ${op.actorId}, ${op.status}, ${op.title}, ${
|
|
35
|
+
VALUES (${op.reviewId}, ${op.actorId}, ${op.status}, ${op.title}, ${date})
|
|
35
36
|
ON CONFLICT (review_id) DO UPDATE SET
|
|
36
37
|
status = EXCLUDED.status,
|
|
37
38
|
updated_at = EXCLUDED.updated_at
|
|
38
39
|
`;
|
|
39
|
-
return;
|
|
40
40
|
case 'setReviewStatus':
|
|
41
|
-
|
|
42
|
-
UPDATE looms_reviews SET status = ${op.status}, updated_at = ${
|
|
41
|
+
return sql `
|
|
42
|
+
UPDATE looms_reviews SET status = ${op.status}, updated_at = ${date}
|
|
43
43
|
WHERE review_id = ${op.reviewId}
|
|
44
44
|
`;
|
|
45
|
-
return;
|
|
46
45
|
default: {
|
|
47
46
|
const exhaustiveCheck = op;
|
|
48
47
|
return exhaustiveCheck;
|
|
@@ -53,56 +52,43 @@ export function postgres(options) {
|
|
|
53
52
|
const createdClient = ownsClient(options);
|
|
54
53
|
const sql = createdClient ? createPostgresClient(options.url) : options.sql;
|
|
55
54
|
const backend = {
|
|
56
|
-
init:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
65
|
-
},
|
|
66
|
-
getActor: async (actorId) => {
|
|
67
|
-
const rows = await sql `
|
|
68
|
-
SELECT actor_id, kind, status, definition_name, parent_actor_id, updated_at
|
|
69
|
-
FROM looms_actors WHERE actor_id = ${actorId}
|
|
70
|
-
`;
|
|
55
|
+
init: () => runProjectorPromise('postgres', () => sql.unsafe(POSTGRES_SCHEMA_SQL)).then(() => undefined),
|
|
56
|
+
applyOps: (ops) => runProjectorPromise('postgres', () => sql.begin((tx) => Effect.runPromise(Effect.forEach(ops, (op) => Effect.promise(() => Promise.resolve(applyOp(tx, op))), {
|
|
57
|
+
discard: true,
|
|
58
|
+
})))).then(() => undefined),
|
|
59
|
+
getActor: (actorId) => runProjectorPromise('postgres', () => sql `
|
|
60
|
+
SELECT actor_id, kind, status, definition_name, parent_actor_id, updated_at
|
|
61
|
+
FROM looms_actors WHERE actor_id = ${actorId}
|
|
62
|
+
`).then((rows) => {
|
|
71
63
|
const row = rows[0];
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const rows = actorId
|
|
86
|
-
? await sql `
|
|
64
|
+
return row
|
|
65
|
+
? {
|
|
66
|
+
actorId: row.actor_id,
|
|
67
|
+
kind: row.kind,
|
|
68
|
+
status: row.status,
|
|
69
|
+
definitionName: row.definition_name,
|
|
70
|
+
parentActorId: row.parent_actor_id,
|
|
71
|
+
updatedAt: row.updated_at.getTime(),
|
|
72
|
+
}
|
|
73
|
+
: null;
|
|
74
|
+
}),
|
|
75
|
+
listReviews: (actorId) => runProjectorPromise('postgres', () => actorId
|
|
76
|
+
? sql `
|
|
87
77
|
SELECT review_id, actor_id, status, title, updated_at
|
|
88
78
|
FROM looms_reviews WHERE actor_id = ${actorId}
|
|
89
79
|
`
|
|
90
|
-
|
|
80
|
+
: sql `
|
|
91
81
|
SELECT review_id, actor_id, status, title, updated_at FROM looms_reviews
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (createdClient) {
|
|
103
|
-
await sql.end({ timeout: 5 });
|
|
104
|
-
}
|
|
105
|
-
},
|
|
82
|
+
`).then((rows) => rows.map((row) => ({
|
|
83
|
+
reviewId: row.review_id,
|
|
84
|
+
actorId: row.actor_id,
|
|
85
|
+
status: row.status,
|
|
86
|
+
title: row.title,
|
|
87
|
+
updatedAt: row.updated_at.getTime(),
|
|
88
|
+
}))),
|
|
89
|
+
dispose: () => createdClient
|
|
90
|
+
? runProjectorPromise('postgres', () => sql.end({ timeout: 5 })).then(() => undefined)
|
|
91
|
+
: Promise.resolve(),
|
|
106
92
|
};
|
|
107
93
|
return createIndexProjector('postgres', backend);
|
|
108
94
|
}
|
|
@@ -1,7 +1,23 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
1
2
|
import type { EventEnvelope } from '@swirls/looms/core';
|
|
3
|
+
declare const ProjectorError_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 & {
|
|
4
|
+
readonly _tag: "ProjectorError";
|
|
5
|
+
} & Readonly<A>;
|
|
6
|
+
export declare class ProjectorError extends ProjectorError_base<{
|
|
7
|
+
readonly projector: string;
|
|
8
|
+
readonly cause: unknown;
|
|
9
|
+
readonly message: string;
|
|
10
|
+
}> {
|
|
11
|
+
constructor(projector: string, cause: unknown);
|
|
12
|
+
}
|
|
13
|
+
export declare function runProjectorEffect<A>(projector: string, operation: () => PromiseLike<A>): Effect.Effect<A, ProjectorError>;
|
|
14
|
+
export declare function runProjectorSync<A>(projector: string, operation: () => A): Promise<A>;
|
|
15
|
+
export declare function runProjectorPromise<A>(projector: string, operation: () => PromiseLike<A>): Promise<A>;
|
|
2
16
|
export interface Projector {
|
|
3
17
|
/** Used in error reporting / diagnostics. */
|
|
4
18
|
readonly name: string;
|
|
19
|
+
/** Changes when projection semantics or its target schema changes. */
|
|
20
|
+
readonly version: string;
|
|
5
21
|
/** Optional one-time setup (open connection, create tables). Runtime calls this before the first `project`. */
|
|
6
22
|
readonly init?: () => Promise<void>;
|
|
7
23
|
/** Apply a batch of events in log order. Must be idempotent (a batch may be redelivered). */
|
|
@@ -9,3 +25,4 @@ export interface Projector {
|
|
|
9
25
|
readonly dispose?: () => Promise<void>;
|
|
10
26
|
}
|
|
11
27
|
export type ProjectorErrorHandler = (error: Error, projector: Projector, events: ReadonlyArray<EventEnvelope>) => void;
|
|
28
|
+
export {};
|
|
@@ -1 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
import { Data, Effect } from 'effect';
|
|
2
|
+
export class ProjectorError extends Data.TaggedError('ProjectorError') {
|
|
3
|
+
constructor(projector, cause) {
|
|
4
|
+
super({
|
|
5
|
+
projector,
|
|
6
|
+
cause,
|
|
7
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
8
|
+
});
|
|
9
|
+
this.name = 'ProjectorError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function runProjectorEffect(projector, operation) {
|
|
13
|
+
return Effect.tryPromise({
|
|
14
|
+
try: () => Promise.resolve(operation()),
|
|
15
|
+
catch: (cause) => new ProjectorError(projector, cause),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function runProjectorSync(projector, operation) {
|
|
19
|
+
return Effect.runPromise(Effect.try({
|
|
20
|
+
try: operation,
|
|
21
|
+
catch: (cause) => new ProjectorError(projector, cause),
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
export function runProjectorPromise(projector, operation) {
|
|
25
|
+
return Effect.runPromise(runProjectorEffect(projector, operation));
|
|
26
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SqliteExec } from '@swirls/looms/core/sqlite-store';
|
|
2
|
+
import { type ProjectorCursorStore } from './delivery';
|
|
3
|
+
export declare const SQLITE_PROJECTOR_DELIVERY_SCHEMA: string;
|
|
4
|
+
export declare function sqliteProjectorCursorStore(exec: SqliteExec): ProjectorCursorStore;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Effect, Schema } from 'effect';
|
|
2
|
+
import { ProjectorCursorError, } from './delivery.js';
|
|
3
|
+
export const SQLITE_PROJECTOR_DELIVERY_SCHEMA = `
|
|
4
|
+
CREATE TABLE IF NOT EXISTS looms_projector_delivery (
|
|
5
|
+
run_id TEXT NOT NULL,
|
|
6
|
+
projector_name TEXT NOT NULL,
|
|
7
|
+
projector_version TEXT NOT NULL,
|
|
8
|
+
cursor INTEGER NOT NULL DEFAULT 0,
|
|
9
|
+
state TEXT NOT NULL DEFAULT 'idle',
|
|
10
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
11
|
+
failed_seq INTEGER,
|
|
12
|
+
next_retry_at INTEGER,
|
|
13
|
+
last_error TEXT,
|
|
14
|
+
updated_at INTEGER NOT NULL,
|
|
15
|
+
PRIMARY KEY (run_id, projector_name, projector_version)
|
|
16
|
+
);
|
|
17
|
+
CREATE INDEX IF NOT EXISTS looms_projector_delivery_retry_idx
|
|
18
|
+
ON looms_projector_delivery (next_retry_at)
|
|
19
|
+
`.trim();
|
|
20
|
+
const DeliveryRowSchema = Schema.Struct({
|
|
21
|
+
run_id: Schema.String,
|
|
22
|
+
projector_name: Schema.String,
|
|
23
|
+
projector_version: Schema.String,
|
|
24
|
+
cursor: Schema.Finite,
|
|
25
|
+
state: Schema.String,
|
|
26
|
+
attempts: Schema.Finite,
|
|
27
|
+
failed_seq: Schema.NullOr(Schema.Finite),
|
|
28
|
+
next_retry_at: Schema.NullOr(Schema.Finite),
|
|
29
|
+
last_error: Schema.NullOr(Schema.String),
|
|
30
|
+
updated_at: Schema.Finite,
|
|
31
|
+
});
|
|
32
|
+
const decodeDeliveryRow = Schema.decodeUnknownSync(DeliveryRowSchema);
|
|
33
|
+
function parseState(value) {
|
|
34
|
+
switch (value) {
|
|
35
|
+
case 'idle':
|
|
36
|
+
case 'retrying':
|
|
37
|
+
case 'dead-letter':
|
|
38
|
+
return value;
|
|
39
|
+
default:
|
|
40
|
+
throw new Error(`Invalid projector delivery state: ${value}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function deliveryStatus(row) {
|
|
44
|
+
return {
|
|
45
|
+
runId: row.run_id,
|
|
46
|
+
projector: row.projector_name,
|
|
47
|
+
version: row.projector_version,
|
|
48
|
+
cursor: row.cursor,
|
|
49
|
+
state: parseState(row.state),
|
|
50
|
+
attempts: row.attempts,
|
|
51
|
+
failedSeq: row.failed_seq,
|
|
52
|
+
nextRetryAt: row.next_retry_at,
|
|
53
|
+
lastError: row.last_error,
|
|
54
|
+
updatedAt: row.updated_at,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function initialize(exec) {
|
|
58
|
+
for (const statement of SQLITE_PROJECTOR_DELIVERY_SCHEMA.split(';')) {
|
|
59
|
+
const sql = statement.trim();
|
|
60
|
+
if (sql.length > 0) {
|
|
61
|
+
exec.run(sql);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function query(run) {
|
|
66
|
+
return Effect.try({
|
|
67
|
+
try: run,
|
|
68
|
+
catch: (cause) => new ProjectorCursorError({
|
|
69
|
+
message: cause instanceof Error ? cause.message : 'Projector cursor query failed',
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export function sqliteProjectorCursorStore(exec) {
|
|
74
|
+
initialize(exec);
|
|
75
|
+
const get = (runId, projector) => {
|
|
76
|
+
const [row] = exec.rows(`SELECT run_id, projector_name, projector_version, cursor, state, attempts,
|
|
77
|
+
failed_seq, next_retry_at, last_error, updated_at
|
|
78
|
+
FROM looms_projector_delivery
|
|
79
|
+
WHERE run_id = ? AND projector_name = ? AND projector_version = ?`, [runId, projector.name, projector.version]);
|
|
80
|
+
return row === undefined
|
|
81
|
+
? {
|
|
82
|
+
runId,
|
|
83
|
+
projector: projector.name,
|
|
84
|
+
version: projector.version,
|
|
85
|
+
cursor: 0,
|
|
86
|
+
state: 'idle',
|
|
87
|
+
attempts: 0,
|
|
88
|
+
failedSeq: null,
|
|
89
|
+
nextRetryAt: null,
|
|
90
|
+
lastError: null,
|
|
91
|
+
updatedAt: 0,
|
|
92
|
+
}
|
|
93
|
+
: deliveryStatus(decodeDeliveryRow(row));
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
get: (runId, projector) => query(() => get(runId, projector)),
|
|
97
|
+
advance: (runId, projector, cursor, now) => query(() => {
|
|
98
|
+
exec.run(`INSERT INTO looms_projector_delivery
|
|
99
|
+
(run_id, projector_name, projector_version, cursor, updated_at)
|
|
100
|
+
VALUES (?, ?, ?, ?, ?)
|
|
101
|
+
ON CONFLICT (run_id, projector_name, projector_version) DO UPDATE
|
|
102
|
+
SET cursor = MAX(cursor, excluded.cursor), state = 'idle', attempts = 0,
|
|
103
|
+
failed_seq = NULL, next_retry_at = NULL, last_error = NULL,
|
|
104
|
+
updated_at = excluded.updated_at`, [runId, projector.name, projector.version, cursor, now]);
|
|
105
|
+
}),
|
|
106
|
+
fail: (runId, projector, failure) => query(() => {
|
|
107
|
+
exec.run(`INSERT INTO looms_projector_delivery
|
|
108
|
+
(run_id, projector_name, projector_version, state, attempts, failed_seq,
|
|
109
|
+
next_retry_at, last_error, updated_at)
|
|
110
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
111
|
+
ON CONFLICT (run_id, projector_name, projector_version) DO UPDATE
|
|
112
|
+
SET state = excluded.state, attempts = excluded.attempts,
|
|
113
|
+
failed_seq = excluded.failed_seq, next_retry_at = excluded.next_retry_at,
|
|
114
|
+
last_error = excluded.last_error, updated_at = excluded.updated_at
|
|
115
|
+
WHERE cursor < excluded.failed_seq`, [
|
|
116
|
+
runId,
|
|
117
|
+
projector.name,
|
|
118
|
+
projector.version,
|
|
119
|
+
failure.nextRetryAt === null ? 'dead-letter' : 'retrying',
|
|
120
|
+
failure.attempts,
|
|
121
|
+
failure.failedSeq,
|
|
122
|
+
failure.nextRetryAt,
|
|
123
|
+
failure.error,
|
|
124
|
+
failure.now,
|
|
125
|
+
]);
|
|
126
|
+
}),
|
|
127
|
+
requeue: (runId, projector, now) => query(() => {
|
|
128
|
+
exec.run(`INSERT INTO looms_projector_delivery
|
|
129
|
+
(run_id, projector_name, projector_version, state, next_retry_at, updated_at)
|
|
130
|
+
VALUES (?, ?, ?, 'retrying', ?, ?)
|
|
131
|
+
ON CONFLICT (run_id, projector_name, projector_version) DO UPDATE
|
|
132
|
+
SET state = 'retrying', attempts = 0, next_retry_at = excluded.next_retry_at,
|
|
133
|
+
last_error = NULL, updated_at = excluded.updated_at`, [runId, projector.name, projector.version, now, now]);
|
|
134
|
+
}),
|
|
135
|
+
list: (runId) => {
|
|
136
|
+
const params = runId === undefined ? [] : [runId];
|
|
137
|
+
const where = runId === undefined ? '' : 'WHERE run_id = ?';
|
|
138
|
+
return query(() => exec
|
|
139
|
+
.rows(`SELECT run_id, projector_name, projector_version, cursor, state, attempts,
|
|
140
|
+
failed_seq, next_retry_at, last_error, updated_at
|
|
141
|
+
FROM looms_projector_delivery ${where}
|
|
142
|
+
ORDER BY run_id, projector_name, projector_version`, params)
|
|
143
|
+
.map((row) => decodeDeliveryRow(row))
|
|
144
|
+
.map(deliveryStatus));
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|