@spooky-sync/core 0.0.1-canary.21 → 0.0.1-canary.210

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 (163) hide show
  1. package/AGENTS.md +57 -0
  2. package/dist/index.d.ts +2514 -58
  3. package/dist/index.js +12561 -2449
  4. package/dist/otel/index.d.ts +2 -2
  5. package/dist/otel/index.js +6 -6
  6. package/dist/sqlite-open.js +303 -0
  7. package/dist/sqlite-worker.d.ts +1 -0
  8. package/dist/sqlite-worker.js +439 -0
  9. package/dist/tabs-broker-worker.d.ts +8 -0
  10. package/dist/tabs-broker-worker.js +472 -0
  11. package/dist/types.d.ts +751 -11
  12. package/package.json +11 -7
  13. package/scripts/check-broker-bundle.mjs +33 -0
  14. package/skills/{spooky-core → sp00ky-core}/SKILL.md +12 -12
  15. package/skills/{spooky-core → sp00ky-core}/references/auth.md +1 -1
  16. package/skills/{spooky-core → sp00ky-core}/references/config.md +2 -2
  17. package/src/bucket-blurhash.test.ts +148 -0
  18. package/src/build-globals.d.ts +12 -0
  19. package/src/events/events.test.ts +2 -1
  20. package/src/events/index.ts +3 -0
  21. package/src/index.ts +36 -2
  22. package/src/modules/app-release/index.test.ts +125 -0
  23. package/src/modules/app-release/index.ts +201 -0
  24. package/src/modules/auth/auth.local-first.test.ts +101 -0
  25. package/src/modules/auth/events/index.ts +2 -1
  26. package/src/modules/auth/index.ts +127 -24
  27. package/src/modules/cache/cache.relay.test.ts +95 -0
  28. package/src/modules/cache/index.ts +163 -43
  29. package/src/modules/cache/types.ts +2 -2
  30. package/src/modules/crdt/crdt-field.ts +294 -0
  31. package/src/modules/crdt/crdt-hydration.test.ts +210 -0
  32. package/src/modules/crdt/crdt-reconnect.test.ts +195 -0
  33. package/src/modules/crdt/index.ts +463 -0
  34. package/src/modules/crdt/loro-loader.ts +25 -0
  35. package/src/modules/data/data.hydration.test.ts +142 -0
  36. package/src/modules/data/data.membership.test.ts +523 -0
  37. package/src/modules/data/data.notify-table.test.ts +41 -0
  38. package/src/modules/data/data.pending-ids.test.ts +199 -0
  39. package/src/modules/data/data.rebind.test.ts +170 -0
  40. package/src/modules/data/data.rematerialize.test.ts +114 -0
  41. package/src/modules/data/data.run.test.ts +113 -0
  42. package/src/modules/data/data.settled-writes.test.ts +206 -0
  43. package/src/modules/data/data.status.test.ts +249 -0
  44. package/src/modules/data/id-set-plan.test.ts +122 -0
  45. package/src/modules/data/index.ts +1815 -151
  46. package/src/modules/data/mutation-id.test.ts +25 -0
  47. package/src/modules/data/mutation-id.ts +35 -0
  48. package/src/modules/data/window-query.test.ts +52 -0
  49. package/src/modules/data/window-query.ts +194 -0
  50. package/src/modules/devtools/flags.ts +349 -0
  51. package/src/modules/devtools/index.ts +450 -46
  52. package/src/modules/devtools/notify-throttle.test.ts +154 -0
  53. package/src/modules/devtools/state-shape.test.ts +146 -0
  54. package/src/modules/devtools/storage-info.test.ts +79 -0
  55. package/src/modules/devtools/storage-info.ts +168 -0
  56. package/src/modules/devtools/versions.test.ts +74 -0
  57. package/src/modules/devtools/versions.ts +110 -0
  58. package/src/modules/feature-flag/index.test.ts +251 -0
  59. package/src/modules/feature-flag/index.ts +308 -0
  60. package/src/modules/ref-tables.test.ts +91 -0
  61. package/src/modules/ref-tables.ts +88 -0
  62. package/src/modules/sync/engine.ts +164 -82
  63. package/src/modules/sync/events/index.ts +9 -2
  64. package/src/modules/sync/queue/queue-down.test.ts +180 -0
  65. package/src/modules/sync/queue/queue-down.ts +80 -13
  66. package/src/modules/sync/queue/queue-up.forwarded.test.ts +164 -0
  67. package/src/modules/sync/queue/queue-up.ts +241 -57
  68. package/src/modules/sync/scheduler.pause.test.ts +109 -0
  69. package/src/modules/sync/scheduler.retry.test.ts +237 -0
  70. package/src/modules/sync/scheduler.ts +215 -13
  71. package/src/modules/sync/sync.cleanup.test.ts +116 -0
  72. package/src/modules/sync/sync.health.test.ts +149 -0
  73. package/src/modules/sync/sync.heartbeat.test.ts +80 -0
  74. package/src/modules/sync/sync.live-removal.test.ts +175 -0
  75. package/src/modules/sync/sync.reconnect.test.ts +145 -0
  76. package/src/modules/sync/sync.subquery.test.ts +82 -0
  77. package/src/modules/sync/sync.tabs.test.ts +249 -0
  78. package/src/modules/sync/sync.ts +1726 -99
  79. package/src/modules/sync/utils.test.ts +269 -2
  80. package/src/modules/sync/utils.ts +201 -17
  81. package/src/otel/index.ts +13 -10
  82. package/src/services/blobs/blob-cache.test.ts +359 -0
  83. package/src/services/blobs/blob-cache.ts +603 -0
  84. package/src/services/blobs/blob-manifest.ts +227 -0
  85. package/src/services/blobs/blob-store.test.ts +77 -0
  86. package/src/services/blobs/blob-store.ts +359 -0
  87. package/src/services/blobs/blob.fixture.ts +90 -0
  88. package/src/services/blobs/index.ts +70 -0
  89. package/src/services/database/cache-engine.ts +193 -0
  90. package/src/services/database/connection-supervisor.test.ts +289 -0
  91. package/src/services/database/connection-supervisor.ts +415 -0
  92. package/src/services/database/database.query-timeout.test.ts +83 -0
  93. package/src/services/database/database.ts +41 -12
  94. package/src/services/database/engine-factory.ts +33 -0
  95. package/src/services/database/errors.ts +34 -0
  96. package/src/services/database/events/index.ts +2 -1
  97. package/src/services/database/index.ts +7 -0
  98. package/src/services/database/local-migrator.ts +30 -27
  99. package/src/services/database/local.test.ts +64 -0
  100. package/src/services/database/local.ts +484 -67
  101. package/src/services/database/plan-render.test.ts +159 -0
  102. package/src/services/database/plan-render.ts +108 -0
  103. package/src/services/database/relation-resolver.test.ts +413 -0
  104. package/src/services/database/relation-resolver.ts +0 -0
  105. package/src/services/database/remote.ts +110 -14
  106. package/src/services/database/sqlite-cache-engine.test.ts +616 -0
  107. package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
  108. package/src/services/database/sqlite-cache-engine.ts +1358 -0
  109. package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
  110. package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
  111. package/src/services/database/sqlite-lock-verify.test.ts +33 -0
  112. package/src/services/database/sqlite-lock-verify.ts +45 -0
  113. package/src/services/database/sqlite-open.test.ts +150 -0
  114. package/src/services/database/sqlite-open.ts +164 -0
  115. package/src/services/database/sqlite-plan-sql.test.ts +104 -0
  116. package/src/services/database/sqlite-plan-sql.ts +138 -0
  117. package/src/services/database/sqlite-projection.test.ts +99 -0
  118. package/src/services/database/sqlite-select.integration.test.ts +185 -0
  119. package/src/services/database/sqlite-select.test.ts +246 -0
  120. package/src/services/database/sqlite-select.ts +131 -0
  121. package/src/services/database/sqlite-transport.fixture.ts +30 -0
  122. package/src/services/database/sqlite-transport.ts +224 -0
  123. package/src/services/database/sqlite-worker.ts +437 -0
  124. package/src/services/database/surql-translate.ts +416 -0
  125. package/src/services/database/surreal-cache-engine.ts +161 -0
  126. package/src/services/logger/index.ts +3 -2
  127. package/src/services/persistence/localstorage.ts +2 -2
  128. package/src/services/persistence/resilient.ts +11 -4
  129. package/src/services/persistence/surrealdb.ts +10 -10
  130. package/src/services/stream-processor/index.ts +796 -84
  131. package/src/services/stream-processor/permissions.test.ts +47 -0
  132. package/src/services/stream-processor/permissions.ts +53 -0
  133. package/src/services/stream-processor/stream-processor.batch.test.ts +186 -0
  134. package/src/services/stream-processor/stream-processor.prime.test.ts +198 -0
  135. package/src/services/stream-processor/stream-processor.reset.test.ts +226 -0
  136. package/src/services/stream-processor/stream-processor.test.ts +1 -1
  137. package/src/services/stream-processor/wasm-types.ts +59 -3
  138. package/src/services/tabs/broker-client.ts +283 -0
  139. package/src/services/tabs/broker.test.ts +327 -0
  140. package/src/services/tabs/coordinator.test.ts +365 -0
  141. package/src/services/tabs/coordinator.ts +633 -0
  142. package/src/services/tabs/fake-ports.fixture.ts +112 -0
  143. package/src/services/tabs/leader-locks.ts +75 -0
  144. package/src/services/tabs/protocol.ts +258 -0
  145. package/src/services/tabs/support.ts +36 -0
  146. package/src/services/tabs/tabs-broker-worker.ts +640 -0
  147. package/src/sp00ky.auth-order.test.ts +92 -0
  148. package/src/sp00ky.init-query.test.ts +183 -0
  149. package/src/sp00ky.local-first.test.ts +60 -0
  150. package/src/sp00ky.ts +1693 -0
  151. package/src/types.ts +528 -13
  152. package/src/utils/blurhash.ts +90 -0
  153. package/src/utils/error-classification.test.ts +44 -0
  154. package/src/utils/error-classification.ts +7 -0
  155. package/src/utils/index.ts +79 -13
  156. package/src/utils/parser.test.ts +49 -120
  157. package/src/utils/parser.ts +32 -2
  158. package/src/utils/semver.test.ts +32 -0
  159. package/src/utils/semver.ts +30 -0
  160. package/src/utils/surql.ts +30 -18
  161. package/src/utils/withRetry.test.ts +1 -1
  162. package/tsdown.config.ts +86 -1
  163. package/src/spooky.ts +0 -395
