@spooky-sync/core 0.0.1-canary.32 → 0.0.1-canary.321
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.
- package/AGENTS.md +81 -0
- package/README.md +56 -16
- package/dist/index.d.ts +2034 -137
- package/dist/index.js +15236 -2467
- package/dist/live.d.ts +141 -0
- package/dist/live.js +733 -0
- package/dist/otel/index.d.ts +1 -1
- package/dist/otel/index.js +3 -3
- package/dist/pure.d.ts +365 -0
- package/dist/pure.js +24 -0
- package/dist/sqlite-open.js +379 -0
- package/dist/sqlite-worker.d.ts +1 -0
- package/dist/sqlite-worker.js +439 -0
- package/dist/sw.d.ts +207 -0
- package/dist/sw.js +393 -0
- package/dist/tabs-broker-worker.d.ts +8 -0
- package/dist/tabs-broker-worker.js +472 -0
- package/dist/types.d.ts +1821 -38
- package/dist/worker-edges.js +470 -0
- package/dist/worker-impersonation.js +60 -0
- package/dist/worker-ref-tables.d.ts +48 -0
- package/dist/worker-types.d.ts +112 -0
- package/dist/worker-types.js +73 -0
- package/package.json +27 -7
- package/scripts/check-broker-bundle.mjs +33 -0
- package/scripts/check-worker-entries.mjs +172 -0
- package/skills/sp00ky-core/SKILL.md +31 -6
- package/skills/sp00ky-core/references/config.md +6 -0
- package/src/boot/auth-flip.saga.test.ts +135 -0
- package/src/boot/auth-flip.saga.ts +77 -0
- package/src/boot/boot.saga.test.ts +180 -0
- package/src/boot/boot.saga.ts +142 -0
- package/src/boot/bucket-switch.saga.test.ts +187 -0
- package/src/boot/bucket-switch.saga.ts +133 -0
- package/src/boot/preload.saga.test.ts +51 -0
- package/src/boot/preload.saga.ts +31 -0
- package/src/bucket-blurhash.test.ts +148 -0
- package/src/build-globals.d.ts +12 -0
- package/src/client/bucket-handle.test.ts +117 -0
- package/src/client/bucket-handle.ts +227 -0
- package/src/client/router.test.ts +58 -0
- package/src/client/router.ts +100 -0
- package/src/client/runtime.test.ts +195 -0
- package/src/client/runtime.ts +296 -0
- package/src/client/services.test.ts +60 -0
- package/src/client/services.ts +418 -0
- package/src/client/sp00ky-client.test.ts +303 -0
- package/src/client/sp00ky-client.ts +586 -0
- package/src/events/events.test.ts +2 -1
- package/src/events/index.ts +3 -0
- package/src/index.ts +69 -1
- package/src/kernel/constants.test.ts +19 -0
- package/src/kernel/constants.ts +80 -0
- package/src/kernel/effects.test.ts +40 -0
- package/src/kernel/effects.ts +163 -0
- package/src/kernel/events.ts +57 -0
- package/src/kernel/interpreter.test.ts +116 -0
- package/src/kernel/interpreter.ts +148 -0
- package/src/kernel/saga.test.ts +89 -0
- package/src/kernel/saga.ts +73 -0
- package/src/live/index.test.ts +501 -0
- package/src/live/index.ts +1039 -0
- package/src/modules/app-release/index.test.ts +125 -0
- package/src/modules/app-release/index.ts +209 -0
- package/src/modules/auth/auth.local-first.test.ts +169 -0
- package/src/modules/auth/events/index.ts +9 -2
- package/src/modules/auth/impersonation-banner.ts +443 -0
- package/src/modules/auth/impersonation.integration.test.ts +205 -0
- package/src/modules/auth/impersonation.test.ts +377 -0
- package/src/modules/auth/impersonation.ts +179 -0
- package/src/modules/auth/index.ts +466 -19
- package/src/modules/crdt/crdt-field.ts +294 -0
- package/src/modules/crdt/crdt-hydration.test.ts +210 -0
- package/src/modules/crdt/crdt-reconnect.test.ts +195 -0
- package/src/modules/crdt/index.ts +463 -0
- package/src/modules/crdt/loro-loader.ts +25 -0
- package/src/modules/devtools/flags.ts +349 -0
- package/src/modules/devtools/index.ts +746 -95
- package/src/modules/devtools/mutations.test.ts +92 -0
- package/src/modules/devtools/mutations.ts +160 -0
- package/src/modules/devtools/notify-throttle.test.ts +166 -0
- package/src/modules/devtools/state-shape.test.ts +263 -0
- package/src/modules/devtools/storage-info.test.ts +79 -0
- package/src/modules/devtools/storage-info.ts +168 -0
- package/src/modules/devtools/versions.test.ts +74 -0
- package/src/modules/devtools/versions.ts +110 -0
- package/src/modules/feature-flag/index.test.ts +298 -0
- package/src/modules/feature-flag/index.ts +316 -0
- package/src/modules/ref-tables.test.ts +93 -0
- package/src/modules/ref-tables.ts +91 -0
- package/src/modules/web-push/index.test.ts +824 -0
- package/src/modules/web-push/index.ts +1027 -0
- package/src/mutation/jobs.test.ts +25 -0
- package/src/mutation/jobs.ts +41 -0
- package/src/mutation/mutation-id.test.ts +37 -0
- package/src/mutation/mutation-id.ts +35 -0
- package/src/mutation/pending-writes.integration.test.ts +175 -0
- package/src/mutation/push.saga.test.ts +285 -0
- package/src/mutation/push.saga.ts +217 -0
- package/src/mutation/rows.test.ts +231 -0
- package/src/mutation/rows.ts +427 -0
- package/src/mutation/tray.saga.test.ts +63 -0
- package/src/mutation/tray.saga.ts +48 -0
- package/src/mutation/write.saga.test.ts +316 -0
- package/src/mutation/write.saga.ts +268 -0
- package/src/otel/index.ts +10 -7
- package/src/pure.ts +66 -0
- package/src/push/types.ts +174 -0
- package/src/query/anon-query-isolation.integration.test.ts +337 -0
- package/src/query/env.test.ts +23 -0
- package/src/query/env.ts +56 -0
- package/src/query/fetch.saga.test.ts +199 -0
- package/src/query/fetch.saga.ts +120 -0
- package/src/query/hash.test.ts +14 -0
- package/src/query/hash.ts +18 -0
- package/src/query/id-set-plan.test.ts +132 -0
- package/src/query/lifecycle.saga.test.ts +274 -0
- package/src/query/lifecycle.saga.ts +159 -0
- package/src/query/list-ref-live.integration.test.ts +203 -0
- package/src/query/materialize.saga.test.ts +115 -0
- package/src/query/materialize.saga.ts +84 -0
- package/src/query/materialize.test.ts +58 -0
- package/src/query/materialize.ts +57 -0
- package/src/query/membership.saga.test.ts +350 -0
- package/src/query/membership.saga.ts +278 -0
- package/src/query/membership.test.ts +118 -0
- package/src/query/membership.ts +201 -0
- package/src/query/register.saga.test.ts +311 -0
- package/src/query/register.saga.ts +229 -0
- package/src/query/render-set.test.ts +47 -0
- package/src/query/render-set.ts +71 -0
- package/src/query/sql.test.ts +112 -0
- package/src/query/sql.ts +149 -0
- package/src/query/window-query.test.ts +70 -0
- package/src/query/window-query.ts +195 -0
- package/src/services/blobs/blob-cache.test.ts +359 -0
- package/src/services/blobs/blob-cache.ts +603 -0
- package/src/services/blobs/blob-manifest.ts +227 -0
- package/src/services/blobs/blob-store.test.ts +77 -0
- package/src/services/blobs/blob-store.ts +359 -0
- package/src/services/blobs/blob.fixture.ts +90 -0
- package/src/services/blobs/index.ts +70 -0
- package/src/services/database/cache-engine.ts +199 -0
- package/src/services/database/connection-supervisor.test.ts +318 -0
- package/src/services/database/connection-supervisor.ts +422 -0
- package/src/services/database/database.query-timeout.test.ts +140 -0
- package/src/services/database/database.responses.test.ts +43 -0
- package/src/services/database/database.ts +146 -61
- package/src/services/database/engine-factory.ts +33 -0
- package/src/services/database/errors.ts +34 -0
- package/src/services/database/events/index.ts +2 -1
- package/src/services/database/index.ts +7 -0
- package/src/services/database/landed-body-clear.integration.test.ts +236 -0
- package/src/services/database/local-migrator.integration.test.ts +110 -0
- package/src/services/database/local-migrator.ts +59 -16
- package/src/services/database/local.test.ts +64 -0
- package/src/services/database/local.ts +487 -63
- package/src/services/database/plan-render.test.ts +159 -0
- package/src/services/database/plan-render.ts +108 -0
- package/src/services/database/relation-resolver.test.ts +484 -0
- package/src/services/database/relation-resolver.ts +0 -0
- package/src/services/database/remote.ts +273 -6
- package/src/services/database/sqlite-cache-engine.test.ts +617 -0
- package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
- package/src/services/database/sqlite-cache-engine.ts +1372 -0
- package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
- package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
- package/src/services/database/sqlite-lock-verify.test.ts +33 -0
- package/src/services/database/sqlite-lock-verify.ts +45 -0
- package/src/services/database/sqlite-open.test.ts +150 -0
- package/src/services/database/sqlite-open.ts +164 -0
- package/src/services/database/sqlite-plan-sql.test.ts +145 -0
- package/src/services/database/sqlite-plan-sql.ts +182 -0
- package/src/services/database/sqlite-projection.test.ts +121 -0
- package/src/services/database/sqlite-select.integration.test.ts +185 -0
- package/src/services/database/sqlite-select.test.ts +261 -0
- package/src/services/database/sqlite-select.ts +131 -0
- package/src/services/database/sqlite-transport.fixture.ts +30 -0
- package/src/services/database/sqlite-transport.ts +224 -0
- package/src/services/database/sqlite-worker.ts +437 -0
- package/src/services/database/surql-translate.ts +416 -0
- package/src/services/database/surreal-cache-engine.ts +161 -0
- package/src/services/logger/index.test.ts +132 -0
- package/src/services/logger/index.ts +189 -7
- package/src/services/persistence/localstorage.ts +2 -2
- package/src/services/persistence/resilient.ts +10 -3
- package/src/services/persistence/surrealdb.ts +4 -4
- package/src/services/stream-processor/index.ts +783 -68
- package/src/services/stream-processor/permissions.test.ts +47 -0
- package/src/services/stream-processor/permissions.ts +53 -0
- package/src/services/stream-processor/stream-processor.batch.test.ts +186 -0
- package/src/services/stream-processor/stream-processor.prime.test.ts +198 -0
- package/src/services/stream-processor/stream-processor.reset.test.ts +226 -0
- package/src/services/stream-processor/stream-processor.test.ts +1 -1
- package/src/services/stream-processor/wasm-types.ts +58 -2
- package/src/services/tabs/broker-client.ts +283 -0
- package/src/services/tabs/broker.test.ts +327 -0
- package/src/services/tabs/coordinator.test.ts +386 -0
- package/src/services/tabs/coordinator.ts +638 -0
- package/src/services/tabs/fake-ports.fixture.ts +112 -0
- package/src/services/tabs/leader-locks.ts +75 -0
- package/src/services/tabs/protocol.ts +262 -0
- package/src/services/tabs/support.ts +36 -0
- package/src/services/tabs/tabs-broker-worker.ts +640 -0
- package/src/sp00ky.ts +5 -395
- package/src/state/client-state.ts +200 -0
- package/src/state/lifecycle.test.ts +141 -0
- package/src/state/lifecycle.ts +99 -0
- package/src/state/reducers.test.ts +304 -0
- package/src/state/reducers.ts +526 -0
- package/src/state/selectors.test.ts +175 -0
- package/src/state/selectors.ts +222 -0
- package/src/sw/index.test.ts +582 -0
- package/src/sw/index.ts +607 -0
- package/src/sw/store.ts +96 -0
- package/src/sync/connection.saga.test.ts +97 -0
- package/src/sync/connection.saga.ts +89 -0
- package/src/sync/live.saga.test.ts +178 -0
- package/src/sync/live.saga.ts +107 -0
- package/src/sync/policy.health.test.ts +56 -0
- package/src/sync/policy.test.ts +663 -0
- package/src/sync/policy.ts +515 -0
- package/src/sync/poll.saga.test.ts +51 -0
- package/src/sync/poll.saga.ts +36 -0
- package/src/sync/tabs.saga.test.ts +78 -0
- package/src/sync/tabs.saga.ts +96 -0
- package/src/testing/adapters.ts +133 -0
- package/src/testing/build.ts +63 -0
- package/src/testing/fake-services.ts +48 -0
- package/src/testing/run-pure.test.ts +92 -0
- package/src/testing/run-pure.ts +128 -0
- package/src/testing/services.ts +17 -0
- package/src/types.ts +624 -10
- package/src/utils/blurhash.ts +90 -0
- package/src/utils/edges.ts +39 -0
- package/src/utils/error-classification.test.ts +54 -0
- package/src/utils/error-classification.ts +25 -0
- package/src/utils/index.ts +78 -12
- package/src/utils/parser.test.ts +67 -102
- package/src/utils/parser.ts +64 -2
- package/src/utils/semver.test.ts +32 -0
- package/src/utils/semver.ts +30 -0
- package/src/utils/sha256.ts +15 -0
- package/src/utils/surql.ts +30 -18
- package/src/utils/utils.test.ts +3 -2
- package/tsdown.config.ts +117 -3
- package/vitest.config.ts +48 -0
- package/src/modules/cache/index.ts +0 -241
- package/src/modules/cache/types.ts +0 -19
- package/src/modules/data/data.test.ts +0 -58
- package/src/modules/data/index.ts +0 -879
- package/src/modules/sync/engine.ts +0 -173
- package/src/modules/sync/events/index.ts +0 -77
- package/src/modules/sync/index.ts +0 -3
- package/src/modules/sync/queue/index.ts +0 -2
- package/src/modules/sync/queue/queue-down.ts +0 -89
- package/src/modules/sync/queue/queue-up.ts +0 -223
- package/src/modules/sync/scheduler.ts +0 -84
- package/src/modules/sync/sync.ts +0 -421
- package/src/modules/sync/utils.test.ts +0 -311
- 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,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
|
-
}
|