@spooky-sync/core 0.0.1-canary.38 → 0.0.1-canary.381

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 (266) hide show
  1. package/AGENTS.md +91 -0
  2. package/README.md +59 -16
  3. package/dist/index.d.ts +2238 -144
  4. package/dist/index.js +18149 -2475
  5. package/dist/live.d.ts +149 -0
  6. package/dist/live.js +739 -0
  7. package/dist/otel/index.d.ts +1 -1
  8. package/dist/pure.d.ts +401 -0
  9. package/dist/pure.js +24 -0
  10. package/dist/sqlite-open.js +392 -0
  11. package/dist/sqlite-worker.d.ts +1 -0
  12. package/dist/sqlite-worker.js +439 -0
  13. package/dist/sw.d.ts +207 -0
  14. package/dist/sw.js +393 -0
  15. package/dist/tabs-broker-worker.d.ts +8 -0
  16. package/dist/tabs-broker-worker.js +477 -0
  17. package/dist/types.d.ts +2082 -39
  18. package/dist/worker-edges.js +498 -0
  19. package/dist/worker-impersonation.js +60 -0
  20. package/dist/worker-ref-tables.d.ts +48 -0
  21. package/dist/worker-types.d.ts +112 -0
  22. package/dist/worker-types.js +73 -0
  23. package/package.json +28 -7
  24. package/scripts/check-broker-bundle.mjs +33 -0
  25. package/scripts/check-worker-entries.mjs +173 -0
  26. package/skills/sp00ky-core/SKILL.md +42 -8
  27. package/skills/sp00ky-core/references/config.md +8 -2
  28. package/src/boot/auth-flip.saga.test.ts +174 -0
  29. package/src/boot/auth-flip.saga.ts +77 -0
  30. package/src/boot/boot.saga.test.ts +203 -0
  31. package/src/boot/boot.saga.ts +146 -0
  32. package/src/boot/bucket-switch.saga.test.ts +191 -0
  33. package/src/boot/bucket-switch.saga.ts +151 -0
  34. package/src/boot/preload.saga.test.ts +96 -0
  35. package/src/boot/preload.saga.ts +45 -0
  36. package/src/bucket-blurhash.test.ts +148 -0
  37. package/src/build-globals.d.ts +12 -0
  38. package/src/client/bucket-handle.test.ts +195 -0
  39. package/src/client/bucket-handle.ts +352 -0
  40. package/src/client/router.test.ts +59 -0
  41. package/src/client/router.ts +104 -0
  42. package/src/client/runtime.test.ts +319 -0
  43. package/src/client/runtime.ts +307 -0
  44. package/src/client/services.test.ts +74 -0
  45. package/src/client/services.ts +437 -0
  46. package/src/client/sp00ky-client.test.ts +428 -0
  47. package/src/client/sp00ky-client.ts +737 -0
  48. package/src/events/index.ts +3 -0
  49. package/src/index.ts +86 -1
  50. package/src/kernel/constants.test.ts +19 -0
  51. package/src/kernel/constants.ts +94 -0
  52. package/src/kernel/effects.test.ts +40 -0
  53. package/src/kernel/effects.ts +171 -0
  54. package/src/kernel/events.ts +60 -0
  55. package/src/kernel/interpreter.test.ts +116 -0
  56. package/src/kernel/interpreter.ts +148 -0
  57. package/src/kernel/saga.test.ts +89 -0
  58. package/src/kernel/saga.ts +73 -0
  59. package/src/live/index.test.ts +512 -0
  60. package/src/live/index.ts +1059 -0
  61. package/src/modules/app-release/index.test.ts +125 -0
  62. package/src/modules/app-release/index.ts +209 -0
  63. package/src/modules/auth/auth.local-first.test.ts +228 -0
  64. package/src/modules/auth/auth.session-recheck.test.ts +140 -0
  65. package/src/modules/auth/events/index.ts +7 -1
  66. package/src/modules/auth/impersonation-banner.ts +443 -0
  67. package/src/modules/auth/impersonation.integration.test.ts +209 -0
  68. package/src/modules/auth/impersonation.test.ts +377 -0
  69. package/src/modules/auth/impersonation.ts +179 -0
  70. package/src/modules/auth/index.ts +538 -14
  71. package/src/modules/channel/channel.test.ts +752 -0
  72. package/src/modules/channel/index.ts +1153 -0
  73. package/src/modules/channel/webrtc.ts +472 -0
  74. package/src/modules/crdt/acceptance.ts +75 -0
  75. package/src/modules/crdt/bytes.ts +50 -0
  76. package/src/modules/crdt/crdt-field.ts +125 -0
  77. package/src/modules/crdt/crdt.test.ts +546 -0
  78. package/src/modules/crdt/hash.ts +103 -0
  79. package/src/modules/crdt/index.ts +821 -0
  80. package/src/modules/crdt/loro-loader.ts +25 -0
  81. package/src/modules/crdt/mirror.ts +37 -0
  82. package/src/modules/devtools/flags.ts +349 -0
  83. package/src/modules/devtools/index.ts +746 -93
  84. package/src/modules/devtools/mutations.test.ts +92 -0
  85. package/src/modules/devtools/mutations.ts +160 -0
  86. package/src/modules/devtools/notify-throttle.test.ts +166 -0
  87. package/src/modules/devtools/state-shape.test.ts +263 -0
  88. package/src/modules/devtools/storage-info.test.ts +79 -0
  89. package/src/modules/devtools/storage-info.ts +168 -0
  90. package/src/modules/devtools/versions.test.ts +74 -0
  91. package/src/modules/devtools/versions.ts +110 -0
  92. package/src/modules/feature-flag/index.test.ts +298 -0
  93. package/src/modules/feature-flag/index.ts +316 -0
  94. package/src/modules/ref-tables.test.ts +93 -0
  95. package/src/modules/ref-tables.ts +91 -0
  96. package/src/modules/web-push/index.test.ts +824 -0
  97. package/src/modules/web-push/index.ts +1027 -0
  98. package/src/mutation/ingest-outage.integration.test.ts +400 -0
  99. package/src/mutation/jobs.test.ts +25 -0
  100. package/src/mutation/jobs.ts +41 -0
  101. package/src/mutation/mutation-id.test.ts +37 -0
  102. package/src/mutation/mutation-id.ts +35 -0
  103. package/src/mutation/pending-writes.integration.test.ts +175 -0
  104. package/src/mutation/push.saga.test.ts +361 -0
  105. package/src/mutation/push.saga.ts +245 -0
  106. package/src/mutation/rows.test.ts +278 -0
  107. package/src/mutation/rows.ts +515 -0
  108. package/src/mutation/tray.saga.test.ts +63 -0
  109. package/src/mutation/tray.saga.ts +48 -0
  110. package/src/mutation/write.saga.test.ts +436 -0
  111. package/src/mutation/write.saga.ts +384 -0
  112. package/src/pure.ts +66 -0
  113. package/src/push/types.ts +174 -0
  114. package/src/query/anon-query-isolation.integration.test.ts +531 -0
  115. package/src/query/env.test.ts +23 -0
  116. package/src/query/env.ts +65 -0
  117. package/src/query/fetch.saga.test.ts +199 -0
  118. package/src/query/fetch.saga.ts +120 -0
  119. package/src/query/hash.test.ts +14 -0
  120. package/src/query/hash.ts +18 -0
  121. package/src/query/id-set-plan.test.ts +132 -0
  122. package/src/query/lifecycle.saga.test.ts +274 -0
  123. package/src/query/lifecycle.saga.ts +186 -0
  124. package/src/query/list-ref-live.integration.test.ts +203 -0
  125. package/src/query/materialize.saga.test.ts +200 -0
  126. package/src/query/materialize.saga.ts +102 -0
  127. package/src/query/materialize.test.ts +58 -0
  128. package/src/query/materialize.ts +57 -0
  129. package/src/query/membership.saga.test.ts +514 -0
  130. package/src/query/membership.saga.ts +339 -0
  131. package/src/query/membership.test.ts +151 -0
  132. package/src/query/membership.ts +234 -0
  133. package/src/query/register.saga.test.ts +369 -0
  134. package/src/query/register.saga.ts +293 -0
  135. package/src/query/render-set.test.ts +47 -0
  136. package/src/query/render-set.ts +71 -0
  137. package/src/query/session-guard.integration.test.ts +304 -0
  138. package/src/query/sql.test.ts +118 -0
  139. package/src/query/sql.ts +174 -0
  140. package/src/query/window-query.test.ts +70 -0
  141. package/src/query/window-query.ts +208 -0
  142. package/src/services/blobs/blob-cache.test.ts +359 -0
  143. package/src/services/blobs/blob-cache.ts +603 -0
  144. package/src/services/blobs/blob-manifest.ts +227 -0
  145. package/src/services/blobs/blob-store.test.ts +77 -0
  146. package/src/services/blobs/blob-store.ts +359 -0
  147. package/src/services/blobs/blob.fixture.ts +90 -0
  148. package/src/services/blobs/index.ts +70 -0
  149. package/src/services/database/cache-engine.ts +209 -0
  150. package/src/services/database/connection-supervisor.test.ts +318 -0
  151. package/src/services/database/connection-supervisor.ts +422 -0
  152. package/src/services/database/database.query-timeout.test.ts +140 -0
  153. package/src/services/database/database.responses.test.ts +43 -0
  154. package/src/services/database/database.ts +151 -56
  155. package/src/services/database/engine-factory.ts +33 -0
  156. package/src/services/database/errors.ts +34 -0
  157. package/src/services/database/index.ts +7 -0
  158. package/src/services/database/landed-body-clear.integration.test.ts +236 -0
  159. package/src/services/database/local-migrator.integration.test.ts +110 -0
  160. package/src/services/database/local-migrator.ts +54 -9
  161. package/src/services/database/local.test.ts +150 -0
  162. package/src/services/database/local.ts +503 -59
  163. package/src/services/database/plan-render.test.ts +159 -0
  164. package/src/services/database/plan-render.ts +108 -0
  165. package/src/services/database/relation-resolver.test.ts +484 -0
  166. package/src/services/database/relation-resolver.ts +0 -0
  167. package/src/services/database/remote.ts +283 -4
  168. package/src/services/database/sqlite-cache-engine.test.ts +628 -0
  169. package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
  170. package/src/services/database/sqlite-cache-engine.ts +1401 -0
  171. package/src/services/database/sqlite-column-reset.integration.test.ts +100 -0
  172. package/src/services/database/sqlite-column-reset.ts +91 -0
  173. package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
  174. package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
  175. package/src/services/database/sqlite-lock-verify.test.ts +33 -0
  176. package/src/services/database/sqlite-lock-verify.ts +45 -0
  177. package/src/services/database/sqlite-open.test.ts +150 -0
  178. package/src/services/database/sqlite-open.ts +164 -0
  179. package/src/services/database/sqlite-plan-sql.test.ts +164 -0
  180. package/src/services/database/sqlite-plan-sql.ts +196 -0
  181. package/src/services/database/sqlite-projection.test.ts +121 -0
  182. package/src/services/database/sqlite-select.integration.test.ts +185 -0
  183. package/src/services/database/sqlite-select.test.ts +261 -0
  184. package/src/services/database/sqlite-select.ts +131 -0
  185. package/src/services/database/sqlite-transport.fixture.ts +30 -0
  186. package/src/services/database/sqlite-transport.ts +224 -0
  187. package/src/services/database/sqlite-worker.ts +437 -0
  188. package/src/services/database/surql-translate.ts +417 -0
  189. package/src/services/database/surreal-cache-engine.ts +163 -0
  190. package/src/services/logger/index.test.ts +132 -0
  191. package/src/services/logger/index.ts +187 -6
  192. package/src/services/persistence/resilient.ts +8 -1
  193. package/src/services/persistence/surrealdb.ts +2 -2
  194. package/src/services/stream-processor/index.ts +801 -63
  195. package/src/services/stream-processor/permissions.test.ts +47 -0
  196. package/src/services/stream-processor/permissions.ts +53 -0
  197. package/src/services/stream-processor/stream-processor.batch.test.ts +186 -0
  198. package/src/services/stream-processor/stream-processor.prime.test.ts +234 -0
  199. package/src/services/stream-processor/stream-processor.reset.test.ts +226 -0
  200. package/src/services/stream-processor/wasm-types.ts +57 -1
  201. package/src/services/tabs/broker-client.ts +284 -0
  202. package/src/services/tabs/broker.test.ts +381 -0
  203. package/src/services/tabs/coordinator.test.ts +386 -0
  204. package/src/services/tabs/coordinator.ts +646 -0
  205. package/src/services/tabs/fake-ports.fixture.ts +112 -0
  206. package/src/services/tabs/leader-locks.ts +75 -0
  207. package/src/services/tabs/protocol.ts +262 -0
  208. package/src/services/tabs/support.ts +36 -0
  209. package/src/services/tabs/tabs-broker-worker.ts +652 -0
  210. package/src/sp00ky.ts +5 -394
  211. package/src/state/client-state.ts +231 -0
  212. package/src/state/lifecycle.test.ts +184 -0
  213. package/src/state/lifecycle.ts +127 -0
  214. package/src/state/reducers.test.ts +337 -0
  215. package/src/state/reducers.ts +571 -0
  216. package/src/state/selectors.test.ts +203 -0
  217. package/src/state/selectors.ts +264 -0
  218. package/src/sw/index.test.ts +582 -0
  219. package/src/sw/index.ts +607 -0
  220. package/src/sw/store.ts +96 -0
  221. package/src/sync/connection.saga.test.ts +97 -0
  222. package/src/sync/connection.saga.ts +89 -0
  223. package/src/sync/live.saga.test.ts +229 -0
  224. package/src/sync/live.saga.ts +116 -0
  225. package/src/sync/policy.health.test.ts +56 -0
  226. package/src/sync/policy.test.ts +663 -0
  227. package/src/sync/policy.ts +515 -0
  228. package/src/sync/poll.saga.test.ts +51 -0
  229. package/src/sync/poll.saga.ts +36 -0
  230. package/src/sync/tabs.saga.test.ts +78 -0
  231. package/src/sync/tabs.saga.ts +96 -0
  232. package/src/testing/adapters.ts +137 -0
  233. package/src/testing/build.ts +63 -0
  234. package/src/testing/fake-services.ts +63 -0
  235. package/src/testing/run-pure.test.ts +92 -0
  236. package/src/testing/run-pure.ts +128 -0
  237. package/src/testing/services.ts +17 -0
  238. package/src/types.ts +694 -7
  239. package/src/utils/blurhash.ts +90 -0
  240. package/src/utils/edges.ts +57 -0
  241. package/src/utils/error-classification.test.ts +116 -0
  242. package/src/utils/error-classification.ts +74 -0
  243. package/src/utils/index.ts +83 -6
  244. package/src/utils/parser.test.ts +67 -102
  245. package/src/utils/parser.ts +63 -2
  246. package/src/utils/semver.test.ts +32 -0
  247. package/src/utils/semver.ts +30 -0
  248. package/src/utils/sha256.ts +15 -0
  249. package/src/utils/surql.ts +22 -11
  250. package/src/utils/utils.test.ts +3 -2
  251. package/tsdown.config.ts +117 -3
  252. package/vitest.config.ts +48 -0
  253. package/src/modules/cache/index.ts +0 -241
  254. package/src/modules/cache/types.ts +0 -19
  255. package/src/modules/data/data.test.ts +0 -58
  256. package/src/modules/data/index.ts +0 -882
  257. package/src/modules/sync/engine.ts +0 -173
  258. package/src/modules/sync/events/index.ts +0 -78
  259. package/src/modules/sync/index.ts +0 -3
  260. package/src/modules/sync/queue/index.ts +0 -2
  261. package/src/modules/sync/queue/queue-down.ts +0 -90
  262. package/src/modules/sync/queue/queue-up.ts +0 -224
  263. package/src/modules/sync/scheduler.ts +0 -84
  264. package/src/modules/sync/sync.ts +0 -422
  265. package/src/modules/sync/utils.test.ts +0 -311
  266. package/src/modules/sync/utils.ts +0 -169