@@ -1,4 +1,4 @@
1
- import { c as PinoTransmit } from "../types.js";
1
+ import { d as PinoTransmit } from "../types.js";
2
2
  import { Level } from "pino";
3
3
 
4
4
  //#region src/otel/index.d.ts
@@ -10,7 +10,7 @@ import { Level } from "pino";
10
10
  * ```ts
11
11
  * import { createOtelTransmit } from '@spooky-sync/core/otel';
12
12
  *
13
- * new SpookyClient({
13
+ * new Sp00kyClient({
14
14
  * // ...
15
15
  * otelTransmit: createOtelTransmit('http://localhost:4318/v1/logs'),
16
16
  * });
@@ -18,7 +18,7 @@ async function loadOtelModules(otelEndpoint) {
18
18
  import("@opentelemetry/semantic-conventions")
19
19
  ]);
20
20
  const loggerProvider = new LoggerProvider({
21
- resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "spooky-client" }),
21
+ resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "sp00ky-client" }),
22
22
  processors: [new BatchLogRecordProcessor(new OTLPLogExporter({ url: otelEndpoint }))]
23
23
  });
24
24
  const otelLoggerCache = {};
@@ -34,7 +34,7 @@ async function loadOtelModules(otelEndpoint) {
34
34
  * ```ts
