@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.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +43 -0
  3. package/src/JsonBounds.ts +6 -0
  4. package/src/SchemaRegistryEntry.ts +6 -0
  5. package/src/SqlMessageStorage.js +818 -0
  6. package/src/SqlMessageStorageEventHistoryQuery.ts +7 -0
  7. package/src/SqliteWriteRetryOptions.ts +7 -0
  8. package/src/adapter/AlertRow.ts +29 -0
  9. package/src/adapter/AlertSeverity.ts +2 -0
  10. package/src/adapter/AlertStatus.ts +2 -0
  11. package/src/adapter/ApprovalRow.ts +13 -0
  12. package/src/adapter/AttemptRow.ts +17 -0
  13. package/src/adapter/CacheRow.ts +12 -0
  14. package/src/adapter/DB_ALERT_ALLOWED_SEVERITIES.js +5 -0
  15. package/src/adapter/DB_ALERT_ALLOWED_STATUSES.js +6 -0
  16. package/src/adapter/DB_ALERT_ID_MAX_LENGTH.js +1 -0
  17. package/src/adapter/DB_ALERT_MESSAGE_MAX_LENGTH.js +1 -0
  18. package/src/adapter/DB_ALERT_POLICY_NAME_MAX_LENGTH.js +1 -0
  19. package/src/adapter/DB_RUN_ALLOWED_STATUSES.js +10 -0
  20. package/src/adapter/DB_RUN_ID_MAX_LENGTH.js +1 -0
  21. package/src/adapter/DB_RUN_WORKFLOW_NAME_MAX_LENGTH.js +1 -0
  22. package/src/adapter/EventHistoryQuery.ts +7 -0
  23. package/src/adapter/HumanRequestRow.ts +19 -0
  24. package/src/adapter/NodeDiffCacheRow.ts +9 -0
  25. package/src/adapter/NodeRow.ts +10 -0
  26. package/src/adapter/PendingHumanRequestRow.ts +7 -0
  27. package/src/adapter/RunAncestryRow.ts +5 -0
  28. package/src/adapter/RunRow.ts +21 -0
  29. package/src/adapter/SignalQuery.ts +6 -0
  30. package/src/adapter/SignalRow.ts +9 -0
  31. package/src/adapter/SmithersDb.js +2236 -0
  32. package/src/adapter/StaleRunRecord.ts +7 -0
  33. package/src/adapter/index.js +27 -0
  34. package/src/adapter.js +2359 -0
  35. package/src/assertJsonPayloadWithinBounds.js +94 -0
  36. package/src/assertMaxBytes.js +23 -0
  37. package/src/assertMaxJsonDepth.js +40 -0
  38. package/src/assertMaxStringLength.js +16 -0
  39. package/src/assertOptionalArrayMaxLength.js +16 -0
  40. package/src/assertOptionalStringMaxLength.js +11 -0
  41. package/src/assertPositiveFiniteInteger.js +14 -0
  42. package/src/assertPositiveFiniteNumber.js +12 -0
  43. package/src/buildHumanRequestId.js +9 -0
  44. package/src/cache/nodeDiffCache.js +124 -0
  45. package/src/ensure.js +18 -0
  46. package/src/ensureSqlMessageStorage.js +11 -0
  47. package/src/ensureSqlMessageStorageEffect.js +12 -0
  48. package/src/frame-codec/FRAME_KEYFRAME_INTERVAL.js +1 -0
  49. package/src/frame-codec/FrameDelta.ts +6 -0
  50. package/src/frame-codec/FrameDeltaOp.ts +20 -0
  51. package/src/frame-codec/FrameEncoding.ts +1 -0
  52. package/src/frame-codec/JsonPath.ts +3 -0
  53. package/src/frame-codec/JsonPathSegment.ts +1 -0
  54. package/src/frame-codec/applyFrameDelta.js +143 -0
  55. package/src/frame-codec/applyFrameDeltaJson.js +10 -0
  56. package/src/frame-codec/encodeFrameDelta.js +247 -0
  57. package/src/frame-codec/index.js +15 -0
  58. package/src/frame-codec/normalizeFrameEncoding.js +13 -0
  59. package/src/frame-codec/parseFrameDelta.js +27 -0
  60. package/src/frame-codec/serializeFrameDelta.js +9 -0
  61. package/src/frame-codec.js +409 -0
  62. package/src/getSqlMessageStorage.js +11 -0
  63. package/src/index.d.ts +5203 -0
  64. package/src/index.js +20 -0
  65. package/src/input-bounds.js +12 -0
  66. package/src/input.js +17 -0
  67. package/src/internal-schema/index.js +19 -0
  68. package/src/internal-schema/smithersAlerts.js +27 -0
  69. package/src/internal-schema/smithersApprovals.js +18 -0
  70. package/src/internal-schema/smithersAttempts.js +20 -0
  71. package/src/internal-schema/smithersCache.js +13 -0
  72. package/src/internal-schema/smithersCron.js +11 -0
  73. package/src/internal-schema/smithersEvents.js +10 -0
  74. package/src/internal-schema/smithersFrames.js +14 -0
  75. package/src/internal-schema/smithersHumanRequests.js +17 -0
  76. package/src/internal-schema/smithersNodeDiffs.js +12 -0
  77. package/src/internal-schema/smithersNodes.js +13 -0
  78. package/src/internal-schema/smithersRalph.js +10 -0
  79. package/src/internal-schema/smithersRuns.js +22 -0
  80. package/src/internal-schema/smithersSandboxes.js +16 -0
  81. package/src/internal-schema/smithersSignals.js +12 -0
  82. package/src/internal-schema/smithersTimeTravelAudit.js +12 -0
  83. package/src/internal-schema/smithersToolCalls.js +19 -0
  84. package/src/internal-schema/smithersVectors.js +12 -0
  85. package/src/internal-schema.js +245 -0
  86. package/src/isRetryableSqliteWriteError.js +53 -0
  87. package/src/loadInputEffect.js +28 -0
  88. package/src/loadOutputsEffect.js +87 -0
  89. package/src/output/OutputKey.ts +1 -0
  90. package/src/output/buildKeyWhere.js +17 -0
  91. package/src/output/buildOutputRow.js +34 -0
  92. package/src/output/describeSchemaShape.js +70 -0
  93. package/src/output/getAgentOutputSchema.js +13 -0
  94. package/src/output/getKeyColumns.js +19 -0
  95. package/src/output/index.js +14 -0
  96. package/src/output/selectOutputRowEffect.js +30 -0
  97. package/src/output/stripAutoColumns.js +10 -0
  98. package/src/output/upsertOutputRowEffect.js +38 -0
  99. package/src/output/validateExistingOutput.js +17 -0
  100. package/src/output/validateOutput.js +17 -0
  101. package/src/output-schema-descriptor.js +163 -0
  102. package/src/output.js +240 -0
  103. package/src/react-output.js +10 -0
  104. package/src/runState/ComputeRunStateOptions.ts +4 -0
  105. package/src/runState/DeriveRunStateInput.ts +10 -0
  106. package/src/runState/RUN_STATE_HEARTBEAT_STALE_MS.js +1 -0
  107. package/src/runState/ReasonBlocked.ts +10 -0
  108. package/src/runState/ReasonUnhealthy.ts +6 -0
  109. package/src/runState/RunState.ts +12 -0
  110. package/src/runState/RunStateView.ts +11 -0
  111. package/src/runState/computeRunState.js +22 -0
  112. package/src/runState/computeRunStateFromRow.js +102 -0
  113. package/src/runState/deriveRunState.js +109 -0
  114. package/src/runState/parseEventMeta.js +18 -0
  115. package/src/runState/parseTimerMeta.js +16 -0
  116. package/src/runState-types.ts +23 -0
  117. package/src/runState.js +7 -0
  118. package/src/schema-signature.js +22 -0
  119. package/src/snapshot.js +125 -0
  120. package/src/sql-message-storage.js +839 -0
  121. package/src/storage/InMemoryStorage.js +484 -0
  122. package/src/storage/StorageService.js +7 -0
  123. package/src/storage/StorageServiceShape.ts +122 -0
  124. package/src/storage/StorageServiceTypes.ts +150 -0
  125. package/src/unwrapZodType.js +17 -0
  126. package/src/utils/camelToSnake.js +6 -0
  127. package/src/withSqliteWriteRetryEffect.js +110 -0
  128. package/src/write-retry.js +49 -0
  129. package/src/zodToCreateTableSQL.js +41 -0
  130. package/src/zodToTable.js +60 -0