package/src/sp00ky.ts CHANGED
@@ -1,394 +1,5 @@
1
- import { DataModule } from './modules/data/index';
2
- import type {
3
- Sp00kyConfig,
4
- QueryTimeToLive,
5
- Sp00kyQueryResultPromise,
6
- PersistenceClient,
7
- UpdateOptions,
8
- RunOptions} from './types';
9
- import {
10
- LocalDatabaseService,
11
- LocalMigrator,
12
- RemoteDatabaseService,
13
- } from './services/database/index';
14
- import type { UpEvent } from './modules/sync/index';
15
- import { Sp00kySync } from './modules/sync/index';
16
- import type {
17
- GetTable,
18
- InnerQuery,
19
- QueryOptions,
20
- SchemaStructure,
21
- TableModel,
22
- TableNames,
23
- BucketNames,
24
- BackendNames,
25
- BackendRoutes,
26
- RoutePayload} from '@spooky-sync/query-builder';
27
- import {
28
- QueryBuilder
29
- } from '@spooky-sync/query-builder';
30
-
31
- import { DevToolsService } from './modules/devtools/index';
32
- import { createLogger } from './services/logger/index';
33
- import { AuthService } from './modules/auth/index';
34
- import { StreamProcessorService } from './services/stream-processor/index';
35
- import { EventSystem } from './events/index';
36
- import { CacheModule } from './modules/cache/index';
37
- import { LocalStoragePersistenceClient } from './services/persistence/localstorage';
38
- import { generateId, parseParams } from './utils/index';
39
- import { SurrealDBPersistenceClient } from './services/persistence/surrealdb';
40
- import { ResilientPersistenceClient } from './services/persistence/resilient';
41
-
42
- export class BucketHandle {
43
- constructor(private bucketName: string, private remote: RemoteDatabaseService) {}
44
-
45
- async put(path: string, content: string | Uint8Array | Blob): Promise<void> {
46
- await this.remote.query(`RETURN f"${this.bucketName}:/${path}".put($content);`, { content });
47
- }
48
-
49
- async get(path: string): Promise<unknown> {
50
- const [result] = await this.remote.query<[unknown]>(`RETURN f"${this.bucketName}:/${path}".get();`);
51
- return result;
52
- }
53
-
54
- async delete(path: string): Promise<void> {
55
- await this.remote.query(`RETURN f"${this.bucketName}:/${path}".delete();`);
56
- }
57
-
58
- async exists(path: string): Promise<boolean> {
59
- const [result] = await this.remote.query<[boolean]>(`RETURN f"${this.bucketName}:/${path}".exists();`);
60
- return result;
61
- }
62
-
63
- async head(path: string): Promise<Record<string, unknown>> {
64
- const [result] = await this.remote.query<[Record<string, unknown>]>(`RETURN f"${this.bucketName}:/${path}".head();`);
65
- return result;
66
- }
67
-
68
- async copy(sourcePath: string, targetPath: string): Promise<void> {
69
- await this.remote.query(`RETURN f"${this.bucketName}:/${sourcePath}".copy($target);`, { target: targetPath });
70
- }
71
-
72
- async rename(sourcePath: string, targetPath: string): Promise<void> {
73
- await this.remote.query(`RETURN f"${this.bucketName}:/${sourcePath}".rename($target);`, { target: targetPath });
74
- }
75
-
76
- async list(prefix?: string): Promise<string[]> {
77
- const p = prefix ?? '';
78
- const [result] = await this.remote.query<[string[]]>(`RETURN f"${this.bucketName}:/${p}".list();`);
79
- return result;
80
- }
81
- }
82
-
83
- export class Sp00kyClient<S extends SchemaStructure> {
84
- private local: LocalDatabaseService;
85
- private remote: RemoteDatabaseService;
86
- private persistenceClient: PersistenceClient;
87
-
88
- private migrator: LocalMigrator;
89
- private cache: CacheModule;
90
- private dataModule: DataModule<S>;
91
- private sync: Sp00kySync<S>;
92
- private devTools: DevToolsService;
93
-
94
- private logger: ReturnType<typeof createLogger>;
95
- public auth: AuthService<S>;
96
- public streamProcessor: StreamProcessorService;
97
-
98
- get remoteClient() {
99
- return this.remote.getClient();
100
- }
101
-
102
- get localClient() {
103
- return this.local.getClient();
104
- }
105
-
106
- get pendingMutationCount(): number {
107
- return this.sync.pendingMutationCount;
108
- }
109
-
110
- subscribeToPendingMutations(cb: (count: number) => void): () => void {
111
- return this.sync.subscribeToPendingMutations(cb);
112
- }
113
-
114
- constructor(private config: Sp00kyConfig<S>) {
115
- const logger = createLogger(config.logLevel ?? 'info', config.otelTransmit);
116
- this.logger = logger.child({ service: 'Sp00kyClient' });
117
-
118
- this.logger.info(
119
- {
120
- config: { ...config, schema: '[SchemaStructure]' },
121
- Category: 'sp00ky-client::Sp00kyClient::constructor',
122
- },
123
- 'Sp00kyClient initialized'
124
- );
125
-
126
- this.local = new LocalDatabaseService(this.config.database, logger);
127
- this.remote = new RemoteDatabaseService(this.config.database, logger);
128
-
129
- if (config.persistenceClient === 'surrealdb') {
130
- this.persistenceClient = new SurrealDBPersistenceClient(this.local, logger);
131
- } else if (config.persistenceClient === 'localstorage' || !config.persistenceClient) {
132
- this.persistenceClient = new LocalStoragePersistenceClient(logger);
133
- } else {
134
- this.persistenceClient = config.persistenceClient;
135
- }
136
-
137
- this.persistenceClient = new ResilientPersistenceClient(this.persistenceClient, logger);
138
-
139
- this.streamProcessor = new StreamProcessorService(
140
- new EventSystem(['stream_update']),
141
- this.local,
142
- this.persistenceClient,
143
- logger
144
- );
145
- this.migrator = new LocalMigrator(this.local, logger);
146
-
147
- this.cache = new CacheModule(
148
- this.local,
149
- this.streamProcessor,
150
- (update) => {
151
- // Direct callback from cache to data module
152
- this.dataModule.onStreamUpdate(update);
153
- },
154
- logger
155
- );
156
-
157
- this.dataModule = new DataModule(
158
- this.cache,
159
- this.local,
160
- this.config.schema,
161
- logger,
162
- this.config.streamDebounceTime
163
- );
164
-
165
- // Initialize Auth
166
- this.auth = new AuthService(this.config.schema, this.remote, this.persistenceClient, logger);
167
-
168
- // Initialize Sync
169
- this.sync = new Sp00kySync(this.local, this.remote, this.cache, this.dataModule, this.config.schema, this.logger);
170
-
171
- // Initialize DevTools
172
- this.devTools = new DevToolsService(
173
- this.local,
174
- this.remote,
175
- logger,
176
- this.config.schema,
177
- this.auth,
178
- this.dataModule
179
- );
180
-
181
- // Register DevTools as a receiver for stream updates
182
- this.streamProcessor.addReceiver(this.devTools);
183
-
184
- // Wire up callbacks instead of events
185
- this.setupCallbacks();
186
- }
187
-
188
- /**
189
- * Setup direct callbacks instead of event subscriptions
190
- */
191
- private setupCallbacks() {
192
- // Mutation callback for sync
193
- this.dataModule.onMutation((mutations: UpEvent[]) => {
194
- // Notify DevTools
195
- this.devTools.onMutation(mutations);
196
-
197
- // Enqueue in Sync
198
- if (mutations.length > 0) {
199
- this.sync.enqueueMutation(mutations);
200
- }
201
- });
202
-
203
- // Sync events for incoming updates
204
- this.sync.events.subscribe('SYNC_QUERY_UPDATED', (event: any) => {
205
- this.devTools.logEvent('SYNC_QUERY_UPDATED', event.payload);
206
- });
207
-
208
- // Database events for DevTools
209
- this.local.getEvents().subscribe('DATABASE_LOCAL_QUERY', (event: any) => {
210
- this.devTools.logEvent('LOCAL_QUERY', event.payload);
211
- });
212
-
213
- this.remote.getEvents().subscribe('DATABASE_REMOTE_QUERY', (event: any) => {
214
- this.devTools.logEvent('REMOTE_QUERY', event.payload);
215
- });
216
- }
217
-
218
- async init() {
219
- this.logger.info(
220
- { Category: 'sp00ky-client::Sp00kyClient::init' },
221
- 'Sp00kyClient initialization started'
222
- );
223
- try {
224
- const clientId = this.config.clientId ?? (await this.loadOrGenerateClientId());
225
- this.persistClientId(clientId);
226
- this.logger.debug(
227
- { clientId, Category: 'sp00ky-client::Sp00kyClient::init' },
228
- 'Client ID loaded'
229
- );
230
-
231
- await this.local.connect();
232
- this.logger.debug(
233
- { Category: 'sp00ky-client::Sp00kyClient::init' },
234
- 'Local database connected'
235
- );
236
-
237
- await this.migrator.provision(this.config.schemaSurql);
238
- this.logger.debug({ Category: 'sp00ky-client::Sp00kyClient::init' }, 'Schema provisioned');
239
-
240
- await this.remote.connect();
241
- this.logger.debug(
242
- { Category: 'sp00ky-client::Sp00kyClient::init' },
243
- 'Remote database connected'
244
- );
245
-
246
- await this.streamProcessor.init();
247
- this.logger.debug(
248
- { Category: 'sp00ky-client::Sp00kyClient::init' },
249
- 'StreamProcessor initialized'
250
- );
251
-
252
- await this.auth.init();
253
- this.logger.debug({ Category: 'sp00ky-client::Sp00kyClient::init' }, 'Auth initialized');
254
-
255
- await this.dataModule.init();
256
- this.logger.debug(
257
- { Category: 'sp00ky-client::Sp00kyClient::init' },
258
- 'DataModule initialized'
259
- );
260
-
261
- await this.sync.init(clientId);
262
- this.logger.debug({ Category: 'sp00ky-client::Sp00kyClient::init' }, 'Sync initialized');
263
-
264
- this.logger.info(
265
- { Category: 'sp00ky-client::Sp00kyClient::init' },
266
- 'Sp00kyClient initialization completed successfully'
267
- );
268
- } catch (e) {
269
- this.logger.error(
270
- { error: e, Category: 'sp00ky-client::Sp00kyClient::init' },
271
- 'Sp00kyClient initialization failed'
272
- );
273
- throw e;
274
- }
275
- }
276
-
277
- async close() {
278
- await this.local.close();
279
- await this.remote.close();
280
- }
281
-
282
- authenticate(token: string) {
283
- return this.remote.getClient().authenticate(token);
284
- }
285
-
286
- deauthenticate() {
287
- return this.remote.getClient().invalidate();
288
- }
289
-
290
- query<Table extends TableNames<S>>(
291
- table: Table,
292
- options: QueryOptions<TableModel<GetTable<S, Table>>, false>,
293
- ttl: QueryTimeToLive = '10m'
294
- ): QueryBuilder<S, Table, Sp00kyQueryResultPromise> {
295
- return new QueryBuilder<S, Table, Sp00kyQueryResultPromise>(
296
- this.config.schema,
297
- table,
298
- async (q) => ({
299
- hash: await this.initQuery(table, q, ttl),
300
- }),
301
- options
302
- );
303
- }
304
-
305
- private async initQuery<Table extends TableNames<S>>(
306
- table: Table,
307
- q: InnerQuery<any, any, any>,
308
- ttl: QueryTimeToLive
309
- ) {
310
- const tableSchema = this.config.schema.tables.find((t) => t.name === table);
311
- if (!tableSchema) {
312
- throw new Error(`Table ${table} not found`);
313
- }
314
-
315
- const hash = await this.dataModule.query(
316
- table,
317
- q.selectQuery.query,
318
- parseParams(tableSchema.columns, q.selectQuery.vars ?? {}),
319
- ttl
320
- );
321
-
322
- await this.sync.enqueueDownEvent({
323
- type: 'register',
324
- payload: {
325
- hash,
326
- },
327
- });
328
-
329
- return hash;
330
- }
331
-
332
- async queryRaw(sql: string, params: Record<string, any>, ttl: QueryTimeToLive) {
333
- const tableName = sql.split('FROM ')[1].split(' ')[0];
334
- return this.dataModule.query(tableName, sql, params, ttl);
335
- }
336
-
337
- async subscribe(
338
- queryHash: string,
339
- callback: (records: Record<string, any>[]) => void,
340
- options?: { immediate?: boolean }
341
- ): Promise<() => void> {
342
- return this.dataModule.subscribe(queryHash, callback, options);
343
- }
344
-
345
- run<
346
- B extends BackendNames<S>,
347
- R extends BackendRoutes<S, B>,
348
- >(backend: B, path: R, payload: RoutePayload<S, B, R>, options?: RunOptions) {
349
- return this.dataModule.run(backend, path, payload, options);
350
- }
351
-
352
- bucket<B extends BucketNames<S>>(name: B): BucketHandle {
353
- return new BucketHandle(name, this.remote);
354
- }
355
-
356
- create(id: string, data: Record<string, unknown>) {
357
- return this.dataModule.create(id, data);
358
- }
359
-
360
- update(table: string, id: string, data: Record<string, unknown>, options?: UpdateOptions) {
361
- return this.dataModule.update(table, id, data, options);
362
- }
363
-
364
- delete(table: string, id: string) {
365
- return this.dataModule.delete(table, id);
366
- }
367
-
368
- async useRemote<T>(fn: (client: Surreal) => Promise<T> | T): Promise<T> {
369
- return fn(this.remote.getClient());
370
- }
371
-
372
- private persistClientId(id: string) {
373
- try {
374
- this.persistenceClient.set('sp00ky_client_id', id);
375
- } catch (e) {
376
- this.logger.warn(
377
- { error: e, Category: 'sp00ky-client::Sp00kyClient::persistClientId' },
378
- 'Failed to persist client ID'
379
- );
380
- }
381
- }
382
-
383
- private async loadOrGenerateClientId(): Promise<string> {
384
- const clientId = await this.persistenceClient.get<string>('sp00ky_client_id');
385
-
386
- if (clientId) {
387
- return clientId;
388
- }
389
-
390
- const newId = generateId();
391
- await this.persistClientId(newId);
392
- return newId;
393
- }
394
- }
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,231 @@
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
+ * A request carrying this row may have committed without the answer
84
+ * reaching this client: it threw a transport error, or the row was loaded
85
+ * from the store, so a session that died mid-push may have sent it. Its
86
+ * CREATE then accepts the row already on the server when that row holds
87
+ * what it would write (see `remoteBatch`). Absent means false.
88
+ */
89
+ readonly inDoubt?: boolean;
90
+ }
91
+
92
+ export type TabRole = 'solo' | 'leader' | 'follower';
93
+
94
+ /** A debounced update accumulating locally until its flush writes the outbox row. */
95
+ export interface PendingWrite {
96
+ readonly key: string;
97
+ readonly table: string;
98
+ readonly recordId: string;
99
+ readonly data: Readonly<Record<string, unknown>>;
100
+ readonly before: Readonly<Record<string, unknown>> | null;
101
+ readonly firstAt: number;
102
+ /** Its durable copy in `_00_pending_writes`; null when `flushOnHide` is off. */
103
+ readonly mirrorId: string | null;
104
+ }
105
+
106
+ /**
107
+ * A create or delete a channel peer sent, applied here before the server has
108
+ * it (see `modules/channel`). Shown like an own optimistic write through the
109
+ * overlay; when `expiresAt` passes it is either confirmed (the server's body
110
+ * landed) or undone.
111
+ */
112
+ export interface PeerWrite {
113
+ readonly kind: 'create' | 'delete';
114
+ readonly table: string;
115
+ readonly expiresAt: number;
116
+ }
117
+
118
+ export interface SyncSlice {
119
+ readonly health: SyncHealth;
120
+ readonly consecutiveFailures: number;
121
+ readonly hasSyncedOnce: boolean;
122
+ readonly selfHealAttempts: number;
123
+ readonly pollIdleStreak: number;
124
+ readonly lastReconnectRefetchAt: number | null;
125
+ readonly needsResubscribe: boolean;
126
+ readonly fetchAttempts: number;
127
+ readonly liveUuid: string | null;
128
+ readonly liveTable: string | null;
129
+ readonly lanes: LaneState;
130
+ }
131
+
132
+ export interface ClientState {
133
+ readonly sessionId: string | null;
134
+ readonly userId: string | null;
135
+ /** The principal the current session salt was minted for. */
136
+ readonly saltUserId: string | null;
137
+ /**
138
+ * This page's secret, sent with every registration (`$config.owner`). An
139
+ * anonymous view answers only to the secret it was registered with, so it
140
+ * lives as long as the page's views: a reconnect keeps it, and a reload
141
+ * that keeps the salt keeps it too.
142
+ */
143
+ readonly ownerSecret: string | null;
144
+ /** Latest bucket-switch target; an older switch that wakes up to a newer target skips. */
145
+ readonly pendingBucket: string | null;
146
+ readonly tabId: string;
147
+ readonly tabRole: TabRole;
148
+ readonly bucketId: string | null;
149
+ readonly localReady: boolean;
150
+ readonly primed: boolean;
151
+ readonly queries: ReadonlyMap<QueryHash, QueryEntry>;
152
+ /** Hashes whose local registration is in flight (dedupes concurrent `query()` calls). */
153
+ readonly registering: ReadonlySet<QueryHash>;
154
+ /** Re-read attempts per hash while the server reports a view as `materializing`. */
155
+ readonly membershipReread: ReadonlyMap<QueryHash, number>;
156
+ /** Local body versions (`_00_rv`) by encoded record id. */
157
+ readonly versions: ReadonlyMap<string, number>;
158
+ /**
159
+ * What each durable `_00_view` row vouches for, by view key: its members
160
+ * and subquery children. A body no row names is an orphan: it never paints
161
+ * before the server answers and the GC deletes it.
162
+ */
163
+ readonly views: ReadonlyMap<string, ReadonlyArray<string>>;
164
+ readonly outbox: ReadonlyArray<OutboxItem>;
165
+ readonly pendingWrites: ReadonlyMap<string, PendingWrite>;
166
+ /** Channel peers' unconfirmed creates and deletes, by record id. */
167
+ readonly peerWrites: ReadonlyMap<string, PeerWrite>;
168
+ readonly failedCount: number;
169
+ /** Queries whose render inputs changed since their last materialization. */
170
+ readonly dirty: ReadonlySet<QueryHash>;
171
+ /** Queries whose server membership must be re-read. */
172
+ readonly membershipDirty: ReadonlySet<QueryHash>;
173
+ readonly sync: SyncSlice;
174
+ }
175
+
176
+ export const emptyTelemetry = (): QueryTelemetry => ({
177
+ updateCount: 0,
178
+ errorCount: 0,
179
+ lastUpdatedAt: null,
180
+ materializationSamples: [],
181
+ lastIngestLatencyMs: null,
182
+ phaseSamples: {},
183
+ phaseLast: {},
184
+ registrationTimings: { parseMs: null, planMs: null, snapshotMs: null, wallMs: null },
185
+ });
186
+
187
+ export const initialHealth = (connection: ConnectionState = 'disconnected'): SyncHealth => ({
188
+ status: 'healthy',
189
+ consecutiveFailures: 0,
190
+ everConnected: false,
191
+ connection,
192
+ });
193
+
194
+ export function emptyState(init: { tabId: string }): ClientState {
195
+ return {
196
+ sessionId: null,
197
+ userId: null,
198
+ saltUserId: null,
199
+ ownerSecret: null,
200
+ pendingBucket: null,
201
+ tabId: init.tabId,
202
+ tabRole: 'solo',
203
+ bucketId: null,
204
+ localReady: false,
205
+ primed: false,
206
+ queries: new Map(),
207
+ registering: new Set(),
208
+ membershipReread: new Map(),
209
+ versions: new Map(),
210
+ views: new Map(),
211
+ outbox: [],
212
+ pendingWrites: new Map(),
213
+ peerWrites: new Map(),
214
+ failedCount: 0,
215
+ dirty: new Set(),
216
+ membershipDirty: new Set(),
217
+ sync: {
218
+ health: initialHealth(),
219
+ consecutiveFailures: 0,
220
+ hasSyncedOnce: false,
221
+ selfHealAttempts: 0,
222
+ pollIdleStreak: 0,
223
+ lastReconnectRefetchAt: null,
224
+ needsResubscribe: false,
225
+ fetchAttempts: 0,
226
+ liveUuid: null,
227
+ liveTable: null,
228
+ lanes: emptyLanes(),
229
+ },
230
+ };
231
+ }