35
35
  * import { createOtelTransmit } from '@spooky-sync/core/otel';
36
36
  *
37
- * new SpookyClient({
37
+ * new Sp00kyClient({
38
38
  * // ...
39
39
  * otelTransmit: createOtelTransmit('http://localhost:4318/v1/logs'),
40
40
  * });
@@ -50,10 +50,10 @@ function createOtelTransmit(endpoint, level = "info") {
50
50
  const messages = [...logEvent.messages];
51
51
  const severityNumber = mapLevelToSeverityNumber(levelLabel);
52
52
  let body = "";
53
- const msg = messages.pop();
54
- if (typeof msg === "string") body = msg;
55
- else if (msg) body = JSON.stringify(msg);
56
- let category = "spooky-client::unknown";
53
+ const lastMsg = messages.pop();
54
+ if (typeof lastMsg === "string") body = lastMsg;
55
+ else if (lastMsg) body = JSON.stringify(lastMsg);
56
+ let category = "sp00ky-client::unknown";
57
57
  const attributes = {};
58
58
  for (const msg of messages) if (typeof msg === "object") {
59
59
  if (msg.Category) {
@@ -0,0 +1,303 @@
1
+ //#region src/services/database/cache-engine.ts
2
+ /** Thrown when relation decomposition nests past {@link MAX_RELATION_DEPTH} —
3
+ * a guard against a cyclic schema producing unbounded fan-out. */
4
+ var RelationCycleError = class extends Error {
5
+ constructor(path) {
6
+ super(`Relation nesting exceeded safe depth; possible cyclic schema: ${path.join(" -> ")}`);
7
+ this.name = "RelationCycleError";
8
+ }
9
+ };
10
+ /** Defensive ceiling on relation nesting depth. A finite plan tree never hits
11
+ * this in practice; it exists so a malformed/cyclic plan fails loudly instead
12
+ * of running away. */
13
+ const MAX_RELATION_DEPTH = 12;
14
+
15
+ //#endregion
16
+ //#region src/services/database/relation-resolver.ts
17
+ /**
18
+ * Resolve the `.related()` tree of a query by **level-ordered, batched
19
+ * fan-out** — the engine-neutral replacement for SurrealQL's nested
20
+ * `(SELECT..) AS alias` projections. One batched fetch per relation per level
21
+ * (never per-row N+1, never a flat join); grouping, per-parent ORDER/LIMIT and
22
+ * attachment happen in JS.
23
+ *
24
+ * Correlation (mirrors `buildSubquery`):
25
+ * - `one` → parent[`foreignKeyField`] = child.id → attach `bucket[0] ?? null`
26
+ * - `many` → child[`foreignKeyField`] = parent.id → attach `bucket`
27
+ *
28
+ * Cross-level dependency is handled by ordering: a child's correlation key is a
29
+ * field on its parent, known only once the parent level has resolved, so each
30
+ * level's batch runs after the previous level's rows exist. `parents` is
31
+ * mutated in place — each resolved alias is appended LAST so key order matches
32
+ * SurrealQL's `SELECT *, <sub> AS alias` (base fields, then alias).
33
+ *
34
+ * @throws {RelationCycleError} if nesting exceeds {@link MAX_RELATION_DEPTH}.
35
+ */
36
+ async function resolveRelations(parents, relations, fetcher, depth = 0, path = []) {
37
+ if (!relations || relations.length === 0 || parents.length === 0) return;
38
+ if (depth >= MAX_RELATION_DEPTH) throw new RelationCycleError([...path, relations.map((r) => r.alias).join("|")]);
39
+ await Promise.all(relations.map((relation) => resolveOne(parents, relation, fetcher, depth, path)));
40
+ }
41
+ async function resolveOne(parents, relation, fetcher, depth, path) {
42
+ const isOne = relation.cardinality === "one";
43
+ const matchField = isOne ? "id" : relation.foreignKeyField;
44
+ const parentKeyOf = (parent) => isOne ? parent[relation.foreignKeyField] : parent["id"];
45
+ const keySet = /* @__PURE__ */ new Map();
46
+ for (const parent of parents) {
47
+ const key = parentKeyOf(parent);
48
+ if (key == null) continue;
49
+ const k = stableKey(key);
50
+ if (!keySet.has(k)) keySet.set(k, key);
51
+ }
52
+ const grouped = /* @__PURE__ */ new Map();
53
+ if (keySet.size > 0) {
54
+ const children = await fetcher.fetchRelation({
55
+ table: relation.table,
56
+ matchField,
57
+ keys: [...keySet.values()],
58
+ where: relation.where,
59
+ orderBy: relation.orderBy,
60
+ select: relation.select
61
+ });
62
+ await resolveRelations(children, relation.relations, fetcher, depth + 1, [...path, relation.alias]);
63
+ for (const child of children) {
64
+ const k = stableKey(child[matchField]);
65
+ const bucket = grouped.get(k);
66
+ if (bucket) bucket.push(child);
67
+ else grouped.set(k, [child]);
68
+ }
69
+ }
70
+ for (const parent of parents) {
71
+ const key = parentKeyOf(parent);
72
+ let bucket = key == null ? [] : grouped.get(stableKey(key)) ?? [];
73
+ if (relation.orderBy && relation.orderBy.length > 0) bucket = sortRows(bucket, relation.orderBy);
74
+ if (relation.limit !== void 0) bucket = bucket.slice(0, relation.limit);
75
+ parent[relation.alias] = isOne ? bucket[0] ?? null : bucket;
76
+ }
77
+ }
78
+ /**
79
+ * Stable grouping key for a correlation value. A `RecordId` (duck-typed to
80
+ * avoid a hard surrealdb dependency here) and its `table:id` string form must
81
+ * collapse to the same key so a parent FK stored either way matches the child.
82
+ */
83
+ function stableKey(value) {
84
+ if (value == null) return "\0null";
85
+ if (typeof value === "string") return value;
86
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
87
+ if (typeof value === "object") {
88
+ const rid = value;
89
+ if (rid.tb !== void 0 && rid.id !== void 0) return `${String(rid.tb)}:${stableKey(rid.id)}`;
90
+ if (typeof rid.toString === "function") {
91
+ const s = rid.toString();
92
+ if (s !== "[object Object]") return s;
93
+ }
94
+ return JSON.stringify(value);
95
+ }
96
+ return String(value);
97
+ }
98
+ /**
99
+ * Stable, multi-key sort matching SurrealQL `ORDER BY` semantics closely
100
+ * enough for cache display order: ascending null-last, type-aware compare, and
101
+ * a stable tiebreak (original index) so equal rows keep input order.
102
+ */
103
+ function sortRows(rows, orderBy) {
104
+ const indexed = rows.map((row, i) => [row, i]);
105
+ indexed.sort((a, b) => {
106
+ for (const [field, direction] of orderBy) {
107
+ const cmp = compareValues(a[0][field], b[0][field]);
108
+ if (cmp !== 0) return direction === "desc" ? -cmp : cmp;
109
+ }
110
+ return a[1] - b[1];
111
+ });
112
+ return indexed.map(([row]) => row);
113
+ }
114
+ function compareValues(a, b) {
115
+ if (a == null && b == null) return 0;
116
+ if (a == null) return 1;
117
+ if (b == null) return -1;
118
+ if (typeof a === "number" && typeof b === "number") return a - b;
119
+ const as = stableKey(a);
120
+ const bs = stableKey(b);
121
+ return as < bs ? -1 : as > bs ? 1 : 0;
122
+ }
123
+
124
+ //#endregion
125
+ //#region src/services/database/sqlite-plan-sql.ts
126
+ /**
127
+ * SQL rendering + value (de)serialization for the SQLite cache backend.
128
+ * Extracted from `sqlite-cache-engine.ts` so BOTH sides of the worker boundary
129
+ * can use it: the engine (main thread) for the legacy/shim paths, and
130
+ * `sqlite-worker.js` for worker-side plan execution (`select`). Pure module —
131
+ * no DOM, no worker, no engine imports — mirroring `plan-render.ts`.
132
+ */
133
+ function renderOrderSql(orderBy) {
134
+ return ` ORDER BY ${orderBy.map(([f, d]) => `json_extract(data, '$.${f}') ${d === "desc" ? "DESC" : "ASC"}`).join(", ")}`;
135
+ }
136
+ function comparisonSql(c, bind, params) {
137
+ const lhs = c.field === "id" ? "id" : `json_extract(data, '$.${c.field}')`;
138
+ const value = c.paramRef !== void 0 && (c.value === void 0 || Object.prototype.hasOwnProperty.call(params, c.paramRef)) ? params[c.paramRef] : c.value;
139
+ bind.push(scalar(value));
140
+ const op = c.op === "!=" ? "!=" : c.op;
141
+ return c.swap ? `? ${op} ${lhs}` : `${lhs} ${op} ?`;
142
+ }
143
+ function renderWhereSql(nodes, bind, params) {
144
+ return nodes.map((node) => {
145
+ if ("or" in node) return `(${node.or.map((c) => comparisonSql(c, bind, params)).join(" OR ")})`;
146
+ return comparisonSql(node, bind, params);
147
+ }).join(" AND ");
148
+ }
149
+ /** A comparable scalar for SQL binding: record links → `table:id`, everything
150
+ * else passed through (numbers/strings/bools). */
151
+ function scalar(value) {
152
+ if (value == null) return null;
153
+ if (typeof value === "object") return stableKey(value);
154
+ return value;
155
+ }
156
+ function serializeRow(row) {
157
+ return JSON.stringify(row, (_k, v) => {
158
+ if (v instanceof Uint8Array) return { __u8: toBase64(v) };
159
+ if (v && typeof v === "object") {
160
+ const rid = v;
161
+ if (rid.tb !== void 0 && rid.id !== void 0) return stableKey(v);
162
+ }
163
+ return v;
164
+ });
165
+ }
166
+ function reviveRow(json) {
167
+ if (json.indexOf("\"__u8\"") === -1) return JSON.parse(json);
168
+ return JSON.parse(json, (_k, v) => {
169
+ if (v && typeof v === "object" && typeof v.__u8 === "string") return fromBase64(v.__u8);
170
+ return v;
171
+ });
172
+ }
173
+ /**
174
+ * SELECT-clause expression yielding the row's `data` narrowed to `fields` (plus
175
+ * `id`), so SQLite never returns — and neither side ever parses — the fields the
176
+ * caller did not ask for. On a game list that is every row's `pgn`, which was
177
+ * being read, parsed and thrown away 50 rows at a time for eight rendered fields.
178
+ *
179
+ * Byte-identical to running {@link project} over the fully-parsed row, which is
180
+ * what both paths did before, and verified so against real SQLite:
181
+ * `json_each` walks the keys the row ACTUALLY has, so an ABSENT key stays absent
182
+ * rather than becoming an explicit null — the difference `json_object`/
183
+ * `json_extract` would have introduced. A stored null, nested objects and
184
+ * arrays, and `{__u8}` blob tags all round-trip unchanged, because
185
+ * `json_group_object` understands `json_each`'s `value` column as JSON rather
186
+ * than as text. `COALESCE` covers a row sharing none of the requested keys,
187
+ * where the subquery yields NULL and `reviveRow` would throw.
188
+ *
189
+ * Deliberately unaliased, so the emitted statement keeps the exact shape the
190
+ * callers already produce (`FROM "t" WHERE id IN (…)`).
191
+ *
192
+ * Binds one parameter per key, pushed onto `bind` — these land in the SELECT
193
+ * clause, so they must be bound BEFORE any WHERE parameters.
194
+ */
195
+ function projectedDataSql(fields, bind) {
196
+ const keys = ["id", ...fields];
197
+ for (const k of keys) bind.push(k);
198
+ return `COALESCE((SELECT json_group_object(je.key, je.value) FROM json_each(data) je WHERE je.key IN (${keys.map(() => "?").join(", ")})), '{}') AS data`;
199
+ }
200
+ function project(row, fields) {
201
+ const out = {};
202
+ for (const f of ["id", ...fields]) if (f in row) out[f] = row[f];
203
+ return out;
204
+ }
205
+ function toBase64(bytes) {
206
+ let bin = "";
207
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
208
+ return typeof btoa !== "undefined" ? btoa(bin) : Buffer.from(bytes).toString("base64");
209
+ }
210
+ function fromBase64(b64) {
211
+ if (typeof atob !== "undefined") {
212
+ const bin = atob(b64);
213
+ const out = new Uint8Array(bin.length);
214
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
215
+ return out;
216
+ }
217
+ return new Uint8Array(Buffer.from(b64, "base64"));
218
+ }
219
+
220
+ //#endregion
221
+ //#region src/services/database/sqlite-open.ts
222
+ const DEFAULT_MAX_ATTEMPTS = 3;
223
+ /** Bounded on purpose: this runs on the boot path, before the first query. */
224
+ const DEFAULT_BACKOFF_MS = [250, 500];
225
+ /**
226
+ * Leader-promotion profile (~5.8s worst case). A dead leader's sync access
227
+ * handles release when the browser garbage-collects its worker, typically
228
+ * well under a second but not synchronously with the Web Lock release the
229
+ * election observed, so promotion retries longer than a cold boot.
230
+ */
231
+ const PROMOTION_OPEN_OPTIONS = {
232
+ maxAttempts: 10,
233
+ backoffMs: [
234
+ 50,
235
+ 100,
236
+ 200,
237
+ 400,
238
+ 800,
239
+ 1e3
240
+ ]
241
+ };
242
+ /** Failures no retry can fix: the APIs aren't there at all (insecure context,
243
+ * or a browser without sync access handles). Fall back immediately. */
244
+ const UNRETRYABLE = ["Missing required OPFS APIs"];
245
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
246
+ /** Keep the DOMException name (e.g. `NoModificationAllowedError` for a pool
247
+ * locked by another tab): it is the most diagnostic part of the failure. */
248
+ function errMessage(e) {
249
+ if (e instanceof Error) return e.name && e.name !== "Error" ? `${e.name}: ${e.message}` : e.message;
250
+ return String(e);
251
+ }
252
+ function fallbackToMemory(sqlite3, dbName, reason, attempts) {
253
+ const tried = attempts > 0 ? ` after ${attempts} attempt${attempts === 1 ? "" : "s"}` : "";
254
+ console.error(`[sp00ky] OPFS persistence unavailable for "${dbName}"${tried}: ${reason}. The local SQLite cache is running IN MEMORY, which keeps the whole dataset in RAM and loses every local write on reload. The usual cause is another tab of this app holding the storage lock, so closing the other tabs and reloading restores persistence.`);
255
+ return {
256
+ db: new sqlite3.oo1.DB(":memory:", "c"),
257
+ persisted: false,
258
+ opfsError: reason
259
+ };
260
+ }
261
+ /**
262
+ * Open `dbName`'s handle. Never throws for a storage problem: a caller that
263
+ * asked for persistence and can't have it gets a working in-memory handle plus
264
+ * `persisted: false` and an `opfsError` to report.
265
+ */
266
+ async function openDb(sqlite3, dbName, useOpfs, opts = {}) {
267
+ if (!useOpfs) return {
268
+ db: new sqlite3.oo1.DB(":memory:", "c"),
269
+ persisted: false
270
+ };
271
+ if (!sqlite3.installOpfsSAHPoolVfs) {
272
+ if (opts.disallowMemoryFallback) throw new Error("opfs-unavailable: sqlite-wasm build has no installOpfsSAHPoolVfs");
273
+ return fallbackToMemory(sqlite3, dbName, "sqlite-wasm build has no installOpfsSAHPoolVfs", 0);
274
+ }
275
+ const maxAttempts = Math.max(1, opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
276
+ const backoffMs = opts.backoffMs ?? DEFAULT_BACKOFF_MS;
277
+ const sleep = opts.sleep ?? defaultSleep;
278
+ let lastError = "unknown error";
279
+ let attempts = 0;
280
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
281
+ attempts = attempt;
282
+ try {
283
+ const pool = await sqlite3.installOpfsSAHPoolVfs({
284
+ name: `sp00ky-${dbName}`,
285
+ ...attempt > 1 ? { forceReinitIfPreviouslyFailed: true } : {}
286
+ });
287
+ return {
288
+ db: new pool.OpfsSAHPoolDb(`/${dbName}.sqlite3`),
289
+ persisted: true,
290
+ pool
291
+ };
292
+ } catch (e) {
293
+ lastError = errMessage(e);
294
+ if (attempt === maxAttempts || UNRETRYABLE.some((m) => lastError.includes(m))) break;
295
+ await sleep(backoffMs[Math.min(attempt - 1, backoffMs.length - 1)] ?? 0);
296
+ }
297
+ }
298
+ if (opts.disallowMemoryFallback) throw new Error(`opfs-unavailable: ${lastError} (after ${attempts} attempts)`);
299
+ return fallbackToMemory(sqlite3, dbName, lastError, attempts);
300
+ }
301
+
302
+ //#endregion
303
+ export { renderOrderSql as a, serializeRow as c, projectedDataSql as i, resolveRelations as l, openDb as n, renderWhereSql as o, project as r, reviveRow as s, PROMOTION_OPEN_OPTIONS as t, stableKey as u };
@@ -0,0 +1 @@
1
+ export {};