@spooky-sync/core 0.0.1-canary.17 → 0.0.1-canary.170

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 (139) hide show
  1. package/AGENTS.md +57 -0
  2. package/dist/index.d.ts +2046 -365
  3. package/dist/index.js +11016 -2476
  4. package/dist/otel/index.d.ts +21 -0
  5. package/dist/otel/index.js +86 -0
  6. package/dist/sqlite-open.js +276 -0
  7. package/dist/sqlite-worker.d.ts +1 -0
  8. package/dist/sqlite-worker.js +420 -0
  9. package/dist/tabs-broker-worker.d.ts +8 -0
  10. package/dist/tabs-broker-worker.js +434 -0
  11. package/dist/types.d.ts +997 -0
  12. package/package.json +14 -7
  13. package/scripts/check-broker-bundle.mjs +33 -0
  14. package/skills/{spooky-core → sp00ky-core}/SKILL.md +94 -18
  15. package/skills/{spooky-core → sp00ky-core}/references/auth.md +1 -1
  16. package/skills/{spooky-core → sp00ky-core}/references/config.md +4 -4
  17. package/src/build-globals.d.ts +12 -0
  18. package/src/events/events.test.ts +2 -1
  19. package/src/events/index.ts +3 -0
  20. package/src/index.ts +24 -2
  21. package/src/modules/app-release/index.test.ts +125 -0
  22. package/src/modules/app-release/index.ts +201 -0
  23. package/src/modules/auth/events/index.ts +2 -1
  24. package/src/modules/auth/index.ts +59 -20
  25. package/src/modules/cache/index.ts +112 -32
  26. package/src/modules/cache/types.ts +2 -2
  27. package/src/modules/crdt/crdt-field.ts +294 -0
  28. package/src/modules/crdt/crdt-hydration.test.ts +210 -0
  29. package/src/modules/crdt/crdt-reconnect.test.ts +195 -0
  30. package/src/modules/crdt/index.ts +463 -0
  31. package/src/modules/crdt/loro-loader.ts +25 -0
  32. package/src/modules/data/data.hydration.test.ts +142 -0
  33. package/src/modules/data/data.membership.test.ts +297 -0
  34. package/src/modules/data/data.rebind.test.ts +147 -0
  35. package/src/modules/data/data.run.test.ts +113 -0
  36. package/src/modules/data/data.status.test.ts +249 -0
  37. package/src/modules/data/id-set-plan.test.ts +122 -0
  38. package/src/modules/data/index.ts +1403 -129
  39. package/src/modules/data/mutation-id.test.ts +25 -0
  40. package/src/modules/data/mutation-id.ts +35 -0
  41. package/src/modules/data/window-query.test.ts +52 -0
  42. package/src/modules/data/window-query.ts +194 -0
  43. package/src/modules/devtools/flags.ts +349 -0
  44. package/src/modules/devtools/index.ts +382 -37
  45. package/src/modules/devtools/notify-throttle.test.ts +149 -0
  46. package/src/modules/devtools/storage-info.test.ts +79 -0
  47. package/src/modules/devtools/storage-info.ts +168 -0
  48. package/src/modules/devtools/versions.test.ts +74 -0
  49. package/src/modules/devtools/versions.ts +81 -0
  50. package/src/modules/feature-flag/index.test.ts +251 -0
  51. package/src/modules/feature-flag/index.ts +308 -0
  52. package/src/modules/ref-tables.test.ts +91 -0
  53. package/src/modules/ref-tables.ts +88 -0
  54. package/src/modules/sync/engine.ts +101 -37
  55. package/src/modules/sync/events/index.ts +9 -2
  56. package/src/modules/sync/queue/queue-down.ts +12 -5
  57. package/src/modules/sync/queue/queue-up.forwarded.test.ts +164 -0
  58. package/src/modules/sync/queue/queue-up.ts +216 -56
  59. package/src/modules/sync/scheduler.pause.test.ts +109 -0
  60. package/src/modules/sync/scheduler.ts +77 -9
  61. package/src/modules/sync/sync.health.test.ts +149 -0
  62. package/src/modules/sync/sync.live-removal.test.ts +134 -0
  63. package/src/modules/sync/sync.reconnect.test.ts +145 -0
  64. package/src/modules/sync/sync.subquery.test.ts +82 -0
  65. package/src/modules/sync/sync.ts +1456 -92
  66. package/src/modules/sync/utils.test.ts +269 -2
  67. package/src/modules/sync/utils.ts +182 -17
  68. package/src/otel/index.ts +127 -0
  69. package/src/services/blobs/blob-cache.test.ts +359 -0
  70. package/src/services/blobs/blob-cache.ts +603 -0
  71. package/src/services/blobs/blob-manifest.ts +227 -0
  72. package/src/services/blobs/blob-store.test.ts +77 -0
  73. package/src/services/blobs/blob-store.ts +359 -0
  74. package/src/services/blobs/blob.fixture.ts +90 -0
  75. package/src/services/blobs/index.ts +70 -0
  76. package/src/services/database/cache-engine.ts +160 -0
  77. package/src/services/database/connection-supervisor.test.ts +289 -0
  78. package/src/services/database/connection-supervisor.ts +415 -0
  79. package/src/services/database/database.query-timeout.test.ts +83 -0
  80. package/src/services/database/database.ts +32 -12
  81. package/src/services/database/engine-factory.ts +33 -0
  82. package/src/services/database/events/index.ts +2 -1
  83. package/src/services/database/index.ts +7 -0
  84. package/src/services/database/local-migrator.ts +30 -27
  85. package/src/services/database/local.test.ts +64 -0
  86. package/src/services/database/local.ts +478 -67
  87. package/src/services/database/plan-render.test.ts +159 -0
  88. package/src/services/database/plan-render.ts +108 -0
  89. package/src/services/database/relation-resolver.test.ts +413 -0
  90. package/src/services/database/relation-resolver.ts +0 -0
  91. package/src/services/database/remote.ts +110 -14
  92. package/src/services/database/sqlite-cache-engine.test.ts +464 -0
  93. package/src/services/database/sqlite-cache-engine.ts +1159 -0
  94. package/src/services/database/sqlite-open.test.ts +150 -0
  95. package/src/services/database/sqlite-open.ts +164 -0
  96. package/src/services/database/sqlite-plan-sql.test.ts +104 -0
  97. package/src/services/database/sqlite-plan-sql.ts +106 -0
  98. package/src/services/database/sqlite-select.integration.test.ts +185 -0
  99. package/src/services/database/sqlite-select.test.ts +246 -0
  100. package/src/services/database/sqlite-select.ts +113 -0
  101. package/src/services/database/sqlite-transport.fixture.ts +30 -0
  102. package/src/services/database/sqlite-transport.ts +219 -0
  103. package/src/services/database/sqlite-worker.ts +437 -0
  104. package/src/services/database/surql-translate.ts +291 -0
  105. package/src/services/database/surreal-cache-engine.ts +141 -0
  106. package/src/services/logger/index.ts +6 -109
  107. package/src/services/persistence/localstorage.ts +2 -2
  108. package/src/services/persistence/resilient.ts +11 -4
  109. package/src/services/persistence/surrealdb.ts +10 -10
  110. package/src/services/stream-processor/index.ts +444 -52
  111. package/src/services/stream-processor/permissions.test.ts +47 -0
  112. package/src/services/stream-processor/permissions.ts +53 -0
  113. package/src/services/stream-processor/stream-processor.batch.test.ts +136 -0
  114. package/src/services/stream-processor/stream-processor.reset.test.ts +216 -0
  115. package/src/services/stream-processor/stream-processor.test.ts +1 -1
  116. package/src/services/stream-processor/wasm-types.ts +23 -2
  117. package/src/services/tabs/broker-client.ts +283 -0
  118. package/src/services/tabs/broker.test.ts +278 -0
  119. package/src/services/tabs/coordinator.test.ts +244 -0
  120. package/src/services/tabs/coordinator.ts +576 -0
  121. package/src/services/tabs/fake-ports.fixture.ts +112 -0
  122. package/src/services/tabs/leader-locks.ts +75 -0
  123. package/src/services/tabs/protocol.ts +242 -0
  124. package/src/services/tabs/support.ts +36 -0
  125. package/src/services/tabs/tabs-broker-worker.ts +586 -0
  126. package/src/sp00ky.auth-order.test.ts +92 -0
  127. package/src/sp00ky.init-query.test.ts +183 -0
  128. package/src/sp00ky.ts +1424 -0
  129. package/src/types.ts +467 -15
  130. package/src/utils/error-classification.test.ts +44 -0
  131. package/src/utils/error-classification.ts +7 -0
  132. package/src/utils/index.ts +73 -13
  133. package/src/utils/parser.ts +3 -2
  134. package/src/utils/semver.test.ts +32 -0
  135. package/src/utils/semver.ts +30 -0
  136. package/src/utils/surql.ts +30 -18
  137. package/src/utils/withRetry.test.ts +1 -1
  138. package/tsdown.config.ts +86 -1
  139. package/src/spooky.ts +0 -395
