@spooky-sync/core 0.0.1-canary.30 → 0.0.1-canary.300

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 (257) hide show
  1. package/AGENTS.md +81 -0
  2. package/README.md +56 -16
  3. package/dist/index.d.ts +2026 -137
  4. package/dist/index.js +14939 -2432
  5. package/dist/live.d.ts +141 -0
  6. package/dist/live.js +709 -0
  7. package/dist/otel/index.d.ts +1 -1
  8. package/dist/otel/index.js +3 -3
  9. package/dist/pure.d.ts +329 -0
  10. package/dist/pure.js +24 -0
  11. package/dist/sqlite-open.js +358 -0
  12. package/dist/sqlite-worker.d.ts +1 -0
  13. package/dist/sqlite-worker.js +439 -0
  14. package/dist/sw.d.ts +207 -0
  15. package/dist/sw.js +393 -0
  16. package/dist/tabs-broker-worker.d.ts +8 -0
  17. package/dist/tabs-broker-worker.js +472 -0
  18. package/dist/types.d.ts +1765 -38
  19. package/dist/worker-edges.js +423 -0
  20. package/dist/worker-impersonation.js +60 -0
  21. package/dist/worker-ref-tables.d.ts +46 -0
  22. package/dist/worker-types.d.ts +112 -0
  23. package/dist/worker-types.js +73 -0
  24. package/package.json +27 -7
  25. package/scripts/check-broker-bundle.mjs +33 -0
  26. package/scripts/check-worker-entries.mjs +172 -0
  27. package/skills/sp00ky-core/SKILL.md +31 -6
  28. package/skills/sp00ky-core/references/config.md +6 -0
  29. package/src/boot/auth-flip.saga.test.ts +82 -0
  30. package/src/boot/auth-flip.saga.ts +62 -0
  31. package/src/boot/boot.saga.test.ts +158 -0
  32. package/src/boot/boot.saga.ts +131 -0
  33. package/src/boot/bucket-switch.saga.test.ts +187 -0
  34. package/src/boot/bucket-switch.saga.ts +133 -0
  35. package/src/boot/preload.saga.test.ts +51 -0
  36. package/src/boot/preload.saga.ts +31 -0
  37. package/src/bucket-blurhash.test.ts +148 -0
  38. package/src/build-globals.d.ts +12 -0
  39. package/src/client/bucket-handle.test.ts +117 -0
  40. package/src/client/bucket-handle.ts +227 -0
  41. package/src/client/router.test.ts +58 -0
  42. package/src/client/router.ts +100 -0
  43. package/src/client/runtime.test.ts +195 -0
  44. package/src/client/runtime.ts +296 -0
  45. package/src/client/services.ts +370 -0
  46. package/src/client/sp00ky-client.test.ts +303 -0
  47. package/src/client/sp00ky-client.ts +586 -0
  48. package/src/events/events.test.ts +2 -1
  49. package/src/events/index.ts +3 -0
  50. package/src/index.ts +69 -1
  51. package/src/kernel/constants.test.ts +19 -0
  52. package/src/kernel/constants.ts +80 -0
  53. package/src/kernel/effects.test.ts +40 -0
  54. package/src/kernel/effects.ts +161 -0
  55. package/src/kernel/events.ts +57 -0
  56. package/src/kernel/interpreter.test.ts +116 -0
  57. package/src/kernel/interpreter.ts +148 -0
  58. package/src/kernel/saga.test.ts +89 -0
  59. package/src/kernel/saga.ts +73 -0
  60. package/src/live/index.test.ts +478 -0
  61. package/src/live/index.ts +1013 -0
  62. package/src/modules/app-release/index.test.ts +125 -0
  63. package/src/modules/app-release/index.ts +209 -0
  64. package/src/modules/auth/auth.local-first.test.ts +141 -0
  65. package/src/modules/auth/events/index.ts +9 -2
  66. package/src/modules/auth/impersonation-banner.ts +443 -0
  67. package/src/modules/auth/impersonation.test.ts +355 -0
  68. package/src/modules/auth/impersonation.ts +179 -0
  69. package/src/modules/auth/index.ts +386 -17
  70. package/src/modules/crdt/crdt-field.ts +294 -0
  71. package/src/modules/crdt/crdt-hydration.test.ts +210 -0
  72. package/src/modules/crdt/crdt-reconnect.test.ts +195 -0
  73. package/src/modules/crdt/index.ts +463 -0
  74. package/src/modules/crdt/loro-loader.ts +25 -0
  75. package/src/modules/devtools/flags.ts +349 -0
  76. package/src/modules/devtools/index.ts +746 -95
  77. package/src/modules/devtools/mutations.test.ts +92 -0
  78. package/src/modules/devtools/mutations.ts +160 -0
  79. package/src/modules/devtools/notify-throttle.test.ts +166 -0
  80. package/src/modules/devtools/state-shape.test.ts +263 -0
  81. package/src/modules/devtools/storage-info.test.ts +79 -0
  82. package/src/modules/devtools/storage-info.ts +168 -0
  83. package/src/modules/devtools/versions.test.ts +74 -0
  84. package/src/modules/devtools/versions.ts +110 -0
  85. package/src/modules/feature-flag/index.test.ts +251 -0
  86. package/src/modules/feature-flag/index.ts +316 -0
  87. package/src/modules/ref-tables.test.ts +91 -0
  88. package/src/modules/ref-tables.ts +88 -0
  89. package/src/modules/web-push/index.test.ts +824 -0
  90. package/src/modules/web-push/index.ts +1027 -0
  91. package/src/mutation/jobs.test.ts +25 -0
  92. package/src/mutation/jobs.ts +41 -0
  93. package/src/mutation/mutation-id.test.ts +37 -0
  94. package/src/mutation/mutation-id.ts +35 -0
  95. package/src/mutation/pending-writes.integration.test.ts +175 -0
  96. package/src/mutation/push.saga.test.ts +285 -0
  97. package/src/mutation/push.saga.ts +217 -0
  98. package/src/mutation/rows.test.ts +231 -0
  99. package/src/mutation/rows.ts +427 -0
  100. package/src/mutation/tray.saga.test.ts +63 -0
  101. package/src/mutation/tray.saga.ts +48 -0
  102. package/src/mutation/write.saga.test.ts +316 -0
  103. package/src/mutation/write.saga.ts +268 -0
  104. package/src/otel/index.ts +10 -7
  105. package/src/pure.ts +65 -0
  106. package/src/push/types.ts +174 -0
  107. package/src/query/env.test.ts +23 -0
  108. package/src/query/env.ts +56 -0
  109. package/src/query/fetch.saga.test.ts +199 -0
  110. package/src/query/fetch.saga.ts +120 -0
  111. package/src/query/hash.test.ts +14 -0
  112. package/src/query/hash.ts +18 -0
  113. package/src/query/id-set-plan.test.ts +132 -0
  114. package/src/query/lifecycle.saga.test.ts +274 -0
  115. package/src/query/lifecycle.saga.ts +159 -0
  116. package/src/query/materialize.saga.test.ts +115 -0
  117. package/src/query/materialize.saga.ts +84 -0
  118. package/src/query/materialize.test.ts +58 -0
  119. package/src/query/materialize.ts +57 -0
  120. package/src/query/membership.saga.test.ts +350 -0
  121. package/src/query/membership.saga.ts +278 -0
  122. package/src/query/membership.test.ts +118 -0
  123. package/src/query/membership.ts +201 -0
  124. package/src/query/register.saga.test.ts +293 -0
  125. package/src/query/register.saga.ts +226 -0
  126. package/src/query/render-set.test.ts +47 -0
  127. package/src/query/render-set.ts +71 -0
  128. package/src/query/sql.test.ts +63 -0
  129. package/src/query/sql.ts +93 -0
  130. package/src/query/window-query.test.ts +70 -0
  131. package/src/query/window-query.ts +195 -0
  132. package/src/services/blobs/blob-cache.test.ts +359 -0
  133. package/src/services/blobs/blob-cache.ts +603 -0
  134. package/src/services/blobs/blob-manifest.ts +227 -0
  135. package/src/services/blobs/blob-store.test.ts +77 -0
  136. package/src/services/blobs/blob-store.ts +359 -0
  137. package/src/services/blobs/blob.fixture.ts +90 -0
  138. package/src/services/blobs/index.ts +70 -0
  139. package/src/services/database/cache-engine.ts +199 -0
  140. package/src/services/database/connection-supervisor.test.ts +318 -0
  141. package/src/services/database/connection-supervisor.ts +422 -0
  142. package/src/services/database/database.query-timeout.test.ts +140 -0
  143. package/src/services/database/database.responses.test.ts +43 -0
  144. package/src/services/database/database.ts +146 -61
  145. package/src/services/database/engine-factory.ts +33 -0
  146. package/src/services/database/errors.ts +34 -0
  147. package/src/services/database/events/index.ts +2 -1
  148. package/src/services/database/index.ts +7 -0
  149. package/src/services/database/landed-body-clear.integration.test.ts +236 -0
  150. package/src/services/database/local-migrator.integration.test.ts +110 -0
  151. package/src/services/database/local-migrator.ts +59 -16
  152. package/src/services/database/local.test.ts +64 -0
  153. package/src/services/database/local.ts +487 -63
  154. package/src/services/database/plan-render.test.ts +159 -0
  155. package/src/services/database/plan-render.ts +108 -0
  156. package/src/services/database/relation-resolver.test.ts +467 -0
  157. package/src/services/database/relation-resolver.ts +0 -0
  158. package/src/services/database/remote.ts +213 -6
  159. package/src/services/database/sqlite-cache-engine.test.ts +617 -0
  160. package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
  161. package/src/services/database/sqlite-cache-engine.ts +1372 -0
  162. package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
  163. package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
  164. package/src/services/database/sqlite-lock-verify.test.ts +33 -0
  165. package/src/services/database/sqlite-lock-verify.ts +45 -0
  166. package/src/services/database/sqlite-open.test.ts +150 -0
  167. package/src/services/database/sqlite-open.ts +164 -0
  168. package/src/services/database/sqlite-plan-sql.test.ts +139 -0
  169. package/src/services/database/sqlite-plan-sql.ts +179 -0
  170. package/src/services/database/sqlite-projection.test.ts +121 -0
  171. package/src/services/database/sqlite-select.integration.test.ts +185 -0
  172. package/src/services/database/sqlite-select.test.ts +246 -0
  173. package/src/services/database/sqlite-select.ts +131 -0
  174. package/src/services/database/sqlite-transport.fixture.ts +30 -0
  175. package/src/services/database/sqlite-transport.ts +224 -0
  176. package/src/services/database/sqlite-worker.ts +437 -0
  177. package/src/services/database/surql-translate.ts +416 -0
  178. package/src/services/database/surreal-cache-engine.ts +161 -0
  179. package/src/services/logger/index.test.ts +132 -0
  180. package/src/services/logger/index.ts +189 -7
  181. package/src/services/persistence/localstorage.ts +2 -2
  182. package/src/services/persistence/resilient.ts +10 -3
  183. package/src/services/persistence/surrealdb.ts +4 -4
  184. package/src/services/stream-processor/index.ts +779 -67
  185. package/src/services/stream-processor/permissions.test.ts +47 -0
  186. package/src/services/stream-processor/permissions.ts +53 -0
  187. package/src/services/stream-processor/stream-processor.batch.test.ts +186 -0
  188. package/src/services/stream-processor/stream-processor.prime.test.ts +198 -0
  189. package/src/services/stream-processor/stream-processor.reset.test.ts +226 -0
  190. package/src/services/stream-processor/stream-processor.test.ts +1 -1
  191. package/src/services/stream-processor/wasm-types.ts +58 -2
  192. package/src/services/tabs/broker-client.ts +283 -0
  193. package/src/services/tabs/broker.test.ts +327 -0
  194. package/src/services/tabs/coordinator.test.ts +386 -0
  195. package/src/services/tabs/coordinator.ts +638 -0
  196. package/src/services/tabs/fake-ports.fixture.ts +112 -0
  197. package/src/services/tabs/leader-locks.ts +75 -0
  198. package/src/services/tabs/protocol.ts +262 -0
  199. package/src/services/tabs/support.ts +36 -0
  200. package/src/services/tabs/tabs-broker-worker.ts +640 -0
  201. package/src/sp00ky.ts +5 -395
  202. package/src/state/client-state.ts +200 -0
  203. package/src/state/lifecycle.test.ts +118 -0
  204. package/src/state/lifecycle.ts +86 -0
  205. package/src/state/reducers.test.ts +304 -0
  206. package/src/state/reducers.ts +526 -0
  207. package/src/state/selectors.test.ts +167 -0
  208. package/src/state/selectors.ts +216 -0
  209. package/src/sw/index.test.ts +582 -0
  210. package/src/sw/index.ts +607 -0
  211. package/src/sw/store.ts +96 -0
  212. package/src/sync/connection.saga.test.ts +86 -0
  213. package/src/sync/connection.saga.ts +85 -0
  214. package/src/sync/live.saga.test.ts +178 -0
  215. package/src/sync/live.saga.ts +107 -0
  216. package/src/sync/policy.health.test.ts +56 -0
  217. package/src/sync/policy.test.ts +659 -0
  218. package/src/sync/policy.ts +502 -0
  219. package/src/sync/poll.saga.test.ts +51 -0
  220. package/src/sync/poll.saga.ts +36 -0
  221. package/src/sync/tabs.saga.test.ts +78 -0
  222. package/src/sync/tabs.saga.ts +96 -0
  223. package/src/testing/adapters.ts +133 -0
  224. package/src/testing/build.ts +63 -0
  225. package/src/testing/fake-services.ts +48 -0
  226. package/src/testing/run-pure.test.ts +92 -0
  227. package/src/testing/run-pure.ts +128 -0
  228. package/src/testing/services.ts +17 -0
  229. package/src/types.ts +624 -10
  230. package/src/utils/blurhash.ts +90 -0
  231. package/src/utils/edges.ts +39 -0
  232. package/src/utils/error-classification.test.ts +54 -0
  233. package/src/utils/error-classification.ts +25 -0
  234. package/src/utils/index.ts +75 -9
  235. package/src/utils/parser.test.ts +67 -102
  236. package/src/utils/parser.ts +64 -2
  237. package/src/utils/semver.test.ts +32 -0
  238. package/src/utils/semver.ts +30 -0
  239. package/src/utils/sha256.ts +15 -0
  240. package/src/utils/surql.ts +30 -18
  241. package/src/utils/utils.test.ts +2 -1
  242. package/tsdown.config.ts +117 -3
  243. package/vitest.config.ts +48 -0
  244. package/src/modules/cache/index.ts +0 -241
  245. package/src/modules/cache/types.ts +0 -19
  246. package/src/modules/data/data.test.ts +0 -58
  247. package/src/modules/data/index.ts +0 -879
  248. package/src/modules/sync/engine.ts +0 -173
  249. package/src/modules/sync/events/index.ts +0 -77
  250. package/src/modules/sync/index.ts +0 -3
  251. package/src/modules/sync/queue/index.ts +0 -2
  252. package/src/modules/sync/queue/queue-down.ts +0 -89
  253. package/src/modules/sync/queue/queue-up.ts +0 -223
  254. package/src/modules/sync/scheduler.ts +0 -84
  255. package/src/modules/sync/sync.ts +0 -421
  256. package/src/modules/sync/utils.test.ts +0 -311
  257. package/src/modules/sync/utils.ts +0 -167