@@ -0,0 +1,94 @@
1
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
2
+ import { assertMaxBytes } from "./assertMaxBytes.js";
3
+ import { assertMaxJsonDepth } from "./assertMaxJsonDepth.js";
4
+ /** @typedef {import("./JsonBounds.ts").JsonBounds} JsonBounds */
5
+
6
+ /**
7
+ * @param {string} field
8
+ * @param {unknown} value
9
+ * @param {JsonBounds} bounds
10
+ * @param {string} path
11
+ * @param {Set<unknown>} seen
12
+ */
13
+ function validateJsonValue(field, value, bounds, path, seen) {
14
+ if (value === null || typeof value === "boolean") {
15
+ return;
16
+ }
17
+ if (typeof value === "string") {
18
+ if (typeof bounds.maxStringLength === "number" &&
19
+ value.length > bounds.maxStringLength) {
20
+ throw new SmithersError("INVALID_INPUT", `${field} contains a string exceeding ${bounds.maxStringLength} characters.`, {
21
+ field,
22
+ path,
23
+ maxLength: bounds.maxStringLength,
24
+ actualLength: value.length,
25
+ });
26
+ }
27
+ return;
28
+ }
29
+ if (typeof value === "number") {
30
+ if (!Number.isFinite(value)) {
31
+ throw new SmithersError("INVALID_INPUT", `${field} must contain only finite numbers.`, { field, path, value });
32
+ }
33
+ return;
34
+ }
35
+ if (value === undefined ||
36
+ typeof value === "bigint" ||
37
+ typeof value === "function" ||
38
+ typeof value === "symbol") {
39
+ throw new SmithersError("INVALID_INPUT", `${field} must be JSON-serializable.`, { field, path, valueType: typeof value });
40
+ }
41
+ if (typeof value !== "object") {
42
+ throw new SmithersError("INVALID_INPUT", `${field} contains an unsupported value.`, { field, path, valueType: typeof value });
43
+ }
44
+ if (seen.has(value)) {
45
+ throw new SmithersError("INVALID_INPUT", `${field} must not contain circular references.`, { field, path });
46
+ }
47
+ seen.add(value);
48
+ if (Array.isArray(value)) {
49
+ if (typeof bounds.maxArrayLength === "number" &&
50
+ value.length > bounds.maxArrayLength) {
51
+ throw new SmithersError("INVALID_INPUT", `${field} contains an array exceeding ${bounds.maxArrayLength} items.`, {
52
+ field,
53
+ path,
54
+ maxLength: bounds.maxArrayLength,
55
+ actualLength: value.length,
56
+ });
57
+ }
58
+ for (let index = 0; index < value.length; index += 1) {
59
+ validateJsonValue(field, value[index], bounds, `${path}[${index}]`, seen);
60
+ }
61
+ seen.delete(value);
62
+ return;
63
+ }
64
+ for (const [key, entry] of Object.entries(value)) {
65
+ validateJsonValue(field, entry, bounds, `${path}.${key}`, seen);
66
+ }
67
+ seen.delete(value);
68
+ }
69
+ /**
70
+ * @param {string} field
71
+ * @param {unknown} value
72
+ * @param {JsonBounds} bounds
73
+ * @returns {string}
74
+ */
75
+ export function assertJsonPayloadWithinBounds(field, value, bounds) {
76
+ let payloadJson;
77
+ try {
78
+ payloadJson = JSON.stringify(value);
79
+ }
80
+ catch (error) {
81
+ throw new SmithersError("INVALID_INPUT", `${field} must be JSON-serializable.`, { field }, { cause: error });
82
+ }
83
+ if (payloadJson === undefined) {
84
+ throw new SmithersError("INVALID_INPUT", `${field} must be JSON-serializable.`, { field });
85
+ }
86
+ if (typeof bounds.maxBytes === "number") {
87
+ assertMaxBytes(field, payloadJson, bounds.maxBytes);
88
+ }
89
+ if (typeof bounds.maxDepth === "number") {
90
+ assertMaxJsonDepth(field, value, bounds.maxDepth);
91
+ }
92
+ validateJsonValue(field, value, bounds, field, new Set());
93
+ return payloadJson;
94
+ }
@@ -0,0 +1,23 @@
1
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
2
+ /**
3
+ * @param {string} field
4
+ * @param {string | ArrayBuffer | ArrayBufferView} value
5
+ * @param {number} maxBytes
6
+ * @returns {number}
7
+ */
8
+ export function assertMaxBytes(field, value, maxBytes) {
9
+ let actualBytes;
10
+ if (typeof value === "string") {
11
+ actualBytes = Buffer.byteLength(value, "utf8");
12
+ }
13
+ else if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
14
+ actualBytes = value.byteLength;
15
+ }
16
+ else {
17
+ throw new SmithersError("INVALID_INPUT", `${field} must be a string or byte buffer.`, { field, valueType: typeof value });
18
+ }
19
+ if (actualBytes > maxBytes) {
20
+ throw new SmithersError("INVALID_INPUT", `${field} exceeds the maximum size of ${maxBytes} bytes.`, { field, maxBytes, actualBytes });
21
+ }
22
+ return actualBytes;
23
+ }
@@ -0,0 +1,40 @@
1
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
2
+ /**
3
+ * @param {string} field
4
+ * @param {unknown} value
5
+ * @param {number} depth
6
+ * @param {number} maxDepth
7
+ * @param {string} path
8
+ * @param {Set<unknown>} seen
9
+ */
10
+ function validateJsonDepth(field, value, depth, maxDepth, path, seen) {
11
+ if (depth > maxDepth) {
12
+ throw new SmithersError("INVALID_INPUT", `${field} exceeds the maximum JSON depth of ${maxDepth}.`, { field, maxDepth, path });
13
+ }
14
+ if (value === null || typeof value !== "object") {
15
+ return;
16
+ }
17
+ if (seen.has(value)) {
18
+ throw new SmithersError("INVALID_INPUT", `${field} must not contain circular references.`, { field, path });
19
+ }
20
+ seen.add(value);
21
+ if (Array.isArray(value)) {
22
+ for (let index = 0; index < value.length; index += 1) {
23
+ validateJsonDepth(field, value[index], depth + 1, maxDepth, `${path}[${index}]`, seen);
24
+ }
25
+ seen.delete(value);
26
+ return;
27
+ }
28
+ for (const [key, entry] of Object.entries(value)) {
29
+ validateJsonDepth(field, entry, depth + 1, maxDepth, `${path}.${key}`, seen);
30
+ }
31
+ seen.delete(value);
32
+ }
33
+ /**
34
+ * @param {string} field
35
+ * @param {unknown} value
36
+ * @param {number} maxDepth
37
+ */
38
+ export function assertMaxJsonDepth(field, value, maxDepth) {
39
+ validateJsonDepth(field, value, 1, maxDepth, field, new Set());
40
+ }
@@ -0,0 +1,16 @@
1
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
2
+ /**
3
+ * @param {string} field
4
+ * @param {unknown} value
5
+ * @param {number} maxLength
6
+ * @returns {string}
7
+ */
8
+ export function assertMaxStringLength(field, value, maxLength) {
9
+ if (typeof value !== "string") {
10
+ throw new SmithersError("INVALID_INPUT", `${field} must be a string.`, { field, valueType: typeof value });
11
+ }
12
+ if (value.length > maxLength) {
13
+ throw new SmithersError("INVALID_INPUT", `${field} exceeds the maximum length of ${maxLength} characters.`, { field, maxLength, actualLength: value.length });
14
+ }
15
+ return value;
16
+ }
@@ -0,0 +1,16 @@
1
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
2
+ /**
3
+ * @param {string} field
4
+ * @param {unknown} value
5
+ * @param {number} maxLength
6
+ */
7
+ export function assertOptionalArrayMaxLength(field, value, maxLength) {
8
+ if (value === undefined || value === null)
9
+ return;
10
+ if (!Array.isArray(value)) {
11
+ throw new SmithersError("INVALID_INPUT", `${field} must be an array.`, { field, valueType: typeof value });
12
+ }
13
+ if (value.length > maxLength) {
14
+ throw new SmithersError("INVALID_INPUT", `${field} exceeds the maximum size of ${maxLength}.`, { field, maxLength, actualLength: value.length });
15
+ }
16
+ }
@@ -0,0 +1,11 @@
1
+ import { assertMaxStringLength } from "./assertMaxStringLength.js";
2
+ /**
3
+ * @param {string} field
4
+ * @param {unknown} value
5
+ * @param {number} maxLength
6
+ */
7
+ export function assertOptionalStringMaxLength(field, value, maxLength) {
8
+ if (value === undefined || value === null)
9
+ return;
10
+ assertMaxStringLength(field, value, maxLength);
11
+ }
@@ -0,0 +1,14 @@
1
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
2
+ import { assertPositiveFiniteNumber } from "./assertPositiveFiniteNumber.js";
3
+ /**
4
+ * @param {string} field
5
+ * @param {unknown} value
6
+ * @returns {number}
7
+ */
8
+ export function assertPositiveFiniteInteger(field, value) {
9
+ const numberValue = assertPositiveFiniteNumber(field, value);
10
+ if (!Number.isInteger(numberValue)) {
11
+ throw new SmithersError("INVALID_INPUT", `${field} must be an integer greater than 0.`, { field, value });
12
+ }
13
+ return numberValue;
14
+ }
@@ -0,0 +1,12 @@
1
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
2
+ /**
3
+ * @param {string} field
4
+ * @param {unknown} value
5
+ * @returns {number}
6
+ */
7
+ export function assertPositiveFiniteNumber(field, value) {
8
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
9
+ throw new SmithersError("INVALID_INPUT", `${field} must be a finite number greater than 0.`, { field, value });
10
+ }
11
+ return value;
12
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @param {string} runId
3
+ * @param {string} nodeId
4
+ * @param {number} iteration
5
+ * @returns {string}
6
+ */
7
+ export function buildHumanRequestId(runId, nodeId, iteration) {
8
+ return `human:${runId}:${nodeId}:${iteration}`;
9
+ }
@@ -0,0 +1,124 @@
1
+ /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} _SmithersDb */
2
+ /** @typedef {import("../adapter/NodeDiffCacheRow.ts").NodeDiffCacheRow} _NodeDiffCacheRow */
3
+ /** @typedef {{ bundle: unknown; sizeBytes: number; cacheResult: "hit" | "miss" }} NodeDiffCacheResult */
4
+
5
+ const NODE_DIFF_MAX_BYTES = 50 * 1024 * 1024;
6
+ /** @type {WeakMap<object, Map<string, Promise<NodeDiffCacheResult>>>} */
7
+ const inflightByDb = new WeakMap();
8
+ /**
9
+ * @param {object} dbKey
10
+ * @returns {Map<string, Promise<NodeDiffCacheResult>>}
11
+ */
12
+ function getInflightMap(dbKey) {
13
+ const existing = inflightByDb.get(dbKey);
14
+ if (existing) return existing;
15
+ /** @type {Map<string, Promise<NodeDiffCacheResult>>} */
16
+ const created = new Map();
17
+ inflightByDb.set(dbKey, created);
18
+ return created;
19
+ }
20
+ export class NodeDiffTooLargeError extends Error {
21
+ code = "DiffTooLarge";
22
+ /** @type {number} */
23
+ sizeBytes;
24
+ /** @param {number} sizeBytes */
25
+ constructor(sizeBytes) {
26
+ super(`Serialized diff exceeds ${NODE_DIFF_MAX_BYTES} bytes`);
27
+ this.name = "NodeDiffTooLargeError";
28
+ this.sizeBytes = sizeBytes;
29
+ }
30
+ }
31
+ export class NodeDiffCache {
32
+ /** @type {_SmithersDb} */
33
+ adapter;
34
+ /** @type {{ warn?: (message: string, details?: Record<string, unknown>) => void }} */
35
+ logger;
36
+ /**
37
+ * @param {_SmithersDb} adapter
38
+ * @param {{ warn?: (message: string, details?: Record<string, unknown>) => void }} [logger]
39
+ */
40
+ constructor(adapter, logger = {}) {
41
+ this.adapter = adapter;
42
+ this.logger = logger;
43
+ }
44
+ /**
45
+ * @param {{ runId: string; nodeId: string; iteration: number; baseRef: string; }} key
46
+ * @returns {string}
47
+ */
48
+ static keyString(key) {
49
+ return `${key.runId}::${key.nodeId}::${key.iteration}::${key.baseRef}`;
50
+ }
51
+ /**
52
+ * @param {{ runId: string; nodeId: string; iteration: number; baseRef: string; }} key
53
+ * @returns {Promise<{ bundle: unknown; sizeBytes: number; } | null>}
54
+ */
55
+ async get(key) {
56
+ const row = await this.adapter.getNodeDiffCache(key.runId, key.nodeId, key.iteration, key.baseRef);
57
+ if (!row || typeof row.diffJson !== "string") return null;
58
+ try {
59
+ const bundle = JSON.parse(row.diffJson);
60
+ const sizeBytes = Number(row.sizeBytes ?? Buffer.byteLength(row.diffJson, "utf8"));
61
+ return {
62
+ bundle,
63
+ sizeBytes: Number.isFinite(sizeBytes) ? sizeBytes : Buffer.byteLength(row.diffJson, "utf8"),
64
+ };
65
+ } catch {
66
+ this.logger.warn?.("Failed to parse cached node diff JSON; treating as miss.", {
67
+ runId: key.runId, nodeId: key.nodeId, iteration: key.iteration,
68
+ });
69
+ return null;
70
+ }
71
+ }
72
+ /**
73
+ * @param {{ runId: string; nodeId: string; iteration: number; baseRef: string; }} key
74
+ * @param {() => Promise<unknown>} compute
75
+ * @returns {Promise<NodeDiffCacheResult>}
76
+ */
77
+ async getOrCompute(key, compute) {
78
+ const hit = await this.get(key);
79
+ if (hit) return { bundle: hit.bundle, sizeBytes: hit.sizeBytes, cacheResult: "hit" };
80
+ const adapterWithDb = /** @type {_SmithersDb & { db?: object }} */ (this.adapter);
81
+ const inflight = getInflightMap(adapterWithDb.db ?? this.adapter);
82
+ const inflightKey = NodeDiffCache.keyString(key);
83
+ const pending = inflight.get(inflightKey);
84
+ if (pending) return pending;
85
+ const computePromise = (async () => {
86
+ const bundle = await compute();
87
+ const diffJson = JSON.stringify(bundle);
88
+ const sizeBytes = Buffer.byteLength(diffJson, "utf8");
89
+ if (sizeBytes > NODE_DIFF_MAX_BYTES) throw new NodeDiffTooLargeError(sizeBytes);
90
+ /** @type {_NodeDiffCacheRow} */
91
+ const row = {
92
+ runId: key.runId, nodeId: key.nodeId, iteration: key.iteration, baseRef: key.baseRef,
93
+ diffJson, computedAtMs: Date.now(), sizeBytes,
94
+ };
95
+ try {
96
+ await this.adapter.upsertNodeDiffCache(row);
97
+ } catch (error) {
98
+ this.logger.warn?.("Failed writing node diff cache row.", {
99
+ runId: key.runId, nodeId: key.nodeId, iteration: key.iteration,
100
+ error: error instanceof Error ? error.message : String(error),
101
+ });
102
+ }
103
+ return { bundle, sizeBytes, cacheResult: /** @type {const} */ ("miss") };
104
+ })().finally(() => { inflight.delete(inflightKey); });
105
+ inflight.set(inflightKey, computePromise);
106
+ return computePromise;
107
+ }
108
+ /**
109
+ * @param {string} runId
110
+ * @param {number} targetFrameNo
111
+ * @returns {ReturnType<_SmithersDb["invalidateNodeDiffsAfterFrame"]>}
112
+ */
113
+ invalidateAfterFrame(runId, targetFrameNo) {
114
+ return this.adapter.invalidateNodeDiffsAfterFrame(runId, targetFrameNo);
115
+ }
116
+ /**
117
+ * @param {string} [runId]
118
+ * @returns {ReturnType<_SmithersDb["countNodeDiffCacheRows"]>}
119
+ */
120
+ countRows(runId) {
121
+ return this.adapter.countNodeDiffCacheRows(runId);
122
+ }
123
+ }
124
+ export { NODE_DIFF_MAX_BYTES };
package/src/ensure.js ADDED
@@ -0,0 +1,18 @@
1
+ import { Effect } from "effect";
2
+ import { ensureSqlMessageStorageEffect } from "./sql-message-storage.js";
3
+ /** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} _BunSQLiteDatabase */
4
+ /** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} _SmithersError */
5
+
6
+ /**
7
+ * @param {_BunSQLiteDatabase<Record<string, unknown>>} db
8
+ * @returns {Effect.Effect<void, _SmithersError>}
9
+ */
10
+ export function ensureSmithersTablesEffect(db) {
11
+ return ensureSqlMessageStorageEffect(db).pipe(Effect.withLogSpan("db:ensure-smithers-tables"));
12
+ }
13
+ /**
14
+ * @param {_BunSQLiteDatabase<Record<string, unknown>>} db
15
+ */
16
+ export function ensureSmithersTables(db) {
17
+ Effect.runSync(ensureSmithersTablesEffect(db));
18
+ }
@@ -0,0 +1,11 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { getSqlMessageStorage } from "./getSqlMessageStorage.js";
3
+ /** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
4
+
5
+ /**
6
+ * @param {BunSQLiteDatabase<any> | Database} db
7
+ * @returns {Promise<void>}
8
+ */
9
+ export function ensureSqlMessageStorage(db) {
10
+ return getSqlMessageStorage(db).ensureSchema();
11
+ }
@@ -0,0 +1,12 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { Effect } from "effect";
3
+ import { getSqlMessageStorage } from "./getSqlMessageStorage.js";
4
+ /** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
5
+
6
+ /**
7
+ * @param {BunSQLiteDatabase<any> | Database} db
8
+ * @returns {Effect.Effect<void, never>}
9
+ */
10
+ export function ensureSqlMessageStorageEffect(db) {
11
+ return getSqlMessageStorage(db).ensureSchemaEffect();
12
+ }
@@ -0,0 +1 @@
1
+ export const FRAME_KEYFRAME_INTERVAL = 50;
@@ -0,0 +1,6 @@
1
+ import type { FrameDeltaOp } from "./FrameDeltaOp";
2
+
3
+ export type FrameDelta = {
4
+ version: 1;
5
+ ops: FrameDeltaOp[];
6
+ };
@@ -0,0 +1,20 @@
1
+ import type { JsonPath } from "./JsonPath";
2
+
3
+ export type FrameDeltaOp =
4
+ | {
5
+ op: "set";
6
+ path: JsonPath;
7
+ value: unknown;
8
+ nodeId?: string;
9
+ }
10
+ | {
11
+ op: "insert";
12
+ path: JsonPath;
13
+ value: unknown;
14
+ nodeId?: string;
15
+ }
16
+ | {
17
+ op: "remove";
18
+ path: JsonPath;
19
+ nodeId?: string;
20
+ };
@@ -0,0 +1 @@
1
+ export type FrameEncoding = "full" | "delta" | "keyframe";
@@ -0,0 +1,3 @@
1
+ import type { JsonPathSegment } from "./JsonPathSegment";
2
+
3
+ export type JsonPath = JsonPathSegment[];
@@ -0,0 +1 @@
1
+ export type JsonPathSegment = string | number;
@@ -0,0 +1,143 @@
1
+ import { canonicalizeXml, parseXmlJson } from "@smithers-orchestrator/graph/utils/xml";
2
+ /** @typedef {import("./FrameDeltaOp.ts").FrameDeltaOp} FrameDeltaOp */
3
+ /** @typedef {import("./JsonPath.ts").JsonPath} JsonPath */
4
+ /** @typedef {import("./JsonPathSegment.ts").JsonPathSegment} JsonPathSegment */
5
+
6
+ /** @typedef {import("./FrameDelta.ts").FrameDelta} FrameDelta */
7
+
8
+ /**
9
+ * @param {string} previousXmlJson
10
+ * @param {FrameDelta} delta
11
+ * @returns {string}
12
+ */
13
+ export function applyFrameDelta(previousXmlJson, delta) {
14
+ const root = cloneValue(parseXmlJson(previousXmlJson));
15
+ const next = applyOps(root, delta.ops);
16
+ return canonicalizeXml(next);
17
+ }
18
+ /**
19
+ * @param {unknown} root
20
+ * @param {FrameDeltaOp[]} ops
21
+ * @returns {unknown}
22
+ */
23
+ function applyOps(root, ops) {
24
+ let current = root;
25
+ for (const op of ops) {
26
+ if (op.op === "set") {
27
+ current = setAtPath(current, op.path, op.value);
28
+ continue;
29
+ }
30
+ if (op.op === "insert") {
31
+ current = insertAtPath(current, op.path, op.value);
32
+ continue;
33
+ }
34
+ current = removeAtPath(current, op.path);
35
+ }
36
+ return current;
37
+ }
38
+ /**
39
+ * @param {unknown} root
40
+ * @param {JsonPath} path
41
+ * @param {unknown} value
42
+ * @returns {unknown}
43
+ */
44
+ function setAtPath(root, path, value) {
45
+ if (path.length === 0) {
46
+ return cloneValue(value);
47
+ }
48
+ const { parent, key } = getParentAndKey(root, path);
49
+ if (Array.isArray(parent)) {
50
+ if (typeof key !== "number") {
51
+ throw new Error("Invalid array set path");
52
+ }
53
+ parent[key] = cloneValue(value);
54
+ return root;
55
+ }
56
+ if (!isRecord(parent) || typeof key !== "string") {
57
+ throw new Error("Invalid object set path");
58
+ }
59
+ parent[key] = cloneValue(value);
60
+ return root;
61
+ }
62
+ /**
63
+ * @param {unknown} root
64
+ * @param {JsonPath} path
65
+ * @param {unknown} value
66
+ * @returns {unknown}
67
+ */
68
+ function insertAtPath(root, path, value) {
69
+ if (path.length === 0) {
70
+ return cloneValue(value);
71
+ }
72
+ const { parent, key } = getParentAndKey(root, path);
73
+ if (!Array.isArray(parent) || typeof key !== "number") {
74
+ throw new Error("Invalid insert path");
75
+ }
76
+ parent.splice(key, 0, cloneValue(value));
77
+ return root;
78
+ }
79
+ /**
80
+ * @param {unknown} root
81
+ * @param {JsonPath} path
82
+ * @returns {unknown}
83
+ */
84
+ function removeAtPath(root, path) {
85
+ if (path.length === 0) {
86
+ return null;
87
+ }
88
+ const { parent, key } = getParentAndKey(root, path);
89
+ if (Array.isArray(parent)) {
90
+ if (typeof key !== "number") {
91
+ throw new Error("Invalid array remove path");
92
+ }
93
+ parent.splice(key, 1);
94
+ return root;
95
+ }
96
+ if (!isRecord(parent) || typeof key !== "string") {
97
+ throw new Error("Invalid object remove path");
98
+ }
99
+ delete parent[key];
100
+ return root;
101
+ }
102
+ /**
103
+ * @param {unknown} root
104
+ * @param {JsonPath} path
105
+ * @returns {{ parent: unknown; key: JsonPathSegment }}
106
+ */
107
+ function getParentAndKey(root, path) {
108
+ let cursor = root;
109
+ for (let i = 0; i < path.length - 1; i += 1) {
110
+ const seg = path[i];
111
+ if (typeof seg === "number") {
112
+ if (!Array.isArray(cursor)) {
113
+ throw new Error("Invalid numeric path segment");
114
+ }
115
+ cursor = cursor[seg];
116
+ continue;
117
+ }
118
+ if (!isRecord(cursor)) {
119
+ throw new Error("Invalid object path segment");
120
+ }
121
+ cursor = cursor[seg];
122
+ }
123
+ return { parent: cursor, key: path[path.length - 1] };
124
+ }
125
+ /**
126
+ * @param {unknown} value
127
+ * @returns {value is Record<string, unknown>}
128
+ */
129
+ function isRecord(value) {
130
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
131
+ }
132
+ /**
133
+ * @template T
134
+ * @param {T} value
135
+ * @returns {T}
136
+ */
137
+ function cloneValue(value) {
138
+ if (value === null)
139
+ return value;
140
+ if (typeof value !== "object")
141
+ return value;
142
+ return JSON.parse(JSON.stringify(value));
143
+ }
@@ -0,0 +1,10 @@
1
+ import { applyFrameDelta } from "./applyFrameDelta.js";
2
+ import { parseFrameDelta } from "./parseFrameDelta.js";
3
+ /**
4
+ * @param {string} previousXmlJson
5
+ * @param {string} deltaJson
6
+ * @returns {string}
7
+ */
8
+ export function applyFrameDeltaJson(previousXmlJson, deltaJson) {
9
+ return applyFrameDelta(previousXmlJson, parseFrameDelta(deltaJson));
10
+ }