@spooky-sync/core 0.0.1-canary.29 → 0.0.1-canary.291

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 +14941 -2433
  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 +750 -96
  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
package/src/sp00ky.ts CHANGED
@@ -1,395 +1,5 @@
1
- import { DataModule } from './modules/data/index';
2
- import {
3
- Sp00kyConfig,
4
- QueryTimeToLive,
5
- Sp00kyQueryResultPromise,
6
- PersistenceClient,
7
- MutationEvent,
8
- UpdateOptions,
9
- RunOptions,
10
- } from './types';
11
- import {
12
- LocalDatabaseService,
13
- LocalMigrator,
14
- RemoteDatabaseService,
15
- } from './services/database/index';
16
- import { Surreal } from 'surrealdb';
17
- import { Sp00kySync, UpEvent } from './modules/sync/index';
18
- import {
19
- GetTable,
20
- InnerQuery,
21
- QueryBuilder,
22
- QueryOptions,
23
- SchemaStructure,
24
- TableModel,
25
- TableNames,
26
- BucketNames,
27
- BackendNames,
28
- BackendRoutes,
29
- RoutePayload,
30
- } from '@spooky-sync/query-builder';
31
-
32
- import { DevToolsService } from './modules/devtools/index';
33
- import { createLogger } from './services/logger/index';
34
- import { AuthService } from './modules/auth/index';
35
- import { StreamProcessorService } from './services/stream-processor/index';
36
- import { EventSystem } from './events/index';
37
- import { CacheModule } from './modules/cache/index';
38
- import { LocalStoragePersistenceClient } from './services/persistence/localstorage';
39
- import { generateId, parseParams } from './utils/index';
40
- import { SurrealDBPersistenceClient } from './services/persistence/surrealdb';
41
- import { ResilientPersistenceClient } from './services/persistence/resilient';
42
-
43
- export class BucketHandle {
44
- constructor(private bucketName: string, private remote: RemoteDatabaseService) {}
45
-
46
- async put(path: string, content: string | Uint8Array | Blob): Promise<void> {
47
- await this.remote.query(`RETURN f"${this.bucketName}:/${path}".put($content);`, { content });
48
- }
49
-
50
- async get(path: string): Promise<unknown> {
51
- const [result] = await this.remote.query<[unknown]>(`RETURN f"${this.bucketName}:/${path}".get();`);
52
- return result;
53
- }
54
-
55
- async delete(path: string): Promise<void> {
56
- await this.remote.query(`RETURN f"${this.bucketName}:/${path}".delete();`);
57
- }
58
-
59
- async exists(path: string): Promise<boolean> {
60
- const [result] = await this.remote.query<[boolean]>(`RETURN f"${this.bucketName}:/${path}".exists();`);
61
- return result;
62
- }
63
-
64
- async head(path: string): Promise<Record<string, unknown>> {
65
- const [result] = await this.remote.query<[Record<string, unknown>]>(`RETURN f"${this.bucketName}:/${path}".head();`);
66
- return result;
67
- }
68
-
69
- async copy(sourcePath: string, targetPath: string): Promise<void> {
70
- await this.remote.query(`RETURN f"${this.bucketName}:/${sourcePath}".copy($target);`, { target: targetPath });
71
- }
72
-
73
- async rename(sourcePath: string, targetPath: string): Promise<void> {
74
- await this.remote.query(`RETURN f"${this.bucketName}:/${sourcePath}".rename($target);`, { target: targetPath });
75
- }
76
-
77
- async list(prefix?: string): Promise<string[]> {
78
- const p = prefix ?? '';
79
- const [result] = await this.remote.query<[string[]]>(`RETURN f"${this.bucketName}:/${p}".list();`);
80
- return result;
81
- }
82
- }
83
-
84
- export class Sp00kyClient<S extends SchemaStructure> {
85
- private local: LocalDatabaseService;
86
- private remote: RemoteDatabaseService;
87
- private persistenceClient: PersistenceClient;
88
-
89
- private migrator: LocalMigrator;
90
- private cache: CacheModule;
91
- private dataModule: DataModule<S>;
92
- private sync: Sp00kySync<S>;
93
- private devTools: DevToolsService;
94
-
95
- private logger: ReturnType<typeof createLogger>;
96
- public auth: AuthService<S>;
97
- public streamProcessor: StreamProcessorService;
98
-
99
- get remoteClient() {
100
- return this.remote.getClient();
101
- }
102
-
103
- get localClient() {
104
- return this.local.getClient();
105
- }
106
-
107
- get pendingMutationCount(): number {
108
- return this.sync.pendingMutationCount;
109
- }
110
-
111
- subscribeToPendingMutations(cb: (count: number) => void): () => void {
112
- return this.sync.subscribeToPendingMutations(cb);
113
- }
114
-
115
- constructor(private config: Sp00kyConfig<S>) {
116
- const logger = createLogger(config.logLevel ?? 'info', config.otelTransmit);
117
- this.logger = logger.child({ service: 'Sp00kyClient' });
118
-
119
- this.logger.info(
120
- {
121
- config: { ...config, schema: '[SchemaStructure]' },
122
- Category: 'sp00ky-client::Sp00kyClient::constructor',
123
- },
124
- 'Sp00kyClient initialized'
125
- );
126
-
127
- this.local = new LocalDatabaseService(this.config.database, logger);
128
- this.remote = new RemoteDatabaseService(this.config.database, logger);
129
-
130
- if (config.persistenceClient === 'surrealdb') {
131
- this.persistenceClient = new SurrealDBPersistenceClient(this.local, logger);
132
- } else if (config.persistenceClient === 'localstorage' || !config.persistenceClient) {
133
- this.persistenceClient = new LocalStoragePersistenceClient(logger);
134
- } else {
135
- this.persistenceClient = config.persistenceClient;
136
- }
137
-
138
- this.persistenceClient = new ResilientPersistenceClient(this.persistenceClient, logger);
139
-
140
- this.streamProcessor = new StreamProcessorService(
141
- new EventSystem(['stream_update']),
142
- this.local,
143
- this.persistenceClient,
144
- logger
145
- );
146
- this.migrator = new LocalMigrator(this.local, logger);
147
-
148
- this.cache = new CacheModule(
149
- this.local,
150
- this.streamProcessor,
151
- (update) => {
152
- // Direct callback from cache to data module
153
- this.dataModule.onStreamUpdate(update);
154
- },
155
- logger
156
- );
157
-
158
- this.dataModule = new DataModule(
159
- this.cache,
160
- this.local,
161
- this.config.schema,
162
- logger,
163
- this.config.streamDebounceTime
164
- );
165
-
166
- // Initialize Auth
167
- this.auth = new AuthService(this.config.schema, this.remote, this.persistenceClient, logger);
168
-
169
- // Initialize Sync
170
- this.sync = new Sp00kySync(this.local, this.remote, this.cache, this.dataModule, this.config.schema, this.logger);
171
-
172
- // Initialize DevTools
173
- this.devTools = new DevToolsService(
174
- this.local,
175
- this.remote,
176
- logger,
177
- this.config.schema,
178
- this.auth,
179
- this.dataModule
180
- );
181
-
182
- // Register DevTools as a receiver for stream updates
183
- this.streamProcessor.addReceiver(this.devTools);
184
-
185
- // Wire up callbacks instead of events
186
- this.setupCallbacks();
187
- }
188
-
189
- /**
190
- * Setup direct callbacks instead of event subscriptions
191
- */
192
- private setupCallbacks() {
193
- // Mutation callback for sync
194
- this.dataModule.onMutation((mutations: UpEvent[]) => {
195
- // Notify DevTools
196
- this.devTools.onMutation(mutations);
197
-
198
- // Enqueue in Sync
199
- if (mutations.length > 0) {
200
- this.sync.enqueueMutation(mutations);
201
- }
202
- });
203
-
204
- // Sync events for incoming updates
205
- this.sync.events.subscribe('SYNC_QUERY_UPDATED', (event: any) => {
206
- this.devTools.logEvent('SYNC_QUERY_UPDATED', event.payload);
207
- });
208
-
209
- // Database events for DevTools
210
- this.local.getEvents().subscribe('DATABASE_LOCAL_QUERY', (event: any) => {
211
- this.devTools.logEvent('LOCAL_QUERY', event.payload);
212
- });
213
-
214
- this.remote.getEvents().subscribe('DATABASE_REMOTE_QUERY', (event: any) => {
215
- this.devTools.logEvent('REMOTE_QUERY', event.payload);
216
- });
217
- }
218
-
219
- async init() {
220
- this.logger.info(
221
- { Category: 'sp00ky-client::Sp00kyClient::init' },
222
- 'Sp00kyClient initialization started'
223
- );
224
- try {
225
- const clientId = this.config.clientId ?? (await this.loadOrGenerateClientId());
226
- this.persistClientId(clientId);
227
- this.logger.debug(
228
- { clientId, Category: 'sp00ky-client::Sp00kyClient::init' },
229
- 'Client ID loaded'
230
- );
231
-
232
- await this.local.connect();
233
- this.logger.debug(
234
- { Category: 'sp00ky-client::Sp00kyClient::init' },
235
- 'Local database connected'
236
- );
237
-
238
- await this.migrator.provision(this.config.schemaSurql);
239
- this.logger.debug({ Category: 'sp00ky-client::Sp00kyClient::init' }, 'Schema provisioned');
240
-
241
- await this.remote.connect();
242
- this.logger.debug(
243
- { Category: 'sp00ky-client::Sp00kyClient::init' },
244
- 'Remote database connected'
245
- );
246
-
247
- await this.streamProcessor.init();
248
- this.logger.debug(
249
- { Category: 'sp00ky-client::Sp00kyClient::init' },
250
- 'StreamProcessor initialized'
251
- );
252
-
253
- await this.auth.init();
254
- this.logger.debug({ Category: 'sp00ky-client::Sp00kyClient::init' }, 'Auth initialized');
255
-
256
- await this.dataModule.init();
257
- this.logger.debug(
258
- { Category: 'sp00ky-client::Sp00kyClient::init' },
259
- 'DataModule initialized'
260
- );
261
-
262
- await this.sync.init(clientId);
263
- this.logger.debug({ Category: 'sp00ky-client::Sp00kyClient::init' }, 'Sync initialized');
264
-
265
- this.logger.info(
266
- { Category: 'sp00ky-client::Sp00kyClient::init' },
267
- 'Sp00kyClient initialization completed successfully'
268
- );
269
- } catch (e) {
270
- this.logger.error(
271
- { error: e, Category: 'sp00ky-client::Sp00kyClient::init' },
272
- 'Sp00kyClient initialization failed'
273
- );
274
- throw e;
275
- }
276
- }
277
-
278
- async close() {
279
- await this.local.close();
280
- await this.remote.close();
281
- }
282
-
283
- authenticate(token: string) {
284
- return this.remote.getClient().authenticate(token);
285
- }
286
-
287
- deauthenticate() {
288
- return this.remote.getClient().invalidate();
289
- }
290
-
291
- query<Table extends TableNames<S>>(
292
- table: Table,
293
- options: QueryOptions<TableModel<GetTable<S, Table>>, false>,
294
- ttl: QueryTimeToLive = '10m'
295
- ): QueryBuilder<S, Table, Sp00kyQueryResultPromise> {
296
- return new QueryBuilder<S, Table, Sp00kyQueryResultPromise>(
297
- this.config.schema,
298
- table,
299
- async (q) => ({
300
- hash: await this.initQuery(table, q, ttl),
301
- }),
302
- options
303
- );
304
- }
305
-
306
- private async initQuery<Table extends TableNames<S>>(
307
- table: Table,
308
- q: InnerQuery<any, any, any>,
309
- ttl: QueryTimeToLive
310
- ) {
311
- const tableSchema = this.config.schema.tables.find((t) => t.name === table);
312
- if (!tableSchema) {
313
- throw new Error(`Table ${table} not found`);
314
- }
315
-
316
- const hash = await this.dataModule.query(
317
- table,
318
- q.selectQuery.query,
319
- parseParams(tableSchema.columns, q.selectQuery.vars ?? {}),
320
- ttl
321
- );
322
-
323
- await this.sync.enqueueDownEvent({
324
- type: 'register',
325
- payload: {
326
- hash,
327
- },
328
- });
329
-
330
- return hash;
331
- }
332
-
333
- async queryRaw(sql: string, params: Record<string, any>, ttl: QueryTimeToLive) {
334
- const tableName = sql.split('FROM ')[1].split(' ')[0];
335
- return this.dataModule.query(tableName, sql, params, ttl);
336
- }
337
-
338
- async subscribe(
339
- queryHash: string,
340
- callback: (records: Record<string, any>[]) => void,
341
- options?: { immediate?: boolean }
342
- ): Promise<() => void> {
343
- return this.dataModule.subscribe(queryHash, callback, options);
344
- }
345
-
346
- run<
347
- B extends BackendNames<S>,
348
- R extends BackendRoutes<S, B>,
349
- >(backend: B, path: R, payload: RoutePayload<S, B, R>, options?: RunOptions) {
350
- return this.dataModule.run(backend, path, payload, options);
351
- }
352
-
353
- bucket<B extends BucketNames<S>>(name: B): BucketHandle {
354
- return new BucketHandle(name, this.remote);
355
- }
356
-
357
- create(id: string, data: Record<string, unknown>) {
358
- return this.dataModule.create(id, data);
359
- }
360
-
361
- update(table: string, id: string, data: Record<string, unknown>, options?: UpdateOptions) {
362
- return this.dataModule.update(table, id, data, options);
363
- }
364
-
365
- delete(table: string, id: string) {
366
- return this.dataModule.delete(table, id);
367
- }
368
-
369
- async useRemote<T>(fn: (client: Surreal) => Promise<T> | T): Promise<T> {
370
- return fn(this.remote.getClient());
371
- }
372
-
373
- private persistClientId(id: string) {
374
- try {
375
- this.persistenceClient.set('sp00ky_client_id', id);
376
- } catch (e) {
377
- this.logger.warn(
378
- { error: e, Category: 'sp00ky-client::Sp00kyClient::persistClientId' },
379
- 'Failed to persist client ID'
380
- );
381
- }
382
- }
383
-
384
- private async loadOrGenerateClientId(): Promise<string> {
385
- const clientId = await this.persistenceClient.get<string>('sp00ky_client_id');
386
-
387
- if (clientId) {
388
- return clientId;
389
- }
390
-
391
- const newId = generateId();
392
- await this.persistClientId(newId);
393
- return newId;
394
- }
395
- }
1
+ // The client lives in client/; this path is kept for `index.ts` and deep imports.
2
+ export { Sp00kyClient } from './client/sp00ky-client';
3
+ export type { Sp00kyClientDeps } from './client/sp00ky-client';
4
+ export { BucketHandle, bucketContentToBlob } from './client/bucket-handle';
5
+ export type { BucketPutOptions, BucketPutResult } from './client/bucket-handle';
@@ -0,0 +1,200 @@
1
+ import type { QueryPlan, RecordId } from '@spooky-sync/query-builder';
2
+ import type {
3
+ ConnectionState,
4
+ MutationEventType,
5
+ QueryHash,
6
+ QueryTimeToLive,
7
+ RecordVersionArray,
8
+ RegistrationTimings,
9
+ SyncHealth,
10
+ } from '../types';
11
+ import type { QueryLifecycle } from './lifecycle';
12
+ import type { LaneState } from '../kernel/saga';
13
+ import { emptyLanes } from '../kernel/saga';
14
+
15
+ export type Row = Record<string, unknown>;
16
+
17
+ /** Everything a query is, minus anything that changes after registration. */
18
+ export interface QueryDefinition {
19
+ /** Session-salted `_00_query:<hash>` record id: the remote view id. */
20
+ readonly id: RecordId<string>;
21
+ readonly hash: QueryHash;
22
+ /** Unsalted `sha256({surql, params})`: key of the durable `_00_view` row. */
23
+ readonly viewKey: string;
24
+ readonly surql: string;
25
+ readonly params: Readonly<Record<string, unknown>>;
26
+ readonly plan?: QueryPlan;
27
+ readonly ttl: QueryTimeToLive;
28
+ readonly ttlMs: number;
29
+ readonly tableName: string;
30
+ readonly createdAt: number;
31
+ }
32
+
33
+ export type ServerViewState = 'materializing' | 'ready' | null;
34
+
35
+ export interface QueryTelemetry {
36
+ readonly updateCount: number;
37
+ readonly errorCount: number;
38
+ readonly lastUpdatedAt: number | null;
39
+ readonly materializationSamples: ReadonlyArray<number>;
40
+ readonly lastIngestLatencyMs: number | null;
41
+ readonly phaseSamples: Readonly<Record<string, ReadonlyArray<number>>>;
42
+ readonly phaseLast: Readonly<Record<string, number | null>>;
43
+ readonly registrationTimings: RegistrationTimings;
44
+ }
45
+
46
+ export interface QueryEntry {
47
+ readonly def: QueryDefinition;
48
+ readonly lifecycle: QueryLifecycle;
49
+ /** Server membership (ids + versions) or the durable seed. */
50
+ readonly remoteArray: RecordVersionArray;
51
+ /** The in-browser SSP view's id-set: local truth for "does this row match". */
52
+ readonly localArray: RecordVersionArray;
53
+ readonly subqueryRemoteArray: RecordVersionArray;
54
+ readonly records: ReadonlyArray<Row>;
55
+ readonly serverState: ServerViewState;
56
+ readonly subscribers: number;
57
+ readonly lastSubscriberLeftAt: number | null;
58
+ readonly lastHeartbeatAt: number | null;
59
+ readonly lastPolledAt: number | null;
60
+ readonly registerAttempts: number;
61
+ /**
62
+ * Re-registrations issued because the `_00_query` row read as gone, since
63
+ * the row was last seen. Paces the next one (see `applyMembership`).
64
+ */
65
+ readonly viewLostCount: number;
66
+ /** When the paced re-registration is due; null when none is pending. */
67
+ readonly viewLostRetryAt: number | null;
68
+ readonly telemetry: QueryTelemetry;
69
+ }
70
+
71
+ export type OutboxStatus = 'pending' | 'acked';
72
+
73
+ /** In-memory mirror of one `_00_pending_mutations` row plus its push progress. */
74
+ export interface OutboxItem {
75
+ readonly id: string;
76
+ readonly type: MutationEventType;
77
+ readonly recordId: string;
78
+ readonly table: string;
79
+ readonly status: OutboxStatus;
80
+ readonly ackedAt: number | null;
81
+ readonly attempts: number;
82
+ }
83
+
84
+ export type TabRole = 'solo' | 'leader' | 'follower';
85
+
86
+ /** A debounced update accumulating locally until its flush writes the outbox row. */
87
+ export interface PendingWrite {
88
+ readonly key: string;
89
+ readonly table: string;
90
+ readonly recordId: string;
91
+ readonly data: Readonly<Record<string, unknown>>;
92
+ readonly before: Readonly<Record<string, unknown>> | null;
93
+ readonly firstAt: number;
94
+ /** Its durable copy in `_00_pending_writes`; null when `flushOnHide` is off. */
95
+ readonly mirrorId: string | null;
96
+ }
97
+
98
+ export interface SyncSlice {
99
+ readonly health: SyncHealth;
100
+ readonly consecutiveFailures: number;
101
+ readonly hasSyncedOnce: boolean;
102
+ readonly selfHealAttempts: number;
103
+ readonly pollIdleStreak: number;
104
+ readonly lastReconnectRefetchAt: number | null;
105
+ readonly needsResubscribe: boolean;
106
+ readonly fetchAttempts: number;
107
+ readonly liveUuid: string | null;
108
+ readonly liveTable: string | null;
109
+ readonly lanes: LaneState;
110
+ }
111
+
112
+ export interface ClientState {
113
+ readonly sessionId: string | null;
114
+ readonly userId: string | null;
115
+ /** The principal the current session salt was minted for. */
116
+ readonly saltUserId: string | null;
117
+ /** Latest bucket-switch target; an older switch that wakes up to a newer target skips. */
118
+ readonly pendingBucket: string | null;
119
+ readonly tabId: string;
120
+ readonly tabRole: TabRole;
121
+ readonly bucketId: string | null;
122
+ readonly localReady: boolean;
123
+ readonly primed: boolean;
124
+ readonly queries: ReadonlyMap<QueryHash, QueryEntry>;
125
+ /** Hashes whose local registration is in flight (dedupes concurrent `query()` calls). */
126
+ readonly registering: ReadonlySet<QueryHash>;
127
+ /** Re-read attempts per hash while the server reports a view as `materializing`. */
128
+ readonly membershipReread: ReadonlyMap<QueryHash, number>;
129
+ /** Local body versions (`_00_rv`) by encoded record id. */
130
+ readonly versions: ReadonlyMap<string, number>;
131
+ /**
132
+ * What each durable `_00_view` row vouches for, by view key: its members
133
+ * and subquery children. A body no row names is an orphan: it never paints
134
+ * before the server answers and the GC deletes it.
135
+ */
136
+ readonly views: ReadonlyMap<string, ReadonlyArray<string>>;
137
+ readonly outbox: ReadonlyArray<OutboxItem>;
138
+ readonly pendingWrites: ReadonlyMap<string, PendingWrite>;
139
+ readonly failedCount: number;
140
+ /** Queries whose render inputs changed since their last materialization. */
141
+ readonly dirty: ReadonlySet<QueryHash>;
142
+ /** Queries whose server membership must be re-read. */
143
+ readonly membershipDirty: ReadonlySet<QueryHash>;
144
+ readonly sync: SyncSlice;
145
+ }
146
+
147
+ export const emptyTelemetry = (): QueryTelemetry => ({
148
+ updateCount: 0,
149
+ errorCount: 0,
150
+ lastUpdatedAt: null,
151
+ materializationSamples: [],
152
+ lastIngestLatencyMs: null,
153
+ phaseSamples: {},
154
+ phaseLast: {},
155
+ registrationTimings: { parseMs: null, planMs: null, snapshotMs: null, wallMs: null },
156
+ });
157
+
158
+ export const initialHealth = (connection: ConnectionState = 'disconnected'): SyncHealth => ({
159
+ status: 'healthy',
160
+ consecutiveFailures: 0,
161
+ everConnected: false,
162
+ connection,
163
+ });
164
+
165
+ export function emptyState(init: { tabId: string }): ClientState {
166
+ return {
167
+ sessionId: null,
168
+ userId: null,
169
+ saltUserId: null,
170
+ pendingBucket: null,
171
+ tabId: init.tabId,
172
+ tabRole: 'solo',
173
+ bucketId: null,
174
+ localReady: false,
175
+ primed: false,
176
+ queries: new Map(),
177
+ registering: new Set(),
178
+ membershipReread: new Map(),
179
+ versions: new Map(),
180
+ views: new Map(),
181
+ outbox: [],
182
+ pendingWrites: new Map(),
183
+ failedCount: 0,
184
+ dirty: new Set(),
185
+ membershipDirty: new Set(),
186
+ sync: {
187
+ health: initialHealth(),
188
+ consecutiveFailures: 0,
189
+ hasSyncedOnce: false,
190
+ selfHealAttempts: 0,
191
+ pollIdleStreak: 0,
192
+ lastReconnectRefetchAt: null,
193
+ needsResubscribe: false,
194
+ fetchAttempts: 0,
195
+ liveUuid: null,
196
+ liveTable: null,
197
+ lanes: emptyLanes(),
198
+ },
199
+ };
200
+ }