@@ -0,0 +1,358 @@
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
+ if (isOne && !looksLikeRecordId(key)) continue;
50
+ const k = stableKey(key);
51
+ if (!keySet.has(k)) keySet.set(k, key);
52
+ }
53
+ const grouped = /* @__PURE__ */ new Map();
54
+ if (keySet.size > 0) {
55
+ const children = await fetcher.fetchRelation({
56
+ table: relation.table,
57
+ matchField,
58
+ keys: [...keySet.values()],
59
+ where: relation.where,
60
+ orderBy: relation.orderBy,
61
+ select: relation.select
62
+ });
63
+ await resolveRelations(children, relation.relations, fetcher, depth + 1, [...path, relation.alias]);
64
+ for (const child of children) {
65
+ const k = stableKey(child[matchField]);
66
+ const bucket = grouped.get(k);
67
+ if (bucket) bucket.push(child);
68
+ else grouped.set(k, [child]);
69
+ }
70
+ }
71
+ for (const parent of parents) {
72
+ const key = parentKeyOf(parent);
73
+ let bucket = key == null ? [] : grouped.get(stableKey(key)) ?? [];
74
+ if (relation.orderBy && relation.orderBy.length > 0) bucket = sortRows(bucket, relation.orderBy);
75
+ if (relation.limit !== void 0) bucket = bucket.slice(0, relation.limit);
76
+ if (isOne) {
77
+ const raw = parent[relation.alias];
78
+ const keepRaw = relation.alias === relation.foreignKeyField && raw != null && !looksLikeRecordId(raw);
79
+ parent[relation.alias] = bucket[0] ?? (keepRaw ? raw : null);
80
+ } else parent[relation.alias] = bucket;
81
+ }
82
+ }
83
+ /**
84
+ * Whether a correlation value can address a record: a RecordId (duck-typed) or
85
+ * a `table:id` string. Anything else (a plain display string, a number) cannot
86
+ * be the target of a to-one relation and is skipped rather than fetched.
87
+ */
88
+ function looksLikeRecordId(value) {
89
+ if (value == null) return false;
90
+ if (typeof value === "string") return /^[A-Za-z_][A-Za-z0-9_]*:.+/.test(value);
91
+ if (typeof value === "object") {
92
+ const rid = value;
93
+ return rid.tb !== void 0 && rid.id !== void 0;
94
+ }
95
+ return false;
96
+ }
97
+ /**
98
+ * Stable grouping key for a correlation value. A `RecordId` (duck-typed to
99
+ * avoid a hard surrealdb dependency here) and its `table:id` string form must
100
+ * collapse to the same key so a parent FK stored either way matches the child.
101
+ */
102
+ function stableKey(value) {
103
+ if (value == null) return "\0null";
104
+ if (typeof value === "string") return value;
105
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
106
+ if (typeof value === "object") {
107
+ const rid = value;
108
+ if (rid.tb !== void 0 && rid.id !== void 0) return `${String(rid.tb)}:${stableKey(rid.id)}`;
109
+ if (typeof rid.toString === "function") {
110
+ const s = rid.toString();
111
+ if (s !== "[object Object]") return s;
112
+ }
113
+ return JSON.stringify(value);
114
+ }
115
+ return String(value);
116
+ }
117
+ /**
118
+ * Stable, multi-key sort matching SurrealQL `ORDER BY` semantics closely
119
+ * enough for cache display order: ascending null-last, type-aware compare, and
120
+ * a stable tiebreak (original index) so equal rows keep input order.
121
+ */
122
+ function sortRows(rows, orderBy) {
123
+ const indexed = rows.map((row, i) => [row, i]);
124
+ indexed.sort((a, b) => {
125
+ for (const [field, direction] of orderBy) {
126
+ const cmp = compareValues(a[0][field], b[0][field]);
127
+ if (cmp !== 0) return direction === "desc" ? -cmp : cmp;
128
+ }
129
+ return a[1] - b[1];
130
+ });
131
+ return indexed.map(([row]) => row);
132
+ }
133
+ function compareValues(a, b) {
134
+ if (a == null && b == null) return 0;
135
+ if (a == null) return 1;
136
+ if (b == null) return -1;
137
+ if (typeof a === "number" && typeof b === "number") return a - b;
138
+ const as = stableKey(a);
139
+ const bs = stableKey(b);
140
+ return as < bs ? -1 : as > bs ? 1 : 0;
141
+ }
142
+
143
+ //#endregion
144
+ //#region src/services/database/sqlite-plan-sql.ts
145
+ /**
146
+ * SQL rendering + value (de)serialization for the SQLite cache backend.
147
+ * Extracted from `sqlite-cache-engine.ts` so BOTH sides of the worker boundary
148
+ * can use it: the engine (main thread) for the legacy/shim paths, and
149
+ * `sqlite-worker.js` for worker-side plan execution (`select`). Pure module —
150
+ * no DOM, no worker, no engine imports — mirroring `plan-render.ts`.
151
+ */
152
+ function renderOrderSql(orderBy) {
153
+ return ` ORDER BY ${orderBy.map(([f, d]) => `json_extract(data, '$.${f}') ${d === "desc" ? "DESC" : "ASC"}`).join(", ")}`;
154
+ }
155
+ function comparisonSql(c, bind, params) {
156
+ const lhs = c.field === "id" ? "id" : `json_extract(data, '$.${c.field}')`;
157
+ const value = c.paramRef !== void 0 && (c.value === void 0 || Object.prototype.hasOwnProperty.call(params, c.paramRef)) ? params[c.paramRef] : c.value;
158
+ bind.push(scalar(value));
159
+ const op = c.op === "!=" ? "!=" : c.op;
160
+ return c.swap ? `? ${op} ${lhs}` : `${lhs} ${op} ?`;
161
+ }
162
+ function renderWhereSql(nodes, bind, params) {
163
+ return nodes.map((node) => {
164
+ if ("or" in node) return `(${node.or.map((c) => comparisonSql(c, bind, params)).join(" OR ")})`;
165
+ return comparisonSql(node, bind, params);
166
+ }).join(" AND ");
167
+ }
168
+ /** A comparable scalar for SQL binding: record links → `table:id`, everything
169
+ * else passed through (numbers/strings/bools). */
170
+ function scalar(value) {
171
+ if (value == null) return null;
172
+ if (typeof value === "object") return stableKey(value);
173
+ return value;
174
+ }
175
+ function serializeRow(row) {
176
+ return JSON.stringify(row, (_k, v) => {
177
+ if (v instanceof Uint8Array) return { __u8: toBase64(v) };
178
+ if (v && typeof v === "object") {
179
+ const rid = v;
180
+ if (rid.tb !== void 0 && rid.id !== void 0) return stableKey(v);
181
+ }
182
+ return v;
183
+ });
184
+ }
185
+ /**
186
+ * Serialize a MERGE row ONCE for both halves of the merge upsert: the row a
187
+ * fresh INSERT stores and the RFC 7396 delta `json_patch` applies on conflict.
188
+ *
189
+ * A top-level own key holding `undefined` means "clear this field", which is
190
+ * what SurrealDB's MERGE does with it (the SDK sends it as NONE). JSON drops
191
+ * such a key, so a patch built by {@link serializeRow} alone never mentioned
192
+ * it and the stored value survived every merge: a field cleared on the server
193
+ * stayed set locally. The patch therefore carries each one as a `null` member,
194
+ * which `json_patch` removes; the insert row simply lacks it, exactly as
195
+ * before. Appended to the already-serialized text, so the common case costs no
196
+ * second stringify. Nested `undefined` stays dropped (never a clear here).
197
+ */
198
+ function serializeMergeRow(row) {
199
+ const insert = serializeRow(row);
200
+ const cleared = [];
201
+ for (const key of Object.keys(row)) if (row[key] === void 0) cleared.push(`${JSON.stringify(key)}:null`);
202
+ if (cleared.length === 0 || insert[0] !== "{") return {
203
+ insert,
204
+ patch: insert
205
+ };
206
+ const members = cleared.join(",");
207
+ return {
208
+ insert,
209
+ patch: insert === "{}" ? `{${members}}` : `${insert.slice(0, -1)},${members}}`
210
+ };
211
+ }
212
+ function reviveRow(json) {
213
+ if (json.indexOf("\"__u8\"") === -1) return JSON.parse(json);
214
+ return JSON.parse(json, (_k, v) => {
215
+ if (v && typeof v === "object" && typeof v.__u8 === "string") return fromBase64(v.__u8);
216
+ return v;
217
+ });
218
+ }
219
+ /**
220
+ * SELECT-clause expression yielding the row's `data` narrowed to `fields` (plus
221
+ * `id`), so SQLite never returns — and neither side ever parses — the fields the
222
+ * caller did not ask for. On a game list that is every row's `pgn`, which was
223
+ * being read, parsed and thrown away 50 rows at a time for eight rendered fields.
224
+ *
225
+ * Byte-identical to running {@link project} over the fully-parsed row, which is
226
+ * what both paths did before, and verified so against real SQLite:
227
+ * `json_each` walks the keys the row ACTUALLY has, so an ABSENT key stays absent
228
+ * rather than becoming an explicit null — the difference `json_object`/
229
+ * `json_extract` would have introduced. A stored null, nested objects and
230
+ * arrays, and `{__u8}` blob tags all round-trip unchanged, because
231
+ * `json_group_object` understands `json_each`'s `value` column as JSON rather
232
+ * than as text. `COALESCE` covers a row sharing none of the requested keys,
233
+ * where the subquery yields NULL and `reviveRow` would throw.
234
+ *
235
+ * The one value `je.value` does NOT carry back is a TOP-LEVEL boolean. SQLite
236
+ * has no boolean type, so `json_each` hands `true`/`false` out as the integers
237
+ * 1/0 and `json_group_object` writes integers back. A projected row therefore
238
+ * read `published: 1` where the same row read whole read `published: true`,
239
+ * and every `=== true` in an app turned false the moment a screen painted from
240
+ * the local cache. (A boolean NESTED in an object or array survives: those come
241
+ * back as whole JSON documents, untouched.) `je.type` still knows what it was,
242
+ * so the two boolean types are rebuilt as JSON literals on the way out.
243
+ *
244
+ * Deliberately unaliased, so the emitted statement keeps the exact shape the
245
+ * callers already produce (`FROM "t" WHERE id IN (…)`).
246
+ *
247
+ * Binds one parameter per key, pushed onto `bind` — these land in the SELECT
248
+ * clause, so they must be bound BEFORE any WHERE parameters.
249
+ */
250
+ function projectedDataSql(fields, bind) {
251
+ const keys = ["id", ...fields];
252
+ for (const k of keys) bind.push(k);
253
+ return `COALESCE((SELECT json_group_object(je.key, CASE je.type WHEN 'true' THEN json('true') WHEN 'false' THEN json('false') ELSE je.value END) FROM json_each(data) je WHERE je.key IN (${keys.map(() => "?").join(", ")})), '{}') AS data`;
254
+ }
255
+ function project(row, fields) {
256
+ const out = {};
257
+ for (const f of ["id", ...fields]) if (f in row) out[f] = row[f];
258
+ return out;
259
+ }
260
+ function toBase64(bytes) {
261
+ let bin = "";
262
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
263
+ return typeof btoa !== "undefined" ? btoa(bin) : Buffer.from(bytes).toString("base64");
264
+ }
265
+ function fromBase64(b64) {
266
+ if (typeof atob !== "undefined") {
267
+ const bin = atob(b64);
268
+ const out = new Uint8Array(bin.length);
269
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
270
+ return out;
271
+ }
272
+ return new Uint8Array(Buffer.from(b64, "base64"));
273
+ }
274
+
275
+ //#endregion
276
+ //#region src/services/database/sqlite-open.ts
277
+ const DEFAULT_MAX_ATTEMPTS = 3;
278
+ /** Bounded on purpose: this runs on the boot path, before the first query. */
279
+ const DEFAULT_BACKOFF_MS = [250, 500];
280
+ /**
281
+ * Leader-promotion profile (~5.8s worst case). A dead leader's sync access
282
+ * handles release when the browser garbage-collects its worker, typically
283
+ * well under a second but not synchronously with the Web Lock release the
284
+ * election observed, so promotion retries longer than a cold boot.
285
+ */
286
+ const PROMOTION_OPEN_OPTIONS = {
287
+ maxAttempts: 10,
288
+ backoffMs: [
289
+ 50,
290
+ 100,
291
+ 200,
292
+ 400,
293
+ 800,
294
+ 1e3
295
+ ]
296
+ };
297
+ /** Failures no retry can fix: the APIs aren't there at all (insecure context,
298
+ * or a browser without sync access handles). Fall back immediately. */
299
+ const UNRETRYABLE = ["Missing required OPFS APIs"];
300
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
301
+ /** Keep the DOMException name (e.g. `NoModificationAllowedError` for a pool
302
+ * locked by another tab): it is the most diagnostic part of the failure. */
303
+ function errMessage(e) {
304
+ if (e instanceof Error) return e.name && e.name !== "Error" ? `${e.name}: ${e.message}` : e.message;
305
+ return String(e);
306
+ }
307
+ function fallbackToMemory(sqlite3, dbName, reason, attempts) {
308
+ const tried = attempts > 0 ? ` after ${attempts} attempt${attempts === 1 ? "" : "s"}` : "";
309
+ 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.`);
310
+ return {
311
+ db: new sqlite3.oo1.DB(":memory:", "c"),
312
+ persisted: false,
313
+ opfsError: reason
314
+ };
315
+ }
316
+ /**
317
+ * Open `dbName`'s handle. Never throws for a storage problem: a caller that
318
+ * asked for persistence and can't have it gets a working in-memory handle plus
319
+ * `persisted: false` and an `opfsError` to report.
320
+ */
321
+ async function openDb(sqlite3, dbName, useOpfs, opts = {}) {
322
+ if (!useOpfs) return {
323
+ db: new sqlite3.oo1.DB(":memory:", "c"),
324
+ persisted: false
325
+ };
326
+ if (!sqlite3.installOpfsSAHPoolVfs) {
327
+ if (opts.disallowMemoryFallback) throw new Error("opfs-unavailable: sqlite-wasm build has no installOpfsSAHPoolVfs");
328
+ return fallbackToMemory(sqlite3, dbName, "sqlite-wasm build has no installOpfsSAHPoolVfs", 0);
329
+ }
330
+ const maxAttempts = Math.max(1, opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
331
+ const backoffMs = opts.backoffMs ?? DEFAULT_BACKOFF_MS;
332
+ const sleep = opts.sleep ?? defaultSleep;
333
+ let lastError = "unknown error";
334
+ let attempts = 0;
335
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
336
+ attempts = attempt;
337
+ try {
338
+ const pool = await sqlite3.installOpfsSAHPoolVfs({
339
+ name: `sp00ky-${dbName}`,
340
+ ...attempt > 1 ? { forceReinitIfPreviouslyFailed: true } : {}
341
+ });
342
+ return {
343
+ db: new pool.OpfsSAHPoolDb(`/${dbName}.sqlite3`),
344
+ persisted: true,
345
+ pool
346
+ };
347
+ } catch (e) {
348
+ lastError = errMessage(e);
349
+ if (attempt === maxAttempts || UNRETRYABLE.some((m) => lastError.includes(m))) break;
350
+ await sleep(backoffMs[Math.min(attempt - 1, backoffMs.length - 1)] ?? 0);
351
+ }
352
+ }
353
+ if (opts.disallowMemoryFallback) throw new Error(`opfs-unavailable: ${lastError} (after ${attempts} attempts)`);
354
+ return fallbackToMemory(sqlite3, dbName, lastError, attempts);
355
+ }
356
+
357
+ //#endregion
358
+ export { renderOrderSql as a, serializeMergeRow as c, stableKey as d, projectedDataSql as i, serializeRow as l, openDb as n, renderWhereSql as o, project as r, reviveRow as s, PROMOTION_OPEN_OPTIONS as t, resolveRelations as u };
@@ -0,0 +1 @@
1
+ export {};