@spooky-sync/core 0.0.1-canary.27 → 0.0.1-canary.271

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 (231) hide show
  1. package/AGENTS.md +78 -0
  2. package/README.md +56 -16
  3. package/dist/index.d.ts +2279 -81
  4. package/dist/index.js +13490 -2585
  5. package/dist/otel/index.d.ts +2 -2
  6. package/dist/otel/index.js +6 -6
  7. package/dist/sqlite-open.js +331 -0
  8. package/dist/sqlite-worker.d.ts +1 -0
  9. package/dist/sqlite-worker.js +439 -0
  10. package/dist/tabs-broker-worker.d.ts +8 -0
  11. package/dist/tabs-broker-worker.js +472 -0
  12. package/dist/types.d.ts +939 -46
  13. package/package.json +16 -8
  14. package/scripts/check-broker-bundle.mjs +33 -0
  15. package/skills/{spooky-core → sp00ky-core}/SKILL.md +30 -12
  16. package/skills/{spooky-core → sp00ky-core}/references/auth.md +1 -1
  17. package/skills/{spooky-core → sp00ky-core}/references/config.md +2 -2
  18. package/src/boot/auth-flip.saga.test.ts +82 -0
  19. package/src/boot/auth-flip.saga.ts +62 -0
  20. package/src/boot/boot.saga.test.ts +151 -0
  21. package/src/boot/boot.saga.ts +128 -0
  22. package/src/boot/bucket-switch.saga.test.ts +187 -0
  23. package/src/boot/bucket-switch.saga.ts +131 -0
  24. package/src/boot/preload.saga.test.ts +51 -0
  25. package/src/boot/preload.saga.ts +31 -0
  26. package/src/bucket-blurhash.test.ts +148 -0
  27. package/src/build-globals.d.ts +12 -0
  28. package/src/client/bucket-handle.test.ts +117 -0
  29. package/src/client/bucket-handle.ts +227 -0
  30. package/src/client/router.test.ts +56 -0
  31. package/src/client/router.ts +96 -0
  32. package/src/client/runtime.test.ts +192 -0
  33. package/src/client/runtime.ts +296 -0
  34. package/src/client/services.ts +386 -0
  35. package/src/client/sp00ky-client.test.ts +264 -0
  36. package/src/client/sp00ky-client.ts +528 -0
  37. package/src/events/events.test.ts +2 -1
  38. package/src/events/index.ts +3 -0
  39. package/src/index.ts +37 -2
  40. package/src/kernel/constants.test.ts +19 -0
  41. package/src/kernel/constants.ts +61 -0
  42. package/src/kernel/effects.test.ts +40 -0
  43. package/src/kernel/effects.ts +161 -0
  44. package/src/kernel/events.ts +56 -0
  45. package/src/kernel/interpreter.test.ts +116 -0
  46. package/src/kernel/interpreter.ts +148 -0
  47. package/src/kernel/saga.test.ts +89 -0
  48. package/src/kernel/saga.ts +73 -0
  49. package/src/modules/app-release/index.test.ts +125 -0
  50. package/src/modules/app-release/index.ts +209 -0
  51. package/src/modules/auth/auth.local-first.test.ts +141 -0
  52. package/src/modules/auth/events/index.ts +9 -2
  53. package/src/modules/auth/impersonation-banner.ts +443 -0
  54. package/src/modules/auth/impersonation.test.ts +355 -0
  55. package/src/modules/auth/impersonation.ts +179 -0
  56. package/src/modules/auth/index.ts +344 -27
  57. package/src/modules/crdt/crdt-field.ts +294 -0
  58. package/src/modules/crdt/crdt-hydration.test.ts +210 -0
  59. package/src/modules/crdt/crdt-reconnect.test.ts +195 -0
  60. package/src/modules/crdt/index.ts +463 -0
  61. package/src/modules/crdt/loro-loader.ts +25 -0
  62. package/src/modules/devtools/flags.ts +349 -0
  63. package/src/modules/devtools/index.ts +529 -47
  64. package/src/modules/devtools/notify-throttle.test.ts +154 -0
  65. package/src/modules/devtools/state-shape.test.ts +149 -0
  66. package/src/modules/devtools/storage-info.test.ts +79 -0
  67. package/src/modules/devtools/storage-info.ts +168 -0
  68. package/src/modules/devtools/versions.test.ts +74 -0
  69. package/src/modules/devtools/versions.ts +110 -0
  70. package/src/modules/feature-flag/index.test.ts +251 -0
  71. package/src/modules/feature-flag/index.ts +316 -0
  72. package/src/modules/ref-tables.test.ts +91 -0
  73. package/src/modules/ref-tables.ts +88 -0
  74. package/src/mutation/jobs.test.ts +25 -0
  75. package/src/mutation/jobs.ts +41 -0
  76. package/src/mutation/mutation-id.test.ts +37 -0
  77. package/src/mutation/mutation-id.ts +35 -0
  78. package/src/mutation/push.saga.test.ts +285 -0
  79. package/src/mutation/push.saga.ts +215 -0
  80. package/src/mutation/rows.test.ts +201 -0
  81. package/src/mutation/rows.ts +377 -0
  82. package/src/mutation/tray.saga.test.ts +63 -0
  83. package/src/mutation/tray.saga.ts +48 -0
  84. package/src/mutation/write.saga.test.ts +163 -0
  85. package/src/mutation/write.saga.ts +166 -0
  86. package/src/otel/index.ts +13 -10
  87. package/src/query/env.test.ts +23 -0
  88. package/src/query/env.ts +56 -0
  89. package/src/query/fetch.saga.test.ts +156 -0
  90. package/src/query/fetch.saga.ts +115 -0
  91. package/src/query/hash.test.ts +14 -0
  92. package/src/query/hash.ts +18 -0
  93. package/src/query/id-set-plan.test.ts +132 -0
  94. package/src/query/lifecycle.saga.test.ts +122 -0
  95. package/src/query/lifecycle.saga.ts +101 -0
  96. package/src/query/materialize.saga.test.ts +82 -0
  97. package/src/query/materialize.saga.ts +75 -0
  98. package/src/query/materialize.test.ts +50 -0
  99. package/src/query/materialize.ts +44 -0
  100. package/src/query/membership.saga.test.ts +322 -0
  101. package/src/query/membership.saga.ts +262 -0
  102. package/src/query/membership.test.ts +118 -0
  103. package/src/query/membership.ts +171 -0
  104. package/src/query/register.saga.test.ts +293 -0
  105. package/src/query/register.saga.ts +226 -0
  106. package/src/query/render-set.test.ts +36 -0
  107. package/src/query/render-set.ts +60 -0
  108. package/src/query/sql.test.ts +62 -0
  109. package/src/query/sql.ts +79 -0
  110. package/src/query/window-query.test.ts +70 -0
  111. package/src/query/window-query.ts +195 -0
  112. package/src/services/blobs/blob-cache.test.ts +359 -0
  113. package/src/services/blobs/blob-cache.ts +603 -0
  114. package/src/services/blobs/blob-manifest.ts +227 -0
  115. package/src/services/blobs/blob-store.test.ts +77 -0
  116. package/src/services/blobs/blob-store.ts +359 -0
  117. package/src/services/blobs/blob.fixture.ts +90 -0
  118. package/src/services/blobs/index.ts +70 -0
  119. package/src/services/database/cache-engine.ts +199 -0
  120. package/src/services/database/connection-supervisor.test.ts +318 -0
  121. package/src/services/database/connection-supervisor.ts +422 -0
  122. package/src/services/database/database.query-timeout.test.ts +140 -0
  123. package/src/services/database/database.responses.test.ts +43 -0
  124. package/src/services/database/database.ts +145 -62
  125. package/src/services/database/engine-factory.ts +33 -0
  126. package/src/services/database/errors.ts +34 -0
  127. package/src/services/database/events/index.ts +2 -1
  128. package/src/services/database/index.ts +7 -0
  129. package/src/services/database/local-migrator.ts +30 -27
  130. package/src/services/database/local.test.ts +64 -0
  131. package/src/services/database/local.ts +491 -67
  132. package/src/services/database/plan-render.test.ts +159 -0
  133. package/src/services/database/plan-render.ts +108 -0
  134. package/src/services/database/relation-resolver.test.ts +467 -0
  135. package/src/services/database/relation-resolver.ts +0 -0
  136. package/src/services/database/remote.ts +222 -15
  137. package/src/services/database/sqlite-cache-engine.test.ts +617 -0
  138. package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
  139. package/src/services/database/sqlite-cache-engine.ts +1366 -0
  140. package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
  141. package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
  142. package/src/services/database/sqlite-lock-verify.test.ts +33 -0
  143. package/src/services/database/sqlite-lock-verify.ts +45 -0
  144. package/src/services/database/sqlite-open.test.ts +150 -0
  145. package/src/services/database/sqlite-open.ts +164 -0
  146. package/src/services/database/sqlite-plan-sql.test.ts +104 -0
  147. package/src/services/database/sqlite-plan-sql.ts +153 -0
  148. package/src/services/database/sqlite-projection.test.ts +121 -0
  149. package/src/services/database/sqlite-select.integration.test.ts +185 -0
  150. package/src/services/database/sqlite-select.test.ts +246 -0
  151. package/src/services/database/sqlite-select.ts +131 -0
  152. package/src/services/database/sqlite-transport.fixture.ts +30 -0
  153. package/src/services/database/sqlite-transport.ts +224 -0
  154. package/src/services/database/sqlite-worker.ts +437 -0
  155. package/src/services/database/surql-translate.ts +416 -0
  156. package/src/services/database/surreal-cache-engine.ts +161 -0
  157. package/src/services/logger/index.ts +3 -2
  158. package/src/services/persistence/localstorage.ts +2 -2
  159. package/src/services/persistence/resilient.ts +11 -4
  160. package/src/services/persistence/surrealdb.ts +10 -10
  161. package/src/services/stream-processor/index.ts +796 -84
  162. package/src/services/stream-processor/permissions.test.ts +47 -0
  163. package/src/services/stream-processor/permissions.ts +53 -0
  164. package/src/services/stream-processor/stream-processor.batch.test.ts +186 -0
  165. package/src/services/stream-processor/stream-processor.prime.test.ts +198 -0
  166. package/src/services/stream-processor/stream-processor.reset.test.ts +226 -0
  167. package/src/services/stream-processor/stream-processor.test.ts +1 -1
  168. package/src/services/stream-processor/wasm-types.ts +59 -3
  169. package/src/services/tabs/broker-client.ts +283 -0
  170. package/src/services/tabs/broker.test.ts +327 -0
  171. package/src/services/tabs/coordinator.test.ts +365 -0
  172. package/src/services/tabs/coordinator.ts +633 -0
  173. package/src/services/tabs/fake-ports.fixture.ts +112 -0
  174. package/src/services/tabs/leader-locks.ts +75 -0
  175. package/src/services/tabs/protocol.ts +262 -0
  176. package/src/services/tabs/support.ts +36 -0
  177. package/src/services/tabs/tabs-broker-worker.ts +640 -0
  178. package/src/sp00ky.ts +5 -0
  179. package/src/state/client-state.ts +191 -0
  180. package/src/state/lifecycle.test.ts +118 -0
  181. package/src/state/lifecycle.ts +86 -0
  182. package/src/state/reducers.test.ts +283 -0
  183. package/src/state/reducers.ts +488 -0
  184. package/src/state/selectors.test.ts +149 -0
  185. package/src/state/selectors.ts +184 -0
  186. package/src/sync/connection.saga.test.ts +86 -0
  187. package/src/sync/connection.saga.ts +85 -0
  188. package/src/sync/live.saga.test.ts +178 -0
  189. package/src/sync/live.saga.ts +107 -0
  190. package/src/sync/policy.health.test.ts +56 -0
  191. package/src/sync/policy.test.ts +659 -0
  192. package/src/sync/policy.ts +502 -0
  193. package/src/sync/poll.saga.test.ts +51 -0
  194. package/src/sync/poll.saga.ts +36 -0
  195. package/src/sync/tabs.saga.test.ts +74 -0
  196. package/src/sync/tabs.saga.ts +93 -0
  197. package/src/testing/adapters.ts +133 -0
  198. package/src/testing/build.ts +63 -0
  199. package/src/testing/fake-services.ts +34 -0
  200. package/src/testing/run-pure.test.ts +92 -0
  201. package/src/testing/run-pure.ts +128 -0
  202. package/src/testing/services.ts +17 -0
  203. package/src/types.ts +603 -14
  204. package/src/utils/blurhash.ts +90 -0
  205. package/src/utils/error-classification.test.ts +54 -0
  206. package/src/utils/error-classification.ts +25 -0
  207. package/src/utils/index.ts +79 -13
  208. package/src/utils/parser.test.ts +49 -120
  209. package/src/utils/parser.ts +32 -2
  210. package/src/utils/semver.test.ts +32 -0
  211. package/src/utils/semver.ts +30 -0
  212. package/src/utils/surql.ts +30 -18
  213. package/src/utils/utils.test.ts +2 -1
  214. package/src/utils/withRetry.test.ts +1 -1
  215. package/tsdown.config.ts +86 -1
  216. package/vitest.config.ts +48 -0
  217. package/src/modules/cache/index.ts +0 -241
  218. package/src/modules/cache/types.ts +0 -19
  219. package/src/modules/data/data.test.ts +0 -58
  220. package/src/modules/data/index.ts +0 -879
  221. package/src/modules/sync/engine.ts +0 -173
  222. package/src/modules/sync/events/index.ts +0 -77
  223. package/src/modules/sync/index.ts +0 -3
  224. package/src/modules/sync/queue/index.ts +0 -2
  225. package/src/modules/sync/queue/queue-down.ts +0 -89
  226. package/src/modules/sync/queue/queue-up.ts +0 -223
  227. package/src/modules/sync/scheduler.ts +0 -84
  228. package/src/modules/sync/sync.ts +0 -421
  229. package/src/modules/sync/utils.test.ts +0 -311
  230. package/src/modules/sync/utils.ts +0 -167
  231. package/src/spooky.ts +0 -395
