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