@smithers-orchestrator/db 0.16.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/LICENSE +21 -0
- package/package.json +43 -0
- package/src/JsonBounds.ts +6 -0
- package/src/SchemaRegistryEntry.ts +6 -0
- package/src/SqlMessageStorage.js +818 -0
- package/src/SqlMessageStorageEventHistoryQuery.ts +7 -0
- package/src/SqliteWriteRetryOptions.ts +7 -0
- package/src/adapter/AlertRow.ts +29 -0
- package/src/adapter/AlertSeverity.ts +2 -0
- package/src/adapter/AlertStatus.ts +2 -0
- package/src/adapter/ApprovalRow.ts +13 -0
- package/src/adapter/AttemptRow.ts +17 -0
- package/src/adapter/CacheRow.ts +12 -0
- package/src/adapter/DB_ALERT_ALLOWED_SEVERITIES.js +5 -0
- package/src/adapter/DB_ALERT_ALLOWED_STATUSES.js +6 -0
- package/src/adapter/DB_ALERT_ID_MAX_LENGTH.js +1 -0
- package/src/adapter/DB_ALERT_MESSAGE_MAX_LENGTH.js +1 -0
- package/src/adapter/DB_ALERT_POLICY_NAME_MAX_LENGTH.js +1 -0
- package/src/adapter/DB_RUN_ALLOWED_STATUSES.js +10 -0
- package/src/adapter/DB_RUN_ID_MAX_LENGTH.js +1 -0
- package/src/adapter/DB_RUN_WORKFLOW_NAME_MAX_LENGTH.js +1 -0
- package/src/adapter/EventHistoryQuery.ts +7 -0
- package/src/adapter/HumanRequestRow.ts +19 -0
- package/src/adapter/NodeDiffCacheRow.ts +9 -0
- package/src/adapter/NodeRow.ts +10 -0
- package/src/adapter/PendingHumanRequestRow.ts +7 -0
- package/src/adapter/RunAncestryRow.ts +5 -0
- package/src/adapter/RunRow.ts +21 -0
- package/src/adapter/SignalQuery.ts +6 -0
- package/src/adapter/SignalRow.ts +9 -0
- package/src/adapter/SmithersDb.js +2236 -0
- package/src/adapter/StaleRunRecord.ts +7 -0
- package/src/adapter/index.js +27 -0
- package/src/adapter.js +2359 -0
- package/src/assertJsonPayloadWithinBounds.js +94 -0
- package/src/assertMaxBytes.js +23 -0
- package/src/assertMaxJsonDepth.js +40 -0
- package/src/assertMaxStringLength.js +16 -0
- package/src/assertOptionalArrayMaxLength.js +16 -0
- package/src/assertOptionalStringMaxLength.js +11 -0
- package/src/assertPositiveFiniteInteger.js +14 -0
- package/src/assertPositiveFiniteNumber.js +12 -0
- package/src/buildHumanRequestId.js +9 -0
- package/src/cache/nodeDiffCache.js +124 -0
- package/src/ensure.js +18 -0
- package/src/ensureSqlMessageStorage.js +11 -0
- package/src/ensureSqlMessageStorageEffect.js +12 -0
- package/src/frame-codec/FRAME_KEYFRAME_INTERVAL.js +1 -0
- package/src/frame-codec/FrameDelta.ts +6 -0
- package/src/frame-codec/FrameDeltaOp.ts +20 -0
- package/src/frame-codec/FrameEncoding.ts +1 -0
- package/src/frame-codec/JsonPath.ts +3 -0
- package/src/frame-codec/JsonPathSegment.ts +1 -0
- package/src/frame-codec/applyFrameDelta.js +143 -0
- package/src/frame-codec/applyFrameDeltaJson.js +10 -0
- package/src/frame-codec/encodeFrameDelta.js +247 -0
- package/src/frame-codec/index.js +15 -0
- package/src/frame-codec/normalizeFrameEncoding.js +13 -0
- package/src/frame-codec/parseFrameDelta.js +27 -0
- package/src/frame-codec/serializeFrameDelta.js +9 -0
- package/src/frame-codec.js +409 -0
- package/src/getSqlMessageStorage.js +11 -0
- package/src/index.d.ts +5203 -0
- package/src/index.js +20 -0
- package/src/input-bounds.js +12 -0
- package/src/input.js +17 -0
- package/src/internal-schema/index.js +19 -0
- package/src/internal-schema/smithersAlerts.js +27 -0
- package/src/internal-schema/smithersApprovals.js +18 -0
- package/src/internal-schema/smithersAttempts.js +20 -0
- package/src/internal-schema/smithersCache.js +13 -0
- package/src/internal-schema/smithersCron.js +11 -0
- package/src/internal-schema/smithersEvents.js +10 -0
- package/src/internal-schema/smithersFrames.js +14 -0
- package/src/internal-schema/smithersHumanRequests.js +17 -0
- package/src/internal-schema/smithersNodeDiffs.js +12 -0
- package/src/internal-schema/smithersNodes.js +13 -0
- package/src/internal-schema/smithersRalph.js +10 -0
- package/src/internal-schema/smithersRuns.js +22 -0
- package/src/internal-schema/smithersSandboxes.js +16 -0
- package/src/internal-schema/smithersSignals.js +12 -0
- package/src/internal-schema/smithersTimeTravelAudit.js +12 -0
- package/src/internal-schema/smithersToolCalls.js +19 -0
- package/src/internal-schema/smithersVectors.js +12 -0
- package/src/internal-schema.js +245 -0
- package/src/isRetryableSqliteWriteError.js +53 -0
- package/src/loadInputEffect.js +28 -0
- package/src/loadOutputsEffect.js +87 -0
- package/src/output/OutputKey.ts +1 -0
- package/src/output/buildKeyWhere.js +17 -0
- package/src/output/buildOutputRow.js +34 -0
- package/src/output/describeSchemaShape.js +70 -0
- package/src/output/getAgentOutputSchema.js +13 -0
- package/src/output/getKeyColumns.js +19 -0
- package/src/output/index.js +14 -0
- package/src/output/selectOutputRowEffect.js +30 -0
- package/src/output/stripAutoColumns.js +10 -0
- package/src/output/upsertOutputRowEffect.js +38 -0
- package/src/output/validateExistingOutput.js +17 -0
- package/src/output/validateOutput.js +17 -0
- package/src/output-schema-descriptor.js +163 -0
- package/src/output.js +240 -0
- package/src/react-output.js +10 -0
- package/src/runState/ComputeRunStateOptions.ts +4 -0
- package/src/runState/DeriveRunStateInput.ts +10 -0
- package/src/runState/RUN_STATE_HEARTBEAT_STALE_MS.js +1 -0
- package/src/runState/ReasonBlocked.ts +10 -0
- package/src/runState/ReasonUnhealthy.ts +6 -0
- package/src/runState/RunState.ts +12 -0
- package/src/runState/RunStateView.ts +11 -0
- package/src/runState/computeRunState.js +22 -0
- package/src/runState/computeRunStateFromRow.js +102 -0
- package/src/runState/deriveRunState.js +109 -0
- package/src/runState/parseEventMeta.js +18 -0
- package/src/runState/parseTimerMeta.js +16 -0
- package/src/runState-types.ts +23 -0
- package/src/runState.js +7 -0
- package/src/schema-signature.js +22 -0
- package/src/snapshot.js +125 -0
- package/src/sql-message-storage.js +839 -0
- package/src/storage/InMemoryStorage.js +484 -0
- package/src/storage/StorageService.js +7 -0
- package/src/storage/StorageServiceShape.ts +122 -0
- package/src/storage/StorageServiceTypes.ts +150 -0
- package/src/unwrapZodType.js +17 -0
- package/src/utils/camelToSnake.js +6 -0
- package/src/withSqliteWriteRetryEffect.js +110 -0
- package/src/write-retry.js +49 -0
- package/src/zodToCreateTableSQL.js +41 -0
- package/src/zodToTable.js +60 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
export type JsonRecord = Record<string, unknown>;
|
|
2
|
+
export type OutputKey = Record<string, string | number | boolean | null>;
|
|
3
|
+
|
|
4
|
+
export type Run = {
|
|
5
|
+
readonly runId: string;
|
|
6
|
+
readonly parentRunId?: string | null;
|
|
7
|
+
readonly workflowName?: string | null;
|
|
8
|
+
readonly workflowPath?: string | null;
|
|
9
|
+
readonly workflowHash?: string | null;
|
|
10
|
+
readonly status: string;
|
|
11
|
+
readonly createdAtMs?: number;
|
|
12
|
+
readonly startedAtMs?: number | null;
|
|
13
|
+
readonly finishedAtMs?: number | null;
|
|
14
|
+
readonly heartbeatAtMs?: number | null;
|
|
15
|
+
readonly runtimeOwnerId?: string | null;
|
|
16
|
+
readonly cancelRequestedAtMs?: number | null;
|
|
17
|
+
readonly hijackRequestedAtMs?: number | null;
|
|
18
|
+
readonly hijackTarget?: string | null;
|
|
19
|
+
readonly vcsType?: string | null;
|
|
20
|
+
readonly vcsRoot?: string | null;
|
|
21
|
+
readonly vcsRevision?: string | null;
|
|
22
|
+
readonly errorJson?: string | null;
|
|
23
|
+
readonly configJson?: string | null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type RunPatch = Partial<Omit<Run, "runId">>;
|
|
27
|
+
|
|
28
|
+
export type RunAncestryRow = {
|
|
29
|
+
readonly runId: string;
|
|
30
|
+
readonly parentRunId: string | null;
|
|
31
|
+
readonly depth: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type Attempt = {
|
|
35
|
+
readonly runId: string;
|
|
36
|
+
readonly nodeId: string;
|
|
37
|
+
readonly iteration: number;
|
|
38
|
+
readonly attempt: number;
|
|
39
|
+
readonly state: string;
|
|
40
|
+
readonly startedAtMs: number;
|
|
41
|
+
readonly finishedAtMs?: number | null;
|
|
42
|
+
readonly heartbeatAtMs?: number | null;
|
|
43
|
+
readonly heartbeatDataJson?: string | null;
|
|
44
|
+
readonly errorJson?: string | null;
|
|
45
|
+
readonly jjPointer?: string | null;
|
|
46
|
+
readonly responseText?: string | null;
|
|
47
|
+
readonly jjCwd?: string | null;
|
|
48
|
+
readonly cached?: boolean;
|
|
49
|
+
readonly metaJson?: string | null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type AttemptPatch = Partial<
|
|
53
|
+
Omit<Attempt, "runId" | "nodeId" | "iteration" | "attempt">
|
|
54
|
+
>;
|
|
55
|
+
|
|
56
|
+
export type FrameRow = {
|
|
57
|
+
readonly runId: string;
|
|
58
|
+
readonly frameNo: number;
|
|
59
|
+
readonly xmlJson?: string | null;
|
|
60
|
+
readonly graphJson?: string | null;
|
|
61
|
+
readonly xmlHash?: string | null;
|
|
62
|
+
readonly xmlEncoding?: string | null;
|
|
63
|
+
readonly createdAtMs?: number;
|
|
64
|
+
readonly [key: string]: unknown;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type SignalInsertRow = {
|
|
68
|
+
readonly runId: string;
|
|
69
|
+
readonly signalName: string;
|
|
70
|
+
readonly correlationId: string | null;
|
|
71
|
+
readonly payloadJson: string;
|
|
72
|
+
readonly receivedAtMs: number;
|
|
73
|
+
readonly receivedBy?: string | null;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type EventRow = {
|
|
77
|
+
readonly runId: string;
|
|
78
|
+
readonly seq: number;
|
|
79
|
+
readonly timestampMs: number;
|
|
80
|
+
readonly type: string;
|
|
81
|
+
readonly payloadJson: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type EventInsertRow = Omit<EventRow, "seq">;
|
|
85
|
+
|
|
86
|
+
export type RalphRow = {
|
|
87
|
+
readonly runId: string;
|
|
88
|
+
readonly ralphId: string;
|
|
89
|
+
readonly iteration: number;
|
|
90
|
+
readonly done?: boolean;
|
|
91
|
+
readonly stateJson?: string | null;
|
|
92
|
+
readonly updatedAtMs?: number;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type SandboxRow = {
|
|
96
|
+
readonly runId: string;
|
|
97
|
+
readonly sandboxId: string;
|
|
98
|
+
readonly [key: string]: unknown;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export type ToolCallRow = {
|
|
102
|
+
readonly runId: string;
|
|
103
|
+
readonly nodeId: string;
|
|
104
|
+
readonly iteration: number;
|
|
105
|
+
readonly [key: string]: unknown;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export type CronRow = {
|
|
109
|
+
readonly cronId: string;
|
|
110
|
+
readonly pattern?: string;
|
|
111
|
+
readonly workflowPath?: string;
|
|
112
|
+
readonly enabled?: boolean;
|
|
113
|
+
readonly lastRunAtMs?: number | null;
|
|
114
|
+
readonly nextRunAtMs?: number | null;
|
|
115
|
+
readonly errorJson?: string | null;
|
|
116
|
+
readonly [key: string]: unknown;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export type ScorerResultRow = {
|
|
120
|
+
readonly runId: string;
|
|
121
|
+
readonly nodeId?: string;
|
|
122
|
+
readonly scorerId?: string;
|
|
123
|
+
readonly scoredAtMs?: number;
|
|
124
|
+
readonly [key: string]: unknown;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export type ClaimRunForResumeParams = {
|
|
128
|
+
readonly runId: string;
|
|
129
|
+
readonly expectedStatus?: string;
|
|
130
|
+
readonly expectedRuntimeOwnerId: string | null;
|
|
131
|
+
readonly expectedHeartbeatAtMs: number | null;
|
|
132
|
+
readonly staleBeforeMs: number;
|
|
133
|
+
readonly claimOwnerId: string;
|
|
134
|
+
readonly claimHeartbeatAtMs: number;
|
|
135
|
+
readonly requireStale?: boolean;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export type ReleaseRunResumeClaimParams = {
|
|
139
|
+
readonly runId: string;
|
|
140
|
+
readonly claimOwnerId: string;
|
|
141
|
+
readonly restoreRuntimeOwnerId: string | null;
|
|
142
|
+
readonly restoreHeartbeatAtMs: number | null;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export type UpdateClaimedRunParams = {
|
|
146
|
+
readonly runId: string;
|
|
147
|
+
readonly expectedRuntimeOwnerId: string;
|
|
148
|
+
readonly expectedHeartbeatAtMs: number | null;
|
|
149
|
+
readonly patch: RunPatch;
|
|
150
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unwraps Zod wrapper types (nullable, optional, default) to get the base type.
|
|
3
|
+
*/
|
|
4
|
+
export function unwrapZodType(t) {
|
|
5
|
+
if (!t)
|
|
6
|
+
return t;
|
|
7
|
+
if (t._zod?.def) {
|
|
8
|
+
const typeName = t._zod.def.type;
|
|
9
|
+
if (typeName === "nullable" ||
|
|
10
|
+
typeName === "optional" ||
|
|
11
|
+
typeName === "default") {
|
|
12
|
+
const inner = t._zod.def.innerType;
|
|
13
|
+
return inner ? unwrapZodType(inner) : t;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return t;
|
|
17
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Duration, Effect, Metric, Schedule, ScheduleDecision, ScheduleIntervals, } from "effect";
|
|
2
|
+
import { dbRetries } from "@smithers-orchestrator/observability/metrics";
|
|
3
|
+
import { retryPolicyToSchedule } from "@smithers-orchestrator/scheduler/retryPolicyToSchedule";
|
|
4
|
+
import { isRetryableSqliteWriteError } from "./isRetryableSqliteWriteError.js";
|
|
5
|
+
/** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} SmithersError */
|
|
6
|
+
/** @typedef {import("./SqliteWriteRetryOptions.ts").SqliteWriteRetryOptions} SqliteWriteRetryOptions */
|
|
7
|
+
|
|
8
|
+
// Raised from 6→10 and 2000→10000: concurrent Worktree tasks can produce
|
|
9
|
+
// short bursts of SQLITE_IOERR_VNODE on macOS; more retries with a wider
|
|
10
|
+
// window gives busy_timeout time to clear the VFS lock.
|
|
11
|
+
const DEFAULT_MAX_ATTEMPTS = 10;
|
|
12
|
+
const DEFAULT_BASE_DELAY_MS = 50;
|
|
13
|
+
const DEFAULT_MAX_DELAY_MS = 10_000;
|
|
14
|
+
/**
|
|
15
|
+
* @param {unknown} error
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
function describeSqliteWriteError(error) {
|
|
19
|
+
const metadata = findSqliteErrorMetadata(error);
|
|
20
|
+
const code = metadata?.code ?? "";
|
|
21
|
+
const message = metadata?.message || String(error?.message ?? error ?? "unknown error");
|
|
22
|
+
return code ? `${code}: ${message}` : message;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* @param {unknown} error
|
|
26
|
+
* @returns {SqliteErrorMetadata | null}
|
|
27
|
+
*/
|
|
28
|
+
function readSqliteErrorMetadata(error) {
|
|
29
|
+
if (!error || (typeof error !== "object" && !(error instanceof Error))) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const code = typeof error?.code === "string" ? error.code : "";
|
|
33
|
+
const message = String(error?.message ?? "");
|
|
34
|
+
return { code, message };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* @param {unknown} error
|
|
38
|
+
* @returns {SqliteErrorMetadata | null}
|
|
39
|
+
*/
|
|
40
|
+
function findSqliteErrorMetadata(error) {
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
let current = error;
|
|
43
|
+
while (current && !seen.has(current)) {
|
|
44
|
+
seen.add(current);
|
|
45
|
+
const metadata = readSqliteErrorMetadata(current);
|
|
46
|
+
if (metadata) {
|
|
47
|
+
const message = metadata.message.toLowerCase();
|
|
48
|
+
if (metadata.code.startsWith("SQLITE_BUSY") ||
|
|
49
|
+
metadata.code.startsWith("SQLITE_IOERR") ||
|
|
50
|
+
message.includes("database is locked") ||
|
|
51
|
+
message.includes("database is busy") ||
|
|
52
|
+
message.includes("disk i/o error")) {
|
|
53
|
+
return metadata;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
current = current?.cause;
|
|
57
|
+
}
|
|
58
|
+
return readSqliteErrorMetadata(error);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* @param {number} maxAttempts
|
|
62
|
+
* @param {number} baseDelayMs
|
|
63
|
+
* @param {number} maxDelayMs
|
|
64
|
+
*/
|
|
65
|
+
function makeSqliteRetrySchedule(maxAttempts, baseDelayMs, maxDelayMs) {
|
|
66
|
+
const boundedBaseDelayMs = Math.max(1, Math.floor(baseDelayMs));
|
|
67
|
+
const boundedMaxDelayMs = Math.max(1, Math.floor(maxDelayMs));
|
|
68
|
+
return retryPolicyToSchedule({
|
|
69
|
+
backoff: "exponential",
|
|
70
|
+
initialDelayMs: boundedBaseDelayMs,
|
|
71
|
+
}).pipe(Schedule.modifyDelay((_, delay) => Duration.millis(Math.min(boundedMaxDelayMs, Duration.toMillis(delay)))), Schedule.jitteredWith({ min: 0.75, max: 1.25 }), Schedule.whileInput(isRetryableSqliteWriteError), Schedule.intersect(Schedule.recurs(Math.max(0, maxAttempts - 1))));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* @template A
|
|
75
|
+
* @param {() => Effect.Effect<A, SmithersError>} operation
|
|
76
|
+
* @param {SqliteWriteRetryOptions} [opts]
|
|
77
|
+
* @returns {Effect.Effect<A, SmithersError>}
|
|
78
|
+
*/
|
|
79
|
+
export function withSqliteWriteRetryEffect(operation, opts = {}) {
|
|
80
|
+
const { label = "sqlite write", maxAttempts = DEFAULT_MAX_ATTEMPTS, baseDelayMs = DEFAULT_BASE_DELAY_MS, maxDelayMs = DEFAULT_MAX_DELAY_MS, sleep, } = opts;
|
|
81
|
+
const boundedMaxAttempts = Math.max(1, Math.floor(maxAttempts));
|
|
82
|
+
let retryAttempt = 0;
|
|
83
|
+
let lastRetryError;
|
|
84
|
+
const retrySchedule = Schedule.mapInput(makeSqliteRetrySchedule(boundedMaxAttempts, baseDelayMs, maxDelayMs), (error) => {
|
|
85
|
+
lastRetryError = error;
|
|
86
|
+
return error;
|
|
87
|
+
}).pipe(Schedule.onDecision((_, decision) => {
|
|
88
|
+
if (ScheduleDecision.isDone(decision) || !lastRetryError) {
|
|
89
|
+
return Effect.void;
|
|
90
|
+
}
|
|
91
|
+
retryAttempt += 1;
|
|
92
|
+
const delayMs = Math.max(1, Math.round(ScheduleIntervals.start(decision.intervals) - Date.now()));
|
|
93
|
+
return Effect.gen(function* () {
|
|
94
|
+
yield* Metric.increment(dbRetries);
|
|
95
|
+
yield* Effect.logWarning(`${label} failed with ${describeSqliteWriteError(lastRetryError)}; retrying in ${delayMs}ms (${retryAttempt}/${boundedMaxAttempts})`);
|
|
96
|
+
if (sleep) {
|
|
97
|
+
yield* Effect.promise(() => sleep(delayMs));
|
|
98
|
+
}
|
|
99
|
+
}).pipe(Effect.annotateLogs({
|
|
100
|
+
retryable: true,
|
|
101
|
+
retryAttempt,
|
|
102
|
+
retryMaxAttempts: boundedMaxAttempts,
|
|
103
|
+
retryDelayMs: delayMs,
|
|
104
|
+
retryLabel: label,
|
|
105
|
+
}));
|
|
106
|
+
}));
|
|
107
|
+
return Effect.suspend(operation).pipe(Effect.retry(sleep
|
|
108
|
+
? Schedule.modifyDelay(retrySchedule, () => Duration.zero)
|
|
109
|
+
: retrySchedule), Effect.withLogSpan("sqlite-write-retry"));
|
|
110
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { isRetryableSqliteWriteError } from "./isRetryableSqliteWriteError.js";
|
|
2
|
+
/** @typedef {import("./SqliteWriteRetryOptions.ts").SqliteWriteRetryOptions} SqliteWriteRetryOptions */
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MAX_ATTEMPTS = 6;
|
|
5
|
+
const DEFAULT_BASE_DELAY_MS = 50;
|
|
6
|
+
const DEFAULT_MAX_DELAY_MS = 2_000;
|
|
7
|
+
export { isRetryableSqliteWriteError } from "./isRetryableSqliteWriteError.js";
|
|
8
|
+
export { withSqliteWriteRetryEffect } from "./withSqliteWriteRetryEffect.js";
|
|
9
|
+
/**
|
|
10
|
+
* @param {number} ms
|
|
11
|
+
* @returns {Promise<void>}
|
|
12
|
+
*/
|
|
13
|
+
function delay(ms) {
|
|
14
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* @param {number} retryAttempt
|
|
18
|
+
* @param {number} baseDelayMs
|
|
19
|
+
* @param {number} maxDelayMs
|
|
20
|
+
* @returns {number}
|
|
21
|
+
*/
|
|
22
|
+
function computeDelayMs(retryAttempt, baseDelayMs, maxDelayMs) {
|
|
23
|
+
const boundedBaseDelayMs = Math.max(1, Math.floor(baseDelayMs));
|
|
24
|
+
const boundedMaxDelayMs = Math.max(1, Math.floor(maxDelayMs));
|
|
25
|
+
const exponentialDelayMs = boundedBaseDelayMs * 2 ** Math.max(0, retryAttempt - 1);
|
|
26
|
+
return Math.min(boundedMaxDelayMs, exponentialDelayMs);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* @template A
|
|
30
|
+
* @param {() => A | PromiseLike<A>} operation
|
|
31
|
+
* @param {SqliteWriteRetryOptions} [opts]
|
|
32
|
+
* @returns {Promise<A>}
|
|
33
|
+
*/
|
|
34
|
+
export async function withSqliteWriteRetry(operation, opts = {}) {
|
|
35
|
+
const { maxAttempts = DEFAULT_MAX_ATTEMPTS, baseDelayMs = DEFAULT_BASE_DELAY_MS, maxDelayMs = DEFAULT_MAX_DELAY_MS, sleep = delay, } = opts;
|
|
36
|
+
const boundedMaxAttempts = Math.max(1, Math.floor(maxAttempts));
|
|
37
|
+
for (let attempt = 1;; attempt += 1) {
|
|
38
|
+
try {
|
|
39
|
+
return await operation();
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (attempt >= boundedMaxAttempts ||
|
|
43
|
+
!isRetryableSqliteWriteError(error)) {
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
await sleep(computeDelayMs(attempt, baseDelayMs, maxDelayMs));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { unwrapZodType } from "./unwrapZodType.js";
|
|
3
|
+
import { camelToSnake } from "./utils/camelToSnake.js";
|
|
4
|
+
/**
|
|
5
|
+
* Determines the Zod base type name from a (possibly unwrapped) Zod type.
|
|
6
|
+
*/
|
|
7
|
+
function getZodBaseTypeName(zodType) {
|
|
8
|
+
return zodType._zod?.def?.type ?? "unknown";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Generates a CREATE TABLE IF NOT EXISTS SQL statement from a Zod schema.
|
|
12
|
+
* Used for runtime table creation without Drizzle migrations.
|
|
13
|
+
*/
|
|
14
|
+
export function zodToCreateTableSQL(tableName, schema, opts) {
|
|
15
|
+
const colDefs = opts?.isInput
|
|
16
|
+
? [`run_id TEXT NOT NULL PRIMARY KEY`]
|
|
17
|
+
: [
|
|
18
|
+
`run_id TEXT NOT NULL`,
|
|
19
|
+
`node_id TEXT NOT NULL`,
|
|
20
|
+
`iteration INTEGER NOT NULL DEFAULT 0`,
|
|
21
|
+
];
|
|
22
|
+
const shape = schema.shape;
|
|
23
|
+
for (const [key] of Object.entries(shape)) {
|
|
24
|
+
const colName = camelToSnake(key);
|
|
25
|
+
const baseType = unwrapZodType(shape[key]);
|
|
26
|
+
const baseTypeName = getZodBaseTypeName(baseType);
|
|
27
|
+
if (baseTypeName === "number" ||
|
|
28
|
+
baseTypeName === "int" ||
|
|
29
|
+
baseTypeName === "float" ||
|
|
30
|
+
baseTypeName === "boolean") {
|
|
31
|
+
colDefs.push(`"${colName}" INTEGER`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
colDefs.push(`"${colName}" TEXT`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!opts?.isInput) {
|
|
38
|
+
colDefs.push(`PRIMARY KEY (run_id, node_id, iteration)`);
|
|
39
|
+
}
|
|
40
|
+
return `CREATE TABLE IF NOT EXISTS "${tableName}" (${colDefs.join(", ")})`;
|
|
41
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { sqliteTable, text, integer, primaryKey, } from "drizzle-orm/sqlite-core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { unwrapZodType } from "./unwrapZodType.js";
|
|
4
|
+
import { camelToSnake } from "./utils/camelToSnake.js";
|
|
5
|
+
/**
|
|
6
|
+
* Determines the Zod base type name from a (possibly unwrapped) Zod type.
|
|
7
|
+
*/
|
|
8
|
+
function getZodBaseTypeName(zodType) {
|
|
9
|
+
return zodType._zod?.def?.type ?? "unknown";
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Generates a Drizzle sqliteTable from a Zod object schema.
|
|
13
|
+
*
|
|
14
|
+
* Each Zod field is mapped to a SQLite column:
|
|
15
|
+
* - z.string() / z.enum() -> text column
|
|
16
|
+
* - z.number() -> integer column
|
|
17
|
+
* - z.boolean() -> integer column with boolean mode
|
|
18
|
+
* - z.array() / z.object() / complex -> text column with json mode
|
|
19
|
+
*
|
|
20
|
+
* All tables include standard smithers key columns:
|
|
21
|
+
* runId, nodeId, iteration with a composite primary key.
|
|
22
|
+
*/
|
|
23
|
+
export function zodToTable(tableName, schema, opts) {
|
|
24
|
+
const columns = opts?.isInput
|
|
25
|
+
? { runId: text("run_id").primaryKey() }
|
|
26
|
+
: {
|
|
27
|
+
runId: text("run_id").notNull(),
|
|
28
|
+
nodeId: text("node_id").notNull(),
|
|
29
|
+
iteration: integer("iteration").notNull().default(0),
|
|
30
|
+
};
|
|
31
|
+
const shape = schema.shape;
|
|
32
|
+
for (const [key, zodType] of Object.entries(shape)) {
|
|
33
|
+
const colName = camelToSnake(key);
|
|
34
|
+
const baseType = unwrapZodType(zodType);
|
|
35
|
+
const baseTypeName = getZodBaseTypeName(baseType);
|
|
36
|
+
if (baseTypeName === "number" ||
|
|
37
|
+
baseTypeName === "int" ||
|
|
38
|
+
baseTypeName === "float") {
|
|
39
|
+
columns[key] = integer(colName);
|
|
40
|
+
}
|
|
41
|
+
else if (baseTypeName === "boolean") {
|
|
42
|
+
columns[key] = integer(colName, { mode: "boolean" });
|
|
43
|
+
}
|
|
44
|
+
else if (baseTypeName === "string" ||
|
|
45
|
+
baseTypeName === "enum" ||
|
|
46
|
+
baseTypeName === "literal") {
|
|
47
|
+
columns[key] = text(colName);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// arrays, objects, unions, and anything complex -> JSON text
|
|
51
|
+
columns[key] = text(colName, { mode: "json" });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (opts?.isInput) {
|
|
55
|
+
return sqliteTable(tableName, columns);
|
|
56
|
+
}
|
|
57
|
+
return sqliteTable(tableName, columns, (t) => [
|
|
58
|
+
primaryKey({ columns: [t.runId, t.nodeId, t.iteration] }),
|
|
59
|
+
]);
|
|
60
|
+
}
|