@spooky-sync/core 0.0.1-canary.34 → 0.0.1-canary.341
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 +90 -0
- package/README.md +59 -16
- package/dist/index.d.ts +2117 -142
- package/dist/index.js +16436 -2575
- package/dist/live.d.ts +149 -0
- package/dist/live.js +739 -0
- package/dist/otel/index.d.ts +1 -1
- package/dist/otel/index.js +3 -3
- package/dist/pure.d.ts +389 -0
- package/dist/pure.js +24 -0
- package/dist/sqlite-open.js +392 -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 +477 -0
- package/dist/types.d.ts +1917 -38
- package/dist/worker-edges.js +480 -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 +28 -7
- package/scripts/check-broker-bundle.mjs +33 -0
- package/scripts/check-worker-entries.mjs +173 -0
- package/skills/sp00ky-core/SKILL.md +37 -7
- package/skills/sp00ky-core/references/config.md +7 -1
- 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 +203 -0
- package/src/boot/boot.saga.ts +146 -0
- package/src/boot/bucket-switch.saga.test.ts +189 -0
- package/src/boot/bucket-switch.saga.ts +136 -0
- package/src/boot/preload.saga.test.ts +96 -0
- package/src/boot/preload.saga.ts +45 -0
- package/src/bucket-blurhash.test.ts +148 -0
- package/src/build-globals.d.ts +12 -0
- package/src/client/bucket-handle.test.ts +195 -0
- package/src/client/bucket-handle.ts +352 -0
- package/src/client/router.test.ts +58 -0
- package/src/client/router.ts +100 -0
- package/src/client/runtime.test.ts +235 -0
- package/src/client/runtime.ts +296 -0
- package/src/client/services.test.ts +74 -0
- package/src/client/services.ts +436 -0
- package/src/client/sp00ky-client.test.ts +341 -0
- package/src/client/sp00ky-client.ts +625 -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 +169 -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 +512 -0
- package/src/live/index.ts +1059 -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 +228 -0
- package/src/modules/auth/auth.session-recheck.test.ts +140 -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 +543 -22
- package/src/modules/crdt/acceptance.ts +75 -0
- package/src/modules/crdt/bytes.ts +50 -0
- package/src/modules/crdt/crdt-field.ts +125 -0
- package/src/modules/crdt/crdt.test.ts +515 -0
- package/src/modules/crdt/hash.ts +103 -0
- package/src/modules/crdt/index.ts +793 -0
- package/src/modules/crdt/loro-loader.ts +25 -0
- package/src/modules/crdt/mirror.ts +37 -0
- package/src/modules/devtools/flags.ts +349 -0
- package/src/modules/devtools/index.ts +748 -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/ingest-outage.integration.test.ts +400 -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 +361 -0
- package/src/mutation/push.saga.ts +245 -0
- package/src/mutation/rows.test.ts +278 -0
- package/src/mutation/rows.ts +499 -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 +531 -0
- package/src/query/env.test.ts +23 -0
- package/src/query/env.ts +65 -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 +186 -0
- package/src/query/list-ref-live.integration.test.ts +203 -0
- package/src/query/materialize.saga.test.ts +188 -0
- package/src/query/materialize.saga.ts +96 -0
- package/src/query/materialize.test.ts +58 -0
- package/src/query/materialize.ts +57 -0
- package/src/query/membership.saga.test.ts +482 -0
- package/src/query/membership.saga.ts +315 -0
- package/src/query/membership.test.ts +151 -0
- package/src/query/membership.ts +234 -0
- package/src/query/register.saga.test.ts +312 -0
- package/src/query/register.saga.ts +235 -0
- package/src/query/render-set.test.ts +47 -0
- package/src/query/render-set.ts +71 -0
- package/src/query/session-guard.integration.test.ts +304 -0
- package/src/query/sql.test.ts +118 -0
- package/src/query/sql.ts +157 -0
- package/src/query/window-query.test.ts +70 -0
- package/src/query/window-query.ts +208 -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 +209 -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 +156 -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 +61 -16
- package/src/services/database/local.test.ts +150 -0
- package/src/services/database/local.ts +508 -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 +285 -6
- package/src/services/database/sqlite-cache-engine.test.ts +628 -0
- package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
- package/src/services/database/sqlite-cache-engine.ts +1401 -0
- package/src/services/database/sqlite-column-reset.integration.test.ts +100 -0
- package/src/services/database/sqlite-column-reset.ts +91 -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 +164 -0
- package/src/services/database/sqlite-plan-sql.ts +196 -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 +417 -0
- package/src/services/database/surreal-cache-engine.ts +163 -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 +806 -67
- 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 +234 -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 +284 -0
- package/src/services/tabs/broker.test.ts +381 -0
- package/src/services/tabs/coordinator.test.ts +386 -0
- package/src/services/tabs/coordinator.ts +646 -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 +652 -0
- package/src/sp00ky.ts +5 -395
- package/src/state/client-state.ts +216 -0
- package/src/state/lifecycle.test.ts +184 -0
- package/src/state/lifecycle.ts +127 -0
- package/src/state/reducers.test.ts +319 -0
- package/src/state/reducers.ts +550 -0
- package/src/state/selectors.test.ts +188 -0
- package/src/state/selectors.ts +259 -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 +229 -0
- package/src/sync/live.saga.ts +116 -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 +137 -0
- package/src/testing/build.ts +63 -0
- package/src/testing/fake-services.ts +63 -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 +661 -10
- package/src/utils/blurhash.ts +90 -0
- package/src/utils/edges.ts +57 -0
- package/src/utils/error-classification.test.ts +105 -0
- package/src/utils/error-classification.ts +64 -0
- package/src/utils/index.ts +89 -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 +29 -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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type * as Loro from 'loro-crdt';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Which edit-log rows may enter a document.
|
|
5
|
+
*
|
|
6
|
+
* Every client and the SSP fold (`packages/ssp-node/src/crdt/acceptance.rs`)
|
|
7
|
+
* apply this exact rule before importing a row, so they import the same rows
|
|
8
|
+
* and converge on the same document: a row one side accepted and another
|
|
9
|
+
* refused would leave them different forever (a CRDT never forgets an op).
|
|
10
|
+
*
|
|
11
|
+
* - the blob decodes as a Loro **update** (a snapshot could smuggle in
|
|
12
|
+
* arbitrary history from any peer);
|
|
13
|
+
* - it is no larger than the column allows;
|
|
14
|
+
* - every op in it was made by the row's `peer`, which the database already
|
|
15
|
+
* checked belongs to the row's author (`_00_crdt_peer`).
|
|
16
|
+
*
|
|
17
|
+
* Loro's decoder can trap on crafted input with a valid checksum, so a raw
|
|
18
|
+
* header check rejects non-update blobs before decoding, and decoding runs
|
|
19
|
+
* under `try`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export type Rejection =
|
|
23
|
+
| 'empty'
|
|
24
|
+
| 'too_large'
|
|
25
|
+
| 'bad_peer'
|
|
26
|
+
| 'header'
|
|
27
|
+
| 'undecodable'
|
|
28
|
+
| 'not_an_update'
|
|
29
|
+
| 'no_ops'
|
|
30
|
+
| 'foreign_peer';
|
|
31
|
+
|
|
32
|
+
export interface Accepted {
|
|
33
|
+
peer: `${number}`;
|
|
34
|
+
/** Exclusive end counter of the row's ops for `peer`. */
|
|
35
|
+
end: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type Verdict = { ok: true; accepted: Accepted } | { ok: false; reason: Rejection };
|
|
39
|
+
|
|
40
|
+
/** The largest blob a row may carry (the generated column's cap). */
|
|
41
|
+
export const MAX_DELTA_BYTES = 256 * 1024;
|
|
42
|
+
|
|
43
|
+
const PEER = /^\d{1,20}$/;
|
|
44
|
+
/** `"loro"`: every Loro blob starts with it. */
|
|
45
|
+
const MAGIC = [0x6c, 0x6f, 0x72, 0x6f];
|
|
46
|
+
/** Header bytes 20..22 (big-endian) carry the encoding mode; 4 is an update. */
|
|
47
|
+
const MODE_UPDATES = 4;
|
|
48
|
+
|
|
49
|
+
export function check(
|
|
50
|
+
loro: Pick<typeof Loro, 'decodeImportBlobMeta'>,
|
|
51
|
+
blob: Uint8Array | undefined,
|
|
52
|
+
peer: unknown,
|
|
53
|
+
maxBytes = MAX_DELTA_BYTES
|
|
54
|
+
): Verdict {
|
|
55
|
+
if (!blob || blob.length === 0) return { ok: false, reason: 'empty' };
|
|
56
|
+
if (blob.length > maxBytes) return { ok: false, reason: 'too_large' };
|
|
57
|
+
if (typeof peer !== 'string' || !PEER.test(peer)) return { ok: false, reason: 'bad_peer' };
|
|
58
|
+
if (blob.length < 22 || MAGIC.some((b, i) => blob[i] !== b)) return { ok: false, reason: 'header' };
|
|
59
|
+
if (((blob[20] << 8) | blob[21]) !== MODE_UPDATES) return { ok: false, reason: 'not_an_update' };
|
|
60
|
+
let meta: ReturnType<typeof Loro.decodeImportBlobMeta>;
|
|
61
|
+
try {
|
|
62
|
+
meta = loro.decodeImportBlobMeta(blob, true);
|
|
63
|
+
} catch {
|
|
64
|
+
return { ok: false, reason: 'undecodable' };
|
|
65
|
+
}
|
|
66
|
+
if (meta.mode !== 'update') return { ok: false, reason: 'not_an_update' };
|
|
67
|
+
const start = meta.partialStartVersionVector.toJSON();
|
|
68
|
+
const authors: Array<[string, number]> = [];
|
|
69
|
+
for (const [p, end] of meta.partialEndVersionVector.toJSON()) {
|
|
70
|
+
if (end > (start.get(p) ?? 0)) authors.push([String(p), end]);
|
|
71
|
+
}
|
|
72
|
+
if (authors.length === 0) return { ok: false, reason: 'no_ops' };
|
|
73
|
+
if (authors.length > 1 || BigInt(authors[0][0]) !== BigInt(peer)) return { ok: false, reason: 'foreign_peer' };
|
|
74
|
+
return { ok: true, accepted: { peer: peer as `${number}`, end: authors[0][1] } };
|
|
75
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte columns reach the client in several shapes depending on the engine and
|
|
3
|
+
* the path: `Uint8Array` (SQLite, a fresh local write), `ArrayBuffer` (the
|
|
4
|
+
* surrealdb SDK's CBOR decode), a typed-array view, or a `number[]` (bytes
|
|
5
|
+
* nested in a FLEXIBLE object on the SurrealDB-WASM engine). Normalize all of
|
|
6
|
+
* them; anything else is not bytes.
|
|
7
|
+
*/
|
|
8
|
+
export function asBytes(value: unknown): Uint8Array | undefined {
|
|
9
|
+
if (value instanceof Uint8Array) return value;
|
|
10
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
11
|
+
if (ArrayBuffer.isView(value)) {
|
|
12
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(value) && value.length > 0 && value.every((n) => typeof n === 'number')) {
|
|
15
|
+
return Uint8Array.from(value as number[]);
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function decodeBase64(b64: string): Uint8Array {
|
|
21
|
+
if (typeof atob === 'function') {
|
|
22
|
+
const binary = atob(b64);
|
|
23
|
+
const bytes = new Uint8Array(binary.length);
|
|
24
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
25
|
+
return bytes;
|
|
26
|
+
}
|
|
27
|
+
return new Uint8Array(Buffer.from(b64, 'base64'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function encodeBase64(bytes: Uint8Array): string {
|
|
31
|
+
if (typeof btoa === 'function') {
|
|
32
|
+
let binary = '';
|
|
33
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
34
|
+
return btoa(binary);
|
|
35
|
+
}
|
|
36
|
+
return Buffer.from(bytes).toString('base64');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A random Loro peer id below 2^53 (so it is exact as a JS number), as the
|
|
41
|
+
* decimal string Loro uses for `PeerID`. Each tab picks a new one per session
|
|
42
|
+
* and claims it in `_00_crdt_peer` before any of its edits are sent.
|
|
43
|
+
*/
|
|
44
|
+
export function randomPeer(): `${number}` {
|
|
45
|
+
const words = new Uint32Array(2);
|
|
46
|
+
crypto.getRandomValues(words);
|
|
47
|
+
const high = words[0] & 0x1fffff; // 21 bits
|
|
48
|
+
const id = high * 0x100000000 + words[1];
|
|
49
|
+
return String(id === 0 ? 1 : id) as `${number}`;
|
|
50
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { LoroDoc } from 'loro-crdt';
|
|
2
|
+
import type { CrdtKind } from './mirror';
|
|
3
|
+
|
|
4
|
+
// ==================== CURSOR UTILITIES ====================
|
|
5
|
+
|
|
6
|
+
export const CURSOR_COLORS = [
|
|
7
|
+
'#3b82f6', '#ef4444', '#22c55e', '#f59e0b',
|
|
8
|
+
'#8b5cf6', '#ec4899', '#14b8a6', '#f97316',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export function cursorColorFromName(name: string): string {
|
|
12
|
+
let hash = 0;
|
|
13
|
+
for (let i = 0; i < name.length; i++) {
|
|
14
|
+
hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0;
|
|
15
|
+
}
|
|
16
|
+
return CURSOR_COLORS[Math.abs(hash) % CURSOR_COLORS.length];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { decodeBase64, encodeBase64 } from './bytes';
|
|
20
|
+
|
|
21
|
+
// ==================== CRDT FIELD ====================
|
|
22
|
+
|
|
23
|
+
/** What a field handle needs from the manager that owns its session. */
|
|
24
|
+
export interface CrdtFieldHost {
|
|
25
|
+
pushPresence(state: Uint8Array): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* An open collaborative field: the Loro document an editor binds to. Edits
|
|
30
|
+
* made on it are sent as the client's own edit-log rows; other clients' rows
|
|
31
|
+
* are imported into it as they arrive. Every open of the same field in one tab
|
|
32
|
+
* shares this document.
|
|
33
|
+
*/
|
|
34
|
+
export class CrdtField {
|
|
35
|
+
private _onCursorUpdate: ((data: Uint8Array) => void) | null = null;
|
|
36
|
+
private pendingCursorUpdate: Uint8Array | null = null;
|
|
37
|
+
private loaded = false;
|
|
38
|
+
private loadedListeners = new Set<() => void>();
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
readonly table: string,
|
|
42
|
+
readonly recordId: string,
|
|
43
|
+
readonly fieldName: string,
|
|
44
|
+
readonly kind: CrdtKind,
|
|
45
|
+
readonly cursorsEnabled: boolean,
|
|
46
|
+
private readonly doc: LoroDoc,
|
|
47
|
+
private readonly host: CrdtFieldHost
|
|
48
|
+
) {}
|
|
49
|
+
|
|
50
|
+
getDoc(): LoroDoc {
|
|
51
|
+
return this.doc;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Whether the document holds anything yet (local state or edits). */
|
|
55
|
+
hasContent(): boolean {
|
|
56
|
+
return this.doc.oplogVersion().length() > 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether the document is known: this device had it, it was handed a draft,
|
|
61
|
+
* or the server answered (with content, or that it has none yet). Until then
|
|
62
|
+
* an empty document means "not loaded", not "empty": show the column's text
|
|
63
|
+
* instead, and do not bind an editor (an editor binding an empty document
|
|
64
|
+
* creates structure that competes with the real one).
|
|
65
|
+
*/
|
|
66
|
+
isLoaded(): boolean {
|
|
67
|
+
return this.loaded;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Runs `cb` once the field is loaded (at once if it already is). Returns
|
|
71
|
+
* an unsubscribe. */
|
|
72
|
+
onLoaded(cb: () => void): () => void {
|
|
73
|
+
if (this.loaded) {
|
|
74
|
+
cb();
|
|
75
|
+
return () => {};
|
|
76
|
+
}
|
|
77
|
+
this.loadedListeners.add(cb);
|
|
78
|
+
return () => this.loadedListeners.delete(cb);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @internal The manager calls this when the document became known. */
|
|
82
|
+
markLoaded(): void {
|
|
83
|
+
if (this.loaded) return;
|
|
84
|
+
this.loaded = true;
|
|
85
|
+
for (const cb of [...this.loadedListeners]) {
|
|
86
|
+
try {
|
|
87
|
+
cb();
|
|
88
|
+
} catch {
|
|
89
|
+
/* a listener's failure is its own */
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
this.loadedListeners.clear();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Publish this session's presence (an `EphemeralStore` encoding, e.g. the
|
|
96
|
+
* editor's cursor). No-op unless the field is `@cursor`. */
|
|
97
|
+
pushCursorState(encoded: Uint8Array): void {
|
|
98
|
+
if (this.cursorsEnabled) this.host.pushPresence(encoded);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Called with every other session's presence blob. A blob that arrives
|
|
102
|
+
* before the editor sets this is replayed when it does. */
|
|
103
|
+
set onCursorUpdate(cb: ((data: Uint8Array) => void) | null) {
|
|
104
|
+
this._onCursorUpdate = cb;
|
|
105
|
+
if (cb && this.pendingCursorUpdate) {
|
|
106
|
+
const data = this.pendingCursorUpdate;
|
|
107
|
+
this.pendingCursorUpdate = null;
|
|
108
|
+
try {
|
|
109
|
+
cb(data);
|
|
110
|
+
} catch {
|
|
111
|
+
/* the editor logs its own apply failures */
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get onCursorUpdate() {
|
|
117
|
+
return this._onCursorUpdate;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** @internal Deliver one remote presence blob. */
|
|
121
|
+
deliverPresence(data: Uint8Array): void {
|
|
122
|
+
if (this._onCursorUpdate) this._onCursorUpdate(data);
|
|
123
|
+
else this.pendingCursorUpdate = data;
|
|
124
|
+
}
|
|
125
|
+
}
|