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

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 (254) hide show
  1. package/AGENTS.md +81 -0
  2. package/README.md +56 -16
  3. package/dist/index.d.ts +2027 -137
  4. package/dist/index.js +14564 -2472
  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 +1700 -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 +366 -0
  46. package/src/client/sp00ky-client.test.ts +271 -0
  47. package/src/client/sp00ky-client.ts +541 -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 +58 -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 +519 -37
  77. package/src/modules/devtools/notify-throttle.test.ts +154 -0
  78. package/src/modules/devtools/state-shape.test.ts +149 -0
  79. package/src/modules/devtools/storage-info.test.ts +79 -0
  80. package/src/modules/devtools/storage-info.ts +168 -0
  81. package/src/modules/devtools/versions.test.ts +74 -0
  82. package/src/modules/devtools/versions.ts +110 -0
  83. package/src/modules/feature-flag/index.test.ts +251 -0
  84. package/src/modules/feature-flag/index.ts +316 -0
  85. package/src/modules/ref-tables.test.ts +91 -0
  86. package/src/modules/ref-tables.ts +88 -0
  87. package/src/modules/web-push/index.test.ts +824 -0
  88. package/src/modules/web-push/index.ts +1027 -0
  89. package/src/mutation/jobs.test.ts +25 -0
  90. package/src/mutation/jobs.ts +41 -0
  91. package/src/mutation/mutation-id.test.ts +37 -0
  92. package/src/mutation/mutation-id.ts +35 -0
  93. package/src/mutation/pending-writes.integration.test.ts +175 -0
  94. package/src/mutation/push.saga.test.ts +285 -0
  95. package/src/mutation/push.saga.ts +217 -0
  96. package/src/mutation/rows.test.ts +231 -0
  97. package/src/mutation/rows.ts +427 -0
  98. package/src/mutation/tray.saga.test.ts +63 -0
  99. package/src/mutation/tray.saga.ts +48 -0
  100. package/src/mutation/write.saga.test.ts +316 -0
  101. package/src/mutation/write.saga.ts +268 -0
  102. package/src/otel/index.ts +10 -7
  103. package/src/pure.ts +65 -0
  104. package/src/push/types.ts +174 -0
  105. package/src/query/env.test.ts +23 -0
  106. package/src/query/env.ts +56 -0
  107. package/src/query/fetch.saga.test.ts +199 -0
  108. package/src/query/fetch.saga.ts +120 -0
  109. package/src/query/hash.test.ts +14 -0
  110. package/src/query/hash.ts +18 -0
  111. package/src/query/id-set-plan.test.ts +132 -0
  112. package/src/query/lifecycle.saga.test.ts +274 -0
  113. package/src/query/lifecycle.saga.ts +159 -0
  114. package/src/query/materialize.saga.test.ts +115 -0
  115. package/src/query/materialize.saga.ts +84 -0
  116. package/src/query/materialize.test.ts +58 -0
  117. package/src/query/materialize.ts +57 -0
  118. package/src/query/membership.saga.test.ts +350 -0
  119. package/src/query/membership.saga.ts +278 -0
  120. package/src/query/membership.test.ts +118 -0
  121. package/src/query/membership.ts +201 -0
  122. package/src/query/register.saga.test.ts +293 -0
  123. package/src/query/register.saga.ts +226 -0
  124. package/src/query/render-set.test.ts +47 -0
  125. package/src/query/render-set.ts +71 -0
  126. package/src/query/sql.test.ts +63 -0
  127. package/src/query/sql.ts +93 -0
  128. package/src/query/window-query.test.ts +70 -0
  129. package/src/query/window-query.ts +195 -0
  130. package/src/services/blobs/blob-cache.test.ts +359 -0
  131. package/src/services/blobs/blob-cache.ts +603 -0
  132. package/src/services/blobs/blob-manifest.ts +227 -0
  133. package/src/services/blobs/blob-store.test.ts +77 -0
  134. package/src/services/blobs/blob-store.ts +359 -0
  135. package/src/services/blobs/blob.fixture.ts +90 -0
  136. package/src/services/blobs/index.ts +70 -0
  137. package/src/services/database/cache-engine.ts +199 -0
  138. package/src/services/database/connection-supervisor.test.ts +318 -0
  139. package/src/services/database/connection-supervisor.ts +422 -0
  140. package/src/services/database/database.query-timeout.test.ts +140 -0
  141. package/src/services/database/database.responses.test.ts +43 -0
  142. package/src/services/database/database.ts +144 -61
  143. package/src/services/database/engine-factory.ts +33 -0
  144. package/src/services/database/errors.ts +34 -0
  145. package/src/services/database/events/index.ts +2 -1
  146. package/src/services/database/index.ts +7 -0
  147. package/src/services/database/landed-body-clear.integration.test.ts +236 -0
  148. package/src/services/database/local-migrator.integration.test.ts +110 -0
  149. package/src/services/database/local-migrator.ts +59 -16
  150. package/src/services/database/local.test.ts +64 -0
  151. package/src/services/database/local.ts +487 -63
  152. package/src/services/database/plan-render.test.ts +159 -0
  153. package/src/services/database/plan-render.ts +108 -0
  154. package/src/services/database/relation-resolver.test.ts +467 -0
  155. package/src/services/database/relation-resolver.ts +0 -0
  156. package/src/services/database/remote.ts +213 -6
  157. package/src/services/database/sqlite-cache-engine.test.ts +617 -0
  158. package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
  159. package/src/services/database/sqlite-cache-engine.ts +1372 -0
  160. package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
  161. package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
  162. package/src/services/database/sqlite-lock-verify.test.ts +33 -0
  163. package/src/services/database/sqlite-lock-verify.ts +45 -0
  164. package/src/services/database/sqlite-open.test.ts +150 -0
  165. package/src/services/database/sqlite-open.ts +164 -0
  166. package/src/services/database/sqlite-plan-sql.test.ts +139 -0
  167. package/src/services/database/sqlite-plan-sql.ts +179 -0
  168. package/src/services/database/sqlite-projection.test.ts +121 -0
  169. package/src/services/database/sqlite-select.integration.test.ts +185 -0
  170. package/src/services/database/sqlite-select.test.ts +246 -0
  171. package/src/services/database/sqlite-select.ts +131 -0
  172. package/src/services/database/sqlite-transport.fixture.ts +30 -0
  173. package/src/services/database/sqlite-transport.ts +224 -0
  174. package/src/services/database/sqlite-worker.ts +437 -0
  175. package/src/services/database/surql-translate.ts +416 -0
  176. package/src/services/database/surreal-cache-engine.ts +161 -0
  177. package/src/services/logger/index.ts +3 -2
  178. package/src/services/persistence/localstorage.ts +2 -2
  179. package/src/services/persistence/resilient.ts +10 -3
  180. package/src/services/persistence/surrealdb.ts +4 -4
  181. package/src/services/stream-processor/index.ts +779 -67
  182. package/src/services/stream-processor/permissions.test.ts +47 -0
  183. package/src/services/stream-processor/permissions.ts +53 -0
  184. package/src/services/stream-processor/stream-processor.batch.test.ts +186 -0
  185. package/src/services/stream-processor/stream-processor.prime.test.ts +198 -0
  186. package/src/services/stream-processor/stream-processor.reset.test.ts +226 -0
  187. package/src/services/stream-processor/stream-processor.test.ts +1 -1
  188. package/src/services/stream-processor/wasm-types.ts +58 -2
  189. package/src/services/tabs/broker-client.ts +283 -0
  190. package/src/services/tabs/broker.test.ts +327 -0
  191. package/src/services/tabs/coordinator.test.ts +386 -0
  192. package/src/services/tabs/coordinator.ts +638 -0
  193. package/src/services/tabs/fake-ports.fixture.ts +112 -0
  194. package/src/services/tabs/leader-locks.ts +75 -0
  195. package/src/services/tabs/protocol.ts +262 -0
  196. package/src/services/tabs/support.ts +36 -0
  197. package/src/services/tabs/tabs-broker-worker.ts +640 -0
  198. package/src/sp00ky.ts +5 -395
  199. package/src/state/client-state.ts +200 -0
  200. package/src/state/lifecycle.test.ts +118 -0
  201. package/src/state/lifecycle.ts +86 -0
  202. package/src/state/reducers.test.ts +304 -0
  203. package/src/state/reducers.ts +526 -0
  204. package/src/state/selectors.test.ts +167 -0
  205. package/src/state/selectors.ts +216 -0
  206. package/src/sw/index.test.ts +582 -0
  207. package/src/sw/index.ts +607 -0
  208. package/src/sw/store.ts +96 -0
  209. package/src/sync/connection.saga.test.ts +86 -0
  210. package/src/sync/connection.saga.ts +85 -0
  211. package/src/sync/live.saga.test.ts +178 -0
  212. package/src/sync/live.saga.ts +107 -0
  213. package/src/sync/policy.health.test.ts +56 -0
  214. package/src/sync/policy.test.ts +659 -0
  215. package/src/sync/policy.ts +502 -0
  216. package/src/sync/poll.saga.test.ts +51 -0
  217. package/src/sync/poll.saga.ts +36 -0
  218. package/src/sync/tabs.saga.test.ts +78 -0
  219. package/src/sync/tabs.saga.ts +96 -0
  220. package/src/testing/adapters.ts +133 -0
  221. package/src/testing/build.ts +63 -0
  222. package/src/testing/fake-services.ts +34 -0
  223. package/src/testing/run-pure.test.ts +92 -0
  224. package/src/testing/run-pure.ts +128 -0
  225. package/src/testing/services.ts +17 -0
  226. package/src/types.ts +624 -10
  227. package/src/utils/blurhash.ts +90 -0
  228. package/src/utils/edges.ts +39 -0
  229. package/src/utils/error-classification.test.ts +54 -0
  230. package/src/utils/error-classification.ts +25 -0
  231. package/src/utils/index.ts +75 -9
  232. package/src/utils/parser.test.ts +67 -102
  233. package/src/utils/parser.ts +64 -2
  234. package/src/utils/semver.test.ts +32 -0
  235. package/src/utils/semver.ts +30 -0
  236. package/src/utils/sha256.ts +15 -0
  237. package/src/utils/surql.ts +30 -18
  238. package/src/utils/utils.test.ts +2 -1
  239. package/tsdown.config.ts +117 -3
  240. package/vitest.config.ts +48 -0
  241. package/src/modules/cache/index.ts +0 -241
  242. package/src/modules/cache/types.ts +0 -19
  243. package/src/modules/data/data.test.ts +0 -58
  244. package/src/modules/data/index.ts +0 -879
  245. package/src/modules/sync/engine.ts +0 -173
  246. package/src/modules/sync/events/index.ts +0 -77
  247. package/src/modules/sync/index.ts +0 -3
  248. package/src/modules/sync/queue/index.ts +0 -2
  249. package/src/modules/sync/queue/queue-down.ts +0 -89
  250. package/src/modules/sync/queue/queue-up.ts +0 -223
  251. package/src/modules/sync/scheduler.ts +0 -84
  252. package/src/modules/sync/sync.ts +0 -421
  253. package/src/modules/sync/utils.test.ts +0 -311
  254. package/src/modules/sync/utils.ts +0 -167