@@ -1,7 +1,7 @@
1
- import { GetTable, SchemaStructure, TableModel, TableNames } from '@spooky-sync/query-builder';
1
+ import type { GetTable, SchemaStructure, TableModel, TableNames } from '@spooky-sync/query-builder';
2
2
  import { Uuid, RecordId, Duration } from 'surrealdb';
3
- import { Logger } from '../services/logger/index';
4
- import { QueryTimeToLive } from '../types';
3
+ import type { Logger } from '../services/logger/index';
4
+ import type { QueryTimeToLive } from '../types';
5
5
 
6
6
  export * from './surql';
7
7
  export * from './parser';
@@ -59,7 +59,7 @@ export function generateNewTableId<S extends SchemaStructure, T extends TableNam
59
59
 
60
60
  // ==================== SCHEMA ENCODING/DECODING ====================
61
61
 
62
- export function decodeFromSpooky<S extends SchemaStructure, T extends TableNames<S>>(
62
+ export function decodeFromSp00ky<S extends SchemaStructure, T extends TableNames<S>>(
63
63
  schema: S,
64
64
  tableName: T,
65
65
  record: TableModel<GetTable<S, T>>
@@ -74,19 +74,19 @@ export function decodeFromSpooky<S extends SchemaStructure, T extends TableNames
74
74
  for (const field of Object.keys(table.columns)) {
75
75
  const column = table.columns[field] as any;
76
76
  const relation = schema.relationships.find((r) => r.from === tableName && r.field === field);
77
- if ((column.recordId || relation) && encoded[field] != null) {
77
+ if ((column.recordId || relation) && encoded[field] !== null && encoded[field] !== undefined) {
78
78
  if (encoded[field] instanceof RecordId) {
79
79
  encoded[field] = `${encoded[field].table.toString()}:${encoded[field].id}`;
80
80
  } else if (
81
81
  relation &&
82
- (encoded[field] instanceof Object || encoded[field] instanceof Array)
82
+ (encoded[field] instanceof Object || Array.isArray(encoded[field]))
83
83
  ) {
84
84
  if (Array.isArray(encoded[field])) {
85
85
  encoded[field] = encoded[field].map((item) =>
86
- decodeFromSpooky(schema, relation.to, item)
86
+ decodeFromSp00ky(schema, relation.to, item)
87
87
  );
88
88
  } else {
89
- encoded[field] = decodeFromSpooky(schema, relation.to, encoded[field]);
89
+ encoded[field] = decodeFromSp00ky(schema, relation.to, encoded[field]);
90
90
  }
91
91
  }
92
92
  }
@@ -97,15 +97,25 @@ export function decodeFromSpooky<S extends SchemaStructure, T extends TableNames
97
97
 
98
98
  // ==================== TIME/DURATION UTILITIES ====================
99
99
 
100
+ /**
101
+ * Read the millisecond count off a surrealdb `Duration`. Different `surrealdb`
102
+ * releases expose it under `milliseconds` (current) or the older private
103
+ * `_milliseconds` field, so read whichever is set; returns 0 when neither is.
104
+ */
105
+ function durationMillis(duration: Duration): number {
106
+ const d = duration as { milliseconds?: number | bigint; _milliseconds?: number | bigint };
107
+ return Number(d.milliseconds || d._milliseconds || 0);
108
+ }
109
+
100
110
  /**
101
111
  * Parse duration string or Duration object to milliseconds
102
112
  */
103
113
  export function parseDuration(duration: QueryTimeToLive | Duration): number {
104
114
  if (duration instanceof Duration) {
105
- const ms = (duration as any).milliseconds || (duration as any)._milliseconds;
106
- if (ms) return Number(ms);
115
+ const ms = durationMillis(duration);
116
+ if (ms) return ms;
107
117
  const str = duration.toString();
108
- if (str !== '[object Object]') return parseDuration(str as any);
118
+ if (str !== '[object Object]') return parseDuration(str as QueryTimeToLive);
109
119
  return 600000;
110
120
  }
111
121
 
@@ -117,7 +127,7 @@ export function parseDuration(duration: QueryTimeToLive | Duration): number {
117
127
 
118
128
  const match = duration.match(/^(\d+)([smh])$/);
119
129
  if (!match) return 600000;
120
- const val = parseInt(match[1], 10);
130
+ const val = Number.parseInt(match[1], 10);
121
131
  const unit = match[2];
122
132
  switch (unit) {
123
133
  case 's':
@@ -137,6 +147,18 @@ export async function fileToUint8Array(file: File | Blob): Promise<Uint8Array> {
137
147
  return new Uint8Array(buffer);
138
148
  }
139
149
 
150
+ // ==================== TEXT UTILITIES ====================
151
+
152
+ /**
153
+ * Convert plain text to simple HTML paragraphs.
154
+ * Useful for seeding a rich-text editor (e.g. TipTap/ProseMirror) with fallback content.
155
+ */
156
+ export function textToHtml(text: string): string {
157
+ return text
158
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
159
+ .split('\n').map((l) => `<p>${l || '<br>'}</p>`).join('');
160
+ }
161
+
140
162
  // ==================== DATABASE UTILITIES ====================
141
163
 
142
164
  /**
@@ -165,7 +187,7 @@ export async function withRetry<T>(
165
187
  attempt: i + 1,
166
188
  retries,
167
189
  error: msg,
168
- Category: 'spooky-client::utils::withRetry',
190
+ Category: 'sp00ky-client::utils::withRetry',
169
191
  },
170
192
  'Retrying DB operation'
171
193
  );
@@ -177,3 +199,41 @@ export async function withRetry<T>(
177
199
  }
178
200
  throw lastError;
179
201
  }
202
+
203
+ /**
204
+ * Reject after `timeoutMs` if `promise` hasn't settled.
205
+ *
206
+ * Exists because a WebSocket RPC has no deadline of its own: the SurrealDB SDK
207
+ * parks each call in a pending map and only rejects it when the socket reports
208
+ * a close. On a half-open socket (peer gone, no `close` event, `readyState`
209
+ * still OPEN) the call never settles at all, which wedges anything serialized
210
+ * behind it.
211
+ *
212
+ * `message` MUST contain "timed out" so `classifySyncError` treats the
213
+ * rejection as `network` — that's what makes the sync queues retry the
214
+ * operation instead of rolling the mutation back as an application error.
215
+ *
216
+ * Non-positive `timeoutMs` returns `promise` unchanged. The underlying promise
217
+ * is NOT cancelled (nothing can cancel an in-flight RPC); its eventual
218
+ * settlement is absorbed, so attach no expectations to it after a timeout.
219
+ */
220
+ export function withTimeout<T>(
221
+ promise: Promise<T>,
222
+ timeoutMs: number,
223
+ message: string
224
+ ): Promise<T> {
225
+ if (!(timeoutMs > 0)) return promise;
226
+ return new Promise<T>((resolve, reject) => {
227
+ const timer = setTimeout(() => reject(new Error(message)), timeoutMs);
228
+ promise.then(
229
+ (value) => {
230
+ clearTimeout(timer);
231
+ resolve(value);
232
+ },
233
+ (err) => {
234
+ clearTimeout(timer);
235
+ reject(err);
236
+ }
237
+ );
238
+ });
239
+ }
@@ -1,4 +1,5 @@
1
- import { ColumnSchema, RecordId } from '@spooky-sync/query-builder';
1
+ import type { ColumnSchema} from '@spooky-sync/query-builder';
2
+ import { RecordId } from '@spooky-sync/query-builder';
2
3
  import { parseRecordIdString } from './index';
3
4
  import { DateTime } from 'surrealdb';
4
5
 
@@ -8,7 +9,7 @@ export function cleanRecord(
8
9
  ): Record<string, any> {
9
10
  const cleaned: Record<string, any> = {};
10
11
  for (const [key, value] of Object.entries(record)) {
11
- if (key === 'id' || key in tableSchema) {
12
+ if (key === 'id' || key.startsWith('_00_') || key in tableSchema) {
12
13
  cleaned[key] = value;
13
14
  }
14
15
  }
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { semverGt } from './semver';
3
+
4
+ describe('semverGt', () => {
5
+ it('compares strictly greater', () => {
6
+ expect(semverGt('1.0.1', '1.0.0')).toBe(true);
7
+ expect(semverGt('1.1.0', '1.0.9')).toBe(true);
8
+ expect(semverGt('2.0.0', '1.9.9')).toBe(true);
9
+ expect(semverGt('1.10.0', '1.9.0')).toBe(true); // numeric, not lexicographic
10
+ });
11
+
12
+ it('equal and lower are not greater', () => {
13
+ expect(semverGt('1.0.0', '1.0.0')).toBe(false);
14
+ expect(semverGt('1.0.0', '1.0.1')).toBe(false);
15
+ expect(semverGt('0.9.9', '1.0.0')).toBe(false);
16
+ });
17
+
18
+ it('missing parts default to zero', () => {
19
+ expect(semverGt('1.2', '1.2.0')).toBe(false);
20
+ expect(semverGt('1.2.1', '1.2')).toBe(true);
21
+ expect(semverGt('2', '1.9.9')).toBe(true);
22
+ });
23
+
24
+ it('junk never compares greater', () => {
25
+ expect(semverGt('abc', '1.0.0')).toBe(false);
26
+ expect(semverGt('1.0.0', 'abc')).toBe(false);
27
+ expect(semverGt('', '0.0.0')).toBe(false);
28
+ expect(semverGt(null, undefined)).toBe(false);
29
+ expect(semverGt('1.0.0-beta', '0.9.0')).toBe(false); // prerelease unsupported
30
+ expect(semverGt('1.2.3.4', '1.0.0')).toBe(false);
31
+ });
32
+ });
@@ -0,0 +1,30 @@
1
+ // Minimal semver comparison for app-release version checks. "X.Y.Z" numeric
2
+ // with missing parts read as 0 ("1.2" == "1.2.0"); any malformed input never
3
+ // compares greater, so a bad release row can never nag (or force-reload)
4
+ // every client.
5
+
6
+ function parse(v: unknown): [number, number, number] | null {
7
+ const parts = String(v ?? '')
8
+ .trim()
9
+ .split('.');
10
+ if (parts.length === 0 || parts.length > 3 || parts[0] === '') return null;
11
+ const nums: number[] = [];
12
+ for (let i = 0; i < 3; i++) {
13
+ const raw = parts[i] ?? '0';
14
+ if (!/^\d+$/.test(raw)) return null;
15
+ nums.push(parseInt(raw, 10));
16
+ }
17
+ return nums as [number, number, number];
18
+ }
19
+
20
+ /** True when `a` is a valid version strictly greater than valid version `b`. */
21
+ export function semverGt(a: unknown, b: unknown): boolean {
22
+ const pa = parse(a);
23
+ const pb = parse(b);
24
+ if (!pa || !pb) return false;
25
+ for (let i = 0; i < 3; i++) {
26
+ if (pa[i] > pb[i]) return true;
27
+ if (pa[i] < pb[i]) return false;
28
+ }
29
+ return false;
30
+ }
@@ -1,4 +1,4 @@
1
- import { MutationEventType } from '../types';
1
+ import type { MutationEventType } from '../types';
2
2
 
3
3
  // ==================== TYPES ====================
4
4
 
@@ -34,6 +34,7 @@ export interface SurqlHelper {
34
34
  keyDataVars: ({ key: string; variable: string } | { statement: string } | string)[]
35
35
  ): string;
36
36
  upsert(idVar: string, dataVar: string): string;
37
+ upsertMerge(idVar: string, dataVar: string): string;
37
38
  updateMerge(idVar: string, dataVar: string): string;
38
39
  updateSet(
39
40
  idVar: string,
@@ -90,17 +91,9 @@ export const surql: SurqlHelper = {
90
91
  returnValues: ({ field: string; alias: string } | string)[]
91
92
  ) {
92
93
  return `SELECT ${returnValues
93
- .map((returnValues) =>
94
- typeof returnValues === 'string'
95
- ? returnValues
96
- : `${returnValues.field} as ${returnValues.alias}`
97
- )
94
+ .map((rv) => (typeof rv === 'string' ? rv : `${rv.field} as ${rv.alias}`))
98
95
  .join(',')} FROM ${table} WHERE ${whereVar
99
- .map((whereVar) =>
100
- typeof whereVar === 'string'
101
- ? `${whereVar} = $${whereVar}`
102
- : `${whereVar.field} = $${whereVar.variable}`
103
- )
96
+ .map((wv) => (typeof wv === 'string' ? `${wv} = $${wv}` : `${wv.field} = $${wv.variable}`))
104
97
  .join(' AND ')}`;
105
98
  },
106
99
 
@@ -127,6 +120,14 @@ export const surql: SurqlHelper = {
127
120
  return `UPSERT ONLY $${idVar} REPLACE $${dataVar}`;
128
121
  },
129
122
 
123
+ // Use this for sync-down ingestion: the remote payload omits local-only
124
+ // fields injected by the CLI's local-schema setup (`_00_crdt`,
125
+ // `_00_cursor`), and REPLACE would wipe them on every round-trip and
126
+ // break offline reload of CRDT state.
127
+ upsertMerge(idVar: string, dataVar: string) {
128
+ return `UPSERT ONLY $${idVar} MERGE $${dataVar}`;
129
+ },
130
+
130
131
  updateMerge(idVar: string, dataVar: string) {
131
132
  return `UPDATE ONLY $${idVar} MERGE $${dataVar}`;
132
133
  },
@@ -136,12 +137,12 @@ export const surql: SurqlHelper = {
136
137
  keyDataVar: ({ key: string; variable: string } | { statement: string } | string)[]
137
138
  ) {
138
139
  return `UPDATE $${idVar} SET ${keyDataVar
139
- .map((keyDataVar) =>
140
- typeof keyDataVar === 'string'
141
- ? `${keyDataVar} = $${keyDataVar}`
142
- : 'statement' in keyDataVar
143
- ? keyDataVar.statement
144
- : `${keyDataVar.key} = $${keyDataVar.variable}`
140
+ .map((kdv) =>
141
+ typeof kdv === 'string'
142
+ ? `${kdv} = $${kdv}`
143
+ : 'statement' in kdv
144
+ ? kdv.statement
145
+ : `${kdv.key} = $${kdv.variable}`
145
146
  )
146
147
  .join(', ')}`;
147
148
  },
@@ -163,7 +164,18 @@ export const surql: SurqlHelper = {
163
164
  ) {
164
165
  switch (t) {
165
166
  case 'create':
166
- return `CREATE ONLY $${mutationIdVar} SET mutationType = 'create', recordId = $${recordIdVar}`;
167
+ // `data` is REQUIRED here, not decorative. The outbox row is the only
168
+ // copy of a pending create once the in-memory UpEvent is gone (reload,
169
+ // or a shared-tabs follower whose row is replayed by the leader). This
170
+ // used to drop `dataVar` on the floor, so every replayed create arrived
171
+ // with `data: undefined` and `processUpEvent` threw on
172
+ // `Object.keys(event.data)` before it ever reached the network: the
173
+ // create was unsendable AND it sat at the head of the queue blocking
174
+ // every later mutation. The payload was simply never persisted, so
175
+ // those creates were unrecoverable.
176
+ return dataVar
177
+ ? `CREATE ONLY $${mutationIdVar} SET mutationType = 'create', recordId = $${recordIdVar}, data = $${dataVar}`
178
+ : `CREATE ONLY $${mutationIdVar} SET mutationType = 'create', recordId = $${recordIdVar}`;
167
179
  case 'update': {
168
180
  let stmt = `CREATE ONLY $${mutationIdVar} SET mutationType = 'update', recordId = $${recordIdVar}, data = $${dataVar}`;
169
181
  if (beforeRecordVar) {
@@ -145,7 +145,7 @@ describe('withRetry', () => {
145
145
  attempt: 1,
146
146
  retries: 3,
147
147
  error: 'Can not open transaction',
148
- Category: 'spooky-client::utils::withRetry',
148
+ Category: 'sp00ky-client::utils::withRetry',
149
149
  }),
150
150
  'Retrying DB operation'
151
151
  );
package/tsdown.config.ts CHANGED
@@ -1,9 +1,94 @@
1
1
  import { defineConfig } from 'tsdown';
2
+ import { createRequire } from 'node:module';
3
+
4
+ const require = createRequire(import.meta.url);
5
+
6
+ // Resolve real package versions at build time so the DevTools can report the
7
+ // actual bundled frontend versions (used to detect frontend/backend drift).
8
+ const corePkg = require('./package.json');
9
+ const coreVersion: string = corePkg.version;
10
+ // ssp-wasm has no `exports` map; resolve its package.json directly, falling
11
+ // back to the relative workspace path if module resolution can't find it.
12
+ let wasmVersion: string;
13
+ try {
14
+ wasmVersion = require('@spooky-sync/ssp-wasm/package.json').version;
15
+ } catch {
16
+ wasmVersion = require('../ssp-wasm/package.json').version;
17
+ }
18
+ // Report the SurrealDB WASM ENGINE version (`@surrealdb/wasm`) — the engine the
19
+ // in-browser local DB actually runs — NOT the `surrealdb` JS client library
20
+ // version. The WASM engine is the one that must stay compatible with the server
21
+ // engine, so it's the meaningful number to compare in DevTools. Prefer the
22
+ // actually-installed version; fall back to our pinned range if its `exports`
23
+ // map blocks importing its package.json.
24
+ let surrealVersion: string;
25
+ try {
26
+ surrealVersion = require('@surrealdb/wasm/package.json').version;
27
+ } catch {
28
+ surrealVersion = String(corePkg.dependencies?.['@surrealdb/wasm'] ?? 'unknown').replace(
29
+ /^[\^~]/,
30
+ ''
31
+ );
32
+ }
33
+
34
+ // tsdown 0.12.x forwards a top-level `define` to rolldown's inputOptions, which
35
+ // rejects it — so we do the substitution ourselves with a tiny transform
36
+ // plugin. Only our own modules reference the `__SP00KY_*__` identifiers, and
37
+ // the early `includes` check skips everything else.
38
+ const replacements: Record<string, string> = {
39
+ __SP00KY_CORE_VERSION__: JSON.stringify(coreVersion),
40
+ __SP00KY_WASM_VERSION__: JSON.stringify(wasmVersion),
41
+ __SP00KY_SURREAL_VERSION__: JSON.stringify(surrealVersion),
42
+ };
43
+
44
+ const versionDefinePlugin = {
45
+ name: 'sp00ky-version-define',
46
+ transform(code: string) {
47
+ if (!code.includes('__SP00KY_')) return null;
48
+ let out = code;
49
+ for (const [token, value] of Object.entries(replacements)) {
50
+ out = out.split(token).join(value);
51
+ }
52
+ return out;
53
+ },
54
+ };
55
+
56
+ // Worker URLs are written as `./<name>.ts` in source so Vite's src-bundling
57
+ // consumers (the example app aliases core to `src`) resolve them. In the
58
+ // published build each worker is emitted at `dist/<name>.js`, so rewrite the
59
+ // references to `.js` here — otherwise the flat `dist/index.js` carries
60
+ // dangling `.ts` URLs that a consumer's bundler can't resolve.
61
+ const workerUrlPlugin = {
62
+ name: 'sp00ky-worker-url',
63
+ transform(code: string) {
64
+ if (!code.includes('sqlite-worker.ts') && !code.includes('tabs-broker-worker.ts')) return null;
65
+ return code
66
+ .split('sqlite-worker.ts')
67
+ .join('sqlite-worker.js')
68
+ .split('tabs-broker-worker.ts')
69
+ .join('tabs-broker-worker.js');
70
+ },
71
+ };
2
72
 
3
73
  export default defineConfig({
4
- entry: ['src/index.ts'],
74
+ // `sqlite-worker` is emitted at `dist/sqlite-worker.js` (top level, NOT under
75
+ // services/database) so the `new URL('./sqlite-worker.js', import.meta.url)`
76
+ // in `SqliteCacheEngine` — which gets bundled into the flat `dist/index.js` —
77
+ // resolves correctly. It (and `@sqlite.org/sqlite-wasm`) load lazily in the
78
+ // Worker; never pulled into the main bundle unless `localEngine: 'sqlite'`.
79
+ entry: {
80
+ index: 'src/index.ts',
81
+ 'otel/index': 'src/otel/index.ts',
82
+ 'sqlite-worker': 'src/services/database/sqlite-worker.ts',
83
+ // The shared-tabs broker MUST stay a self-contained emit (no chunk
84
+ // imports): bundlers cannot trace `new SharedWorker(url)` graphs, so a
85
+ // relative chunk import would 404 in consumers. The source keeps zero
86
+ // runtime imports and scripts/check-broker-bundle.mjs enforces it.
87
+ 'tabs-broker-worker': 'src/services/tabs/tabs-broker-worker.ts',
88
+ },
5
89
  format: ['esm'],
6
90
  dts: true,
7
91
  clean: true,
8
92
  hash: false,
93
+ plugins: [versionDefinePlugin, workerUrlPlugin],
9
94
  });