@@ -1,4 +1,4 @@
1
- import { c as PinoTransmit } from "../types.js";
1
+ import { p 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,331 @@
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
+ function reviveRow(json) {
186
+ if (json.indexOf("\"__u8\"") === -1) return JSON.parse(json);
187
+ return JSON.parse(json, (_k, v) => {
188
+ if (v && typeof v === "object" && typeof v.__u8 === "string") return fromBase64(v.__u8);
189
+ return v;
190
+ });
191
+ }
192
+ /**
193
+ * SELECT-clause expression yielding the row's `data` narrowed to `fields` (plus
194
+ * `id`), so SQLite never returns — and neither side ever parses — the fields the
195
+ * caller did not ask for. On a game list that is every row's `pgn`, which was
196
+ * being read, parsed and thrown away 50 rows at a time for eight rendered fields.
197
+ *
198
+ * Byte-identical to running {@link project} over the fully-parsed row, which is
199
+ * what both paths did before, and verified so against real SQLite:
200
+ * `json_each` walks the keys the row ACTUALLY has, so an ABSENT key stays absent
201
+ * rather than becoming an explicit null — the difference `json_object`/
202
+ * `json_extract` would have introduced. A stored null, nested objects and
203
+ * arrays, and `{__u8}` blob tags all round-trip unchanged, because
204
+ * `json_group_object` understands `json_each`'s `value` column as JSON rather
205
+ * than as text. `COALESCE` covers a row sharing none of the requested keys,
206
+ * where the subquery yields NULL and `reviveRow` would throw.
207
+ *
208
+ * The one value `je.value` does NOT carry back is a TOP-LEVEL boolean. SQLite
209
+ * has no boolean type, so `json_each` hands `true`/`false` out as the integers
210
+ * 1/0 and `json_group_object` writes integers back. A projected row therefore
211
+ * read `published: 1` where the same row read whole read `published: true`,
212
+ * and every `=== true` in an app turned false the moment a screen painted from
213
+ * the local cache. (A boolean NESTED in an object or array survives: those come
214
+ * back as whole JSON documents, untouched.) `je.type` still knows what it was,
215
+ * so the two boolean types are rebuilt as JSON literals on the way out.
216
+ *
217
+ * Deliberately unaliased, so the emitted statement keeps the exact shape the
218
+ * callers already produce (`FROM "t" WHERE id IN (…)`).
219
+ *
220
+ * Binds one parameter per key, pushed onto `bind` — these land in the SELECT
221
+ * clause, so they must be bound BEFORE any WHERE parameters.
222
+ */
223
+ function projectedDataSql(fields, bind) {
224
+ const keys = ["id", ...fields];
225
+ for (const k of keys) bind.push(k);
226
+ 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`;
227
+ }
228
+ function project(row, fields) {
229
+ const out = {};
230
+ for (const f of ["id", ...fields]) if (f in row) out[f] = row[f];
231
+ return out;
232
+ }
233
+ function toBase64(bytes) {
234
+ let bin = "";
235
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
236
+ return typeof btoa !== "undefined" ? btoa(bin) : Buffer.from(bytes).toString("base64");
237
+ }
238
+ function fromBase64(b64) {
239
+ if (typeof atob !== "undefined") {
240
+ const bin = atob(b64);
241
+ const out = new Uint8Array(bin.length);
242
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
243
+ return out;
244
+ }
245
+ return new Uint8Array(Buffer.from(b64, "base64"));
246
+ }
247
+
248
+ //#endregion
249
+ //#region src/services/database/sqlite-open.ts
250
+ const DEFAULT_MAX_ATTEMPTS = 3;
251
+ /** Bounded on purpose: this runs on the boot path, before the first query. */
252
+ const DEFAULT_BACKOFF_MS = [250, 500];
253
+ /**
254
+ * Leader-promotion profile (~5.8s worst case). A dead leader's sync access
255
+ * handles release when the browser garbage-collects its worker, typically
256
+ * well under a second but not synchronously with the Web Lock release the
257
+ * election observed, so promotion retries longer than a cold boot.
258
+ */
259
+ const PROMOTION_OPEN_OPTIONS = {
260
+ maxAttempts: 10,
261
+ backoffMs: [
262
+ 50,
263
+ 100,
264
+ 200,
265
+ 400,
266
+ 800,
267
+ 1e3
268
+ ]
269
+ };
270
+ /** Failures no retry can fix: the APIs aren't there at all (insecure context,
271
+ * or a browser without sync access handles). Fall back immediately. */
272
+ const UNRETRYABLE = ["Missing required OPFS APIs"];
273
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
274
+ /** Keep the DOMException name (e.g. `NoModificationAllowedError` for a pool
275
+ * locked by another tab): it is the most diagnostic part of the failure. */
276
+ function errMessage(e) {
277
+ if (e instanceof Error) return e.name && e.name !== "Error" ? `${e.name}: ${e.message}` : e.message;
278
+ return String(e);
279
+ }
280
+ function fallbackToMemory(sqlite3, dbName, reason, attempts) {
281
+ const tried = attempts > 0 ? ` after ${attempts} attempt${attempts === 1 ? "" : "s"}` : "";
282
+ 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.`);
283
+ return {
284
+ db: new sqlite3.oo1.DB(":memory:", "c"),
285
+ persisted: false,
286
+ opfsError: reason
287
+ };
288
+ }
289
+ /**
290
+ * Open `dbName`'s handle. Never throws for a storage problem: a caller that
291
+ * asked for persistence and can't have it gets a working in-memory handle plus
292
+ * `persisted: false` and an `opfsError` to report.
293
+ */
294
+ async function openDb(sqlite3, dbName, useOpfs, opts = {}) {
295
+ if (!useOpfs) return {
296
+ db: new sqlite3.oo1.DB(":memory:", "c"),
297
+ persisted: false
298
+ };
299
+ if (!sqlite3.installOpfsSAHPoolVfs) {
300
+ if (opts.disallowMemoryFallback) throw new Error("opfs-unavailable: sqlite-wasm build has no installOpfsSAHPoolVfs");
301
+ return fallbackToMemory(sqlite3, dbName, "sqlite-wasm build has no installOpfsSAHPoolVfs", 0);
302
+ }
303
+ const maxAttempts = Math.max(1, opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
304
+ const backoffMs = opts.backoffMs ?? DEFAULT_BACKOFF_MS;
305
+ const sleep = opts.sleep ?? defaultSleep;
306
+ let lastError = "unknown error";
307
+ let attempts = 0;
308
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
309
+ attempts = attempt;
310
+ try {
311
+ const pool = await sqlite3.installOpfsSAHPoolVfs({
312
+ name: `sp00ky-${dbName}`,
313
+ ...attempt > 1 ? { forceReinitIfPreviouslyFailed: true } : {}
314
+ });
315
+ return {
316
+ db: new pool.OpfsSAHPoolDb(`/${dbName}.sqlite3`),
317
+ persisted: true,
318
+ pool
319
+ };
320
+ } catch (e) {
321
+ lastError = errMessage(e);
322
+ if (attempt === maxAttempts || UNRETRYABLE.some((m) => lastError.includes(m))) break;
323
+ await sleep(backoffMs[Math.min(attempt - 1, backoffMs.length - 1)] ?? 0);
324
+ }
325
+ }
326
+ if (opts.disallowMemoryFallback) throw new Error(`opfs-unavailable: ${lastError} (after ${attempts} attempts)`);
327
+ return fallbackToMemory(sqlite3, dbName, lastError, attempts);
328
+ }
329
+
330
+ //#endregion
331
+ 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 {};