@@ -1,173 +0,0 @@
1
- import { RecordId } from 'surrealdb';
2
- import { SchemaStructure } from '@spooky-sync/query-builder';
3
- import { RemoteDatabaseService } from '../../services/database/index';
4
- import { CacheModule, CacheRecord, RecordWithId } from '../cache/index';
5
- import { RecordVersionDiff } from '../../types';
6
- import { Logger } from '../../services/logger/index';
7
- import { SyncEventTypes, createSyncEventSystem } from './events/index';
8
- import { encodeRecordId } from '../../utils/index';
9
- import { cleanRecord } from '../../utils/parser';
10
-
11
- /**
12
- * SyncEngine handles the core sync operations: fetching remote records,
13
- * caching them locally, and ingesting into DBSP.
14
- *
15
- * This is extracted from Sp00kySync to separate "how to sync" from "when to sync".
16
- */
17
- export class SyncEngine {
18
- private logger: Logger;
19
- public events = createSyncEventSystem();
20
-
21
- constructor(
22
- private remote: RemoteDatabaseService,
23
- private cache: CacheModule,
24
- private schema: SchemaStructure,
25
- logger: Logger
26
- ) {
27
- this.logger = logger.child({ service: 'Sp00kySync:SyncEngine' });
28
- }
29
-
30
- /**
31
- * Sync missing/updated/removed records between local and remote.
32
- * Main entry point for sync operations.
33
- * Uses batch processing to minimize events emitted.
34
- */
35
- async syncRecords(diff: RecordVersionDiff): Promise<void> {
36
- const { added, updated, removed } = diff;
37
-
38
- this.logger.debug(
39
- {
40
- added,
41
- updated,
42
- removed,
43
- Category: 'sp00ky-client::SyncEngine::syncRecords',
44
- },
45
- 'SyncEngine.syncRecords diff'
46
- );
47
-
48
- // Handle removed records: verify they don't exist remotely before deleting locally
49
- if (removed.length > 0) {
50
- await this.handleRemovedRecords(removed);
51
- }
52
-
53
- // Fetch added/updated records from remote
54
- const toFetch = [...added, ...updated];
55
- const idsToFetch = toFetch.map((x) => x.id);
56
- if (idsToFetch.length === 0) {
57
- return;
58
- }
59
-
60
- // Build a version map from the diff (versions come from _00_list_ref)
61
- const versionMap = new Map<string, number>();
62
- for (const item of toFetch) {
63
- versionMap.set(encodeRecordId(item.id), item.version);
64
- }
65
-
66
- // Fetch records from remote — avoid SELECT *, <subquery> FROM $param
67
- // pattern which drops the * fields in SurrealDB v3 (known bug).
68
- // Versions are already known from the diff's list_ref data.
69
- const [remoteResults] = await this.remote.query<[RecordWithId[]]>(
70
- 'SELECT * FROM $idsToFetch',
71
- { idsToFetch }
72
- );
73
-
74
- // Prepare batch for cache (which handles both DB and DBSP)
75
- const cacheBatch: CacheRecord[] = [];
76
-
77
- for (const record of remoteResults) {
78
- if (!record?.id) {
79
- this.logger.warn(
80
- {
81
- record,
82
- idsToFetch,
83
- Category: 'sp00ky-client::SyncEngine::syncRecords',
84
- },
85
- 'Remote record has no id (possibly deleted). Skipping record'
86
- );
87
- continue;
88
- }
89
- const fullId = encodeRecordId(record.id);
90
- const table = record.id.table.toString();
91
- const isAdded = added.some((item) => encodeRecordId(item.id) === fullId);
92
- const version = versionMap.get(fullId) ?? 0;
93
-
94
- const localVersion = this.cache.lookup(fullId);
95
- if (localVersion && version <= localVersion) {
96
- this.logger.info(
97
- {
98
- recordId: fullId,
99
- version,
100
- localVersion,
101
- Category: 'sp00ky-client::SyncEngine::syncRecords',
102
- },
103
- 'Local version is higher than remote version. Skipping record'
104
- );
105
- continue;
106
- }
107
- const tableSchema = this.schema.tables.find((t) => t.name === table);
108
- const cleanedRecord = tableSchema
109
- ? cleanRecord(tableSchema.columns, record)
110
- : record;
111
-
112
- cacheBatch.push({
113
- table,
114
- op: isAdded ? 'CREATE' : 'UPDATE',
115
- record: cleanedRecord as RecordWithId,
116
- version,
117
- });
118
- }
119
-
120
- // Use CacheModule to handle both local DB and DBSP ingestion
121
- if (cacheBatch.length > 0) {
122
- await this.cache.saveBatch(cacheBatch);
123
- }
124
-
125
- this.events.emit(SyncEventTypes.RemoteDataIngested, {
126
- records: remoteResults,
127
- });
128
- }
129
-
130
- /**
131
- * Handle records that exist locally but not in remote array.
132
- */
133
- private async handleRemovedRecords(removed: RecordId[]): Promise<void> {
134
- this.logger.debug(
135
- {
136
- removed: removed.map((r) => r.toString()),
137
- Category: 'sp00ky-client::SyncEngine::handleRemovedRecords',
138
- },
139
- 'Checking removed records'
140
- );
141
-
142
- let existingRemoteIds = new Set<string>();
143
- try {
144
- const [existingRemote] = await this.remote.query<[{ id: RecordId }[]]>('SELECT id FROM $ids', {
145
- ids: removed,
146
- });
147
- existingRemoteIds = new Set(existingRemote.map((r) => encodeRecordId(r.id)));
148
- } catch {
149
- // If remote check fails (e.g., SurrealDB parameter serialization issue),
150
- // proceed with deletion — the caller has already determined these should be removed
151
- this.logger.debug(
152
- { Category: 'sp00ky-client::SyncEngine::handleRemovedRecords' },
153
- 'Remote existence check failed, proceeding with deletion'
154
- );
155
- }
156
-
157
- for (const recordId of removed) {
158
- const recordIdStr = encodeRecordId(recordId);
159
- if (!existingRemoteIds.has(recordIdStr)) {
160
- this.logger.debug(
161
- {
162
- recordId: recordIdStr,
163
- Category: 'sp00ky-client::SyncEngine::handleRemovedRecords',
164
- },
165
- 'Deleting confirmed removed record'
166
- );
167
-
168
- // Use CacheModule to handle both local DB and DBSP deletion
169
- await this.cache.delete(recordId.table.name, recordIdStr);
170
- }
171
- }
172
- }
173
- }
@@ -1,77 +0,0 @@
1
- import { createEventSystem, EventDefinition, EventSystem } from '../../../events/index';
2
- import { RecordVersionArray } from '../../../types';
3
-
4
- export const SyncQueueEventTypes = {
5
- MutationEnqueued: 'MUTATION_ENQUEUED',
6
- MutationDequeued: 'MUTATION_DEQUEUED',
7
- QueryItemEnqueued: 'QUERY_ITEM_ENQUEUED',
8
- } as const;
9
-
10
- export type SyncQueueEventTypeMap = {
11
- [SyncQueueEventTypes.MutationEnqueued]: EventDefinition<
12
- typeof SyncQueueEventTypes.MutationEnqueued,
13
- { queueSize: number }
14
- >;
15
- [SyncQueueEventTypes.MutationDequeued]: EventDefinition<
16
- typeof SyncQueueEventTypes.MutationDequeued,
17
- { queueSize: number }
18
- >;
19
- [SyncQueueEventTypes.QueryItemEnqueued]: EventDefinition<
20
- typeof SyncQueueEventTypes.QueryItemEnqueued,
21
- { queueSize: number }
22
- >;
23
- };
24
-
25
- export type SyncQueueEventSystem = EventSystem<SyncQueueEventTypeMap>;
26
-
27
- export function createSyncQueueEventSystem(): SyncQueueEventSystem {
28
- return createEventSystem([
29
- SyncQueueEventTypes.QueryItemEnqueued,
30
- SyncQueueEventTypes.MutationEnqueued,
31
- SyncQueueEventTypes.MutationDequeued,
32
- ]);
33
- }
34
-
35
- export const SyncEventTypes = {
36
- QueryUpdated: 'SYNC_QUERY_UPDATED',
37
- RemoteDataIngested: 'SYNC_REMOTE_DATA_INGESTED',
38
- MutationRolledBack: 'SYNC_MUTATION_ROLLED_BACK',
39
- } as const;
40
-
41
- export type SyncEventTypeMap = {
42
- [SyncEventTypes.QueryUpdated]: EventDefinition<
43
- typeof SyncEventTypes.QueryUpdated,
44
- {
45
- queryId: any; // RecordId<string> but imported
46
- localHash?: string;
47
- localArray?: RecordVersionArray;
48
- remoteHash?: string;
49
- remoteArray?: RecordVersionArray;
50
- records: Record<string, any>[];
51
- }
52
- >;
53
- [SyncEventTypes.RemoteDataIngested]: EventDefinition<
54
- typeof SyncEventTypes.RemoteDataIngested,
55
- {
56
- records: Record<string, any>[];
57
- }
58
- >;
59
- [SyncEventTypes.MutationRolledBack]: EventDefinition<
60
- typeof SyncEventTypes.MutationRolledBack,
61
- {
62
- eventType: string;
63
- recordId: string;
64
- error: string;
65
- }
66
- >;
67
- };
68
-
69
- export type SyncEventSystem = EventSystem<SyncEventTypeMap>;
70
-
71
- export function createSyncEventSystem(): SyncEventSystem {
72
- return createEventSystem([
73
- SyncEventTypes.QueryUpdated,
74
- SyncEventTypes.RemoteDataIngested,
75
- SyncEventTypes.MutationRolledBack,
76
- ]);
77
- }
@@ -1,3 +0,0 @@
1
- export * from './events/index';
2
- export * from './sync';
3
- export * from './queue/index';
@@ -1,2 +0,0 @@
1
- export * from './queue-up';
2
- export * from './queue-down';
@@ -1,89 +0,0 @@
1
- import { LocalDatabaseService } from '../../../services/database/index';
2
- import {
3
- createSyncQueueEventSystem,
4
- SyncQueueEventSystem,
5
- SyncQueueEventTypes,
6
- } from '../events/index';
7
- import { Logger } from '../../../services/logger/index';
8
-
9
- export type RegisterEvent = {
10
- type: 'register';
11
- payload: {
12
- hash: string;
13
- };
14
- };
15
-
16
- export type SyncEvent = {
17
- type: 'sync';
18
- payload: {
19
- hash: string;
20
- };
21
- };
22
-
23
- export type HeartbeatEvent = {
24
- type: 'heartbeat';
25
- payload: {
26
- hash: string;
27
- };
28
- };
29
-
30
- export type CleanupEvent = {
31
- type: 'cleanup';
32
- payload: {
33
- hash: string;
34
- };
35
- };
36
-
37
- export type DownEvent = RegisterEvent | SyncEvent | HeartbeatEvent | CleanupEvent;
38
-
39
- export class DownQueue {
40
- private queue: DownEvent[] = [];
41
- private _events: SyncQueueEventSystem;
42
- private logger: Logger;
43
-
44
- get events(): SyncQueueEventSystem {
45
- return this._events;
46
- }
47
-
48
- constructor(
49
- private local: LocalDatabaseService,
50
- logger: Logger
51
- ) {
52
- this._events = createSyncQueueEventSystem();
53
- this.logger = logger.child({ service: 'DownQueue' });
54
- }
55
-
56
- get size(): number {
57
- return this.queue.length;
58
- }
59
-
60
- push(event: DownEvent) {
61
- this.queue.push(event);
62
- this.emitPushEvent();
63
- }
64
-
65
- private emitPushEvent() {
66
- this._events.addEvent({
67
- type: SyncQueueEventTypes.QueryItemEnqueued,
68
- payload: {
69
- queueSize: this.queue.length,
70
- },
71
- });
72
- }
73
-
74
- async next(fn: (event: DownEvent) => Promise<void>): Promise<void> {
75
- const event = this.queue.shift();
76
- if (event) {
77
- try {
78
- await fn(event);
79
- } catch (error) {
80
- this.logger.error(
81
- { error, event, Category: 'sp00ky-client::DownQueue::next' },
82
- 'Failed to process query'
83
- );
84
- this.queue.unshift(event);
85
- throw error;
86
- }
87
- }
88
- }
89
- }
@@ -1,223 +0,0 @@
1
- import { RecordId } from 'surrealdb';
2
- import { LocalDatabaseService } from '../../../services/database/index';
3
- import {
4
- createSyncQueueEventSystem,
5
- SyncQueueEventSystem,
6
- SyncQueueEventTypes,
7
- } from '../events/index';
8
- import { parseRecordIdString, extractTablePart, classifySyncError } from '../../../utils/index';
9
- import { Logger } from '../../../services/logger/index';
10
- import { PushEventOptions } from '../../../events/index';
11
-
12
- export type CreateEvent = {
13
- type: 'create';
14
- mutation_id: RecordId;
15
- record_id: RecordId;
16
- data: Record<string, unknown>;
17
- record?: Record<string, unknown>;
18
- tableName?: string;
19
- options?: PushEventOptions;
20
- };
21
-
22
- export type UpdateEvent = {
23
- type: 'update';
24
- mutation_id: RecordId;
25
- record_id: RecordId;
26
- data: Record<string, unknown>;
27
- record?: Record<string, unknown>;
28
- beforeRecord?: Record<string, unknown>;
29
- options?: PushEventOptions;
30
- };
31
-
32
- export type DeleteEvent = {
33
- type: 'delete';
34
- mutation_id: RecordId;
35
- record_id: RecordId;
36
- options?: PushEventOptions;
37
- };
38
-
39
- export type UpEvent = CreateEvent | UpdateEvent | DeleteEvent;
40
-
41
- export type RollbackCallback = (event: UpEvent, error: Error) => Promise<void>;
42
-
43
- export class UpQueue {
44
- private queue: UpEvent[] = [];
45
- private _events: SyncQueueEventSystem;
46
- private logger: Logger;
47
- private debouncedMutations: Map<string, { timer: any; firstBeforeRecord?: Record<string, unknown> }>;
48
-
49
- get events(): SyncQueueEventSystem {
50
- return this._events;
51
- }
52
-
53
- constructor(
54
- private local: LocalDatabaseService,
55
- logger: Logger
56
- ) {
57
- this._events = createSyncQueueEventSystem();
58
- this.logger = logger.child({ service: 'UpQueue' });
59
- this.debouncedMutations = new Map();
60
- }
61
-
62
- get size(): number {
63
- return this.queue.length;
64
- }
65
-
66
- push(event: UpEvent) {
67
- if (event.options?.debounced) {
68
- const { key, delay } = event.options.debounced;
69
- this.handleDebouncedMutation(event, key, delay);
70
- return;
71
- }
72
- this.addToQueue(event);
73
- }
74
-
75
- private addToQueue(event: UpEvent) {
76
- this.queue.push(event);
77
- this._events.addEvent({
78
- type: SyncQueueEventTypes.MutationEnqueued,
79
- payload: { queueSize: this.queue.length },
80
- });
81
- }
82
-
83
- private handleDebouncedMutation(event: UpEvent, key: string, delay: number) {
84
- const existing = this.debouncedMutations.get(key);
85
- let firstBeforeRecord: Record<string, unknown> | undefined;
86
-
87
- if (existing) {
88
- clearTimeout(existing.timer);
89
- // Preserve the beforeRecord from the first event in the debounce sequence
90
- firstBeforeRecord = existing.firstBeforeRecord;
91
- } else if (event.type === 'update') {
92
- firstBeforeRecord = event.beforeRecord;
93
- }
94
-
95
- const timer = setTimeout(() => {
96
- this.debouncedMutations.delete(key);
97
- // Attach the first beforeRecord to the final debounced event
98
- if (firstBeforeRecord && event.type === 'update') {
99
- event.beforeRecord = firstBeforeRecord;
100
- }
101
- this.addToQueue(event);
102
- }, delay);
103
-
104
- this.debouncedMutations.set(key, { timer, firstBeforeRecord });
105
- }
106
-
107
- async next(fn: (event: UpEvent) => Promise<void>, onRollback?: RollbackCallback): Promise<void> {
108
- const event = this.queue.shift();
109
- if (event) {
110
- try {
111
- await fn(event);
112
- } catch (error) {
113
- const errorType = classifySyncError(error);
114
-
115
- if (errorType === 'network') {
116
- this.logger.error(
117
- { error, event, Category: 'sp00ky-client::UpQueue::next' },
118
- 'Network error processing mutation, re-queuing'
119
- );
120
- this.queue.unshift(event);
121
- throw error;
122
- }
123
-
124
- // Application error — rollback instead of re-queuing
125
- this.logger.error(
126
- { error, event, Category: 'sp00ky-client::UpQueue::next' },
127
- 'Application error processing mutation, rolling back'
128
- );
129
- try {
130
- await this.removeEventFromDatabase(event.mutation_id);
131
- } catch (removeError) {
132
- this.logger.error(
133
- { error: removeError, event, Category: 'sp00ky-client::UpQueue::next' },
134
- 'Failed to remove rolled-back mutation from database'
135
- );
136
- }
137
- if (onRollback) {
138
- try {
139
- await onRollback(event, error instanceof Error ? error : new Error(String(error)));
140
- } catch (rollbackError) {
141
- this.logger.error(
142
- { error: rollbackError, event, Category: 'sp00ky-client::UpQueue::next' },
143
- 'Rollback handler failed'
144
- );
145
- }
146
- }
147
- this._events.addEvent({
148
- type: SyncQueueEventTypes.MutationDequeued,
149
- payload: { queueSize: this.queue.length },
150
- });
151
- return;
152
- }
153
- try {
154
- await this.removeEventFromDatabase(event.mutation_id);
155
- } catch (error) {
156
- this.logger.error(
157
- { error, event, Category: 'sp00ky-client::UpQueue::next' },
158
- 'Failed to remove mutation from database after successful processing'
159
- );
160
- }
161
- this._events.addEvent({
162
- type: SyncQueueEventTypes.MutationDequeued,
163
- payload: { queueSize: this.queue.length },
164
- });
165
- }
166
- }
167
-
168
- async removeEventFromDatabase(mutation_id: RecordId) {
169
- return this.local.query(`DELETE $mutation_id`, { mutation_id });
170
- }
171
-
172
- async loadFromDatabase() {
173
- try {
174
- const [records] = await this.local.query<any>(
175
- `SELECT * FROM _00_pending_mutations ORDER BY created_at ASC`
176
- );
177
-
178
- this.queue = records
179
- .map((r: any): UpEvent | null => {
180
- switch (r.mutationType) {
181
- case 'create':
182
- return {
183
- type: 'create',
184
- mutation_id: parseRecordIdString(r.id),
185
- record_id: parseRecordIdString(r.recordId),
186
- data: r.data,
187
- tableName: extractTablePart(r.recordId),
188
- };
189
- case 'update':
190
- return {
191
- type: 'update',
192
- mutation_id: parseRecordIdString(r.id),
193
- record_id: parseRecordIdString(r.recordId),
194
- data: r.data,
195
- beforeRecord: r.beforeRecord,
196
- };
197
- case 'delete':
198
- return {
199
- type: 'delete',
200
- mutation_id: parseRecordIdString(r.id),
201
- record_id: parseRecordIdString(r.recordId),
202
- };
203
- default:
204
- this.logger.warn(
205
- {
206
- mutationType: r.mutationType,
207
- record: r,
208
- Category: 'sp00ky-client::UpQueue::loadFromDatabase',
209
- },
210
- 'Unknown mutation type'
211
- );
212
- return null;
213
- }
214
- })
215
- .filter((e: UpEvent | null): e is UpEvent => e !== null);
216
- } catch (error) {
217
- this.logger.error(
218
- { error, Category: 'sp00ky-client::UpQueue::loadFromDatabase' },
219
- 'Failed to load pending mutations from database'
220
- );
221
- }
222
- }
223
- }
@@ -1,84 +0,0 @@
1
- import { Logger } from '../../services/logger/index';
2
- import { UpQueue, DownQueue, DownEvent, UpEvent, RollbackCallback } from './queue/index';
3
- import { SyncQueueEventTypes } from './events/index';
4
-
5
- /**
6
- * SyncScheduler manages when to sync: queue management and orchestration.
7
- * Decides the order and timing of sync operations.
8
- */
9
- export class SyncScheduler {
10
- private isSyncingUp: boolean = false;
11
- private isSyncingDown: boolean = false;
12
-
13
- constructor(
14
- private upQueue: UpQueue,
15
- private downQueue: DownQueue,
16
- private onProcessUp: (event: UpEvent) => Promise<void>,
17
- private onProcessDown: (event: DownEvent) => Promise<void>,
18
- private logger: Logger,
19
- private onRollback?: RollbackCallback
20
- ) {}
21
-
22
- async init() {
23
- await this.upQueue.loadFromDatabase();
24
- this.upQueue.events.subscribe(SyncQueueEventTypes.MutationEnqueued, this.syncUp.bind(this));
25
- this.downQueue.events.subscribe(
26
- SyncQueueEventTypes.QueryItemEnqueued,
27
- this.syncDown.bind(this)
28
- );
29
- }
30
-
31
- /**
32
- * Add mutations to the upload queue
33
- */
34
- enqueueMutation(mutations: UpEvent[]) {
35
- for (const mutation of mutations) {
36
- this.upQueue.push(mutation);
37
- }
38
- }
39
-
40
- /**
41
- * Add query events to the download queue
42
- */
43
- enqueueDownEvent(event: DownEvent) {
44
- this.downQueue.push(event);
45
- }
46
-
47
- /**
48
- * Process upload queue
49
- */
50
- async syncUp() {
51
- if (this.isSyncingUp) return;
52
- this.isSyncingUp = true;
53
- try {
54
- while (this.upQueue.size > 0) {
55
- await this.upQueue.next(this.onProcessUp, this.onRollback);
56
- }
57
- } finally {
58
- this.isSyncingUp = false;
59
- void this.syncDown();
60
- }
61
- }
62
-
63
- /**
64
- * Process download queue
65
- */
66
- async syncDown() {
67
- if (this.isSyncingDown) return;
68
- if (this.upQueue.size > 0) return;
69
-
70
- this.isSyncingDown = true;
71
- try {
72
- while (this.downQueue.size > 0) {
73
- if (this.upQueue.size > 0) break;
74
- await this.downQueue.next(this.onProcessDown);
75
- }
76
- } finally {
77
- this.isSyncingDown = false;
78
- }
79
- }
80
-
81
- get isSyncing() {
82
- return this.isSyncingUp || this.isSyncingDown;
83
- }
84
- }