@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.
Files changed (269) hide show
  1. package/AGENTS.md +90 -0
  2. package/README.md +59 -16
  3. package/dist/index.d.ts +2117 -142
  4. package/dist/index.js +16436 -2575
  5. package/dist/live.d.ts +149 -0
  6. package/dist/live.js +739 -0
  7. package/dist/otel/index.d.ts +1 -1
  8. package/dist/otel/index.js +3 -3
  9. package/dist/pure.d.ts +389 -0
  10. package/dist/pure.js +24 -0
  11. package/dist/sqlite-open.js +392 -0
  12. package/dist/sqlite-worker.d.ts +1 -0
  13. package/dist/sqlite-worker.js +439 -0
  14. package/dist/sw.d.ts +207 -0
  15. package/dist/sw.js +393 -0
  16. package/dist/tabs-broker-worker.d.ts +8 -0
  17. package/dist/tabs-broker-worker.js +477 -0
  18. package/dist/types.d.ts +1917 -38
  19. package/dist/worker-edges.js +480 -0
  20. package/dist/worker-impersonation.js +60 -0
  21. package/dist/worker-ref-tables.d.ts +48 -0
  22. package/dist/worker-types.d.ts +112 -0
  23. package/dist/worker-types.js +73 -0
  24. package/package.json +28 -7
  25. package/scripts/check-broker-bundle.mjs +33 -0
  26. package/scripts/check-worker-entries.mjs +173 -0
  27. package/skills/sp00ky-core/SKILL.md +37 -7
  28. package/skills/sp00ky-core/references/config.md +7 -1
  29. package/src/boot/auth-flip.saga.test.ts +135 -0
  30. package/src/boot/auth-flip.saga.ts +77 -0
  31. package/src/boot/boot.saga.test.ts +203 -0
  32. package/src/boot/boot.saga.ts +146 -0
  33. package/src/boot/bucket-switch.saga.test.ts +189 -0
  34. package/src/boot/bucket-switch.saga.ts +136 -0
  35. package/src/boot/preload.saga.test.ts +96 -0
  36. package/src/boot/preload.saga.ts +45 -0
  37. package/src/bucket-blurhash.test.ts +148 -0
  38. package/src/build-globals.d.ts +12 -0
  39. package/src/client/bucket-handle.test.ts +195 -0
  40. package/src/client/bucket-handle.ts +352 -0
  41. package/src/client/router.test.ts +58 -0
  42. package/src/client/router.ts +100 -0
  43. package/src/client/runtime.test.ts +235 -0
  44. package/src/client/runtime.ts +296 -0
  45. package/src/client/services.test.ts +74 -0
  46. package/src/client/services.ts +436 -0
  47. package/src/client/sp00ky-client.test.ts +341 -0
  48. package/src/client/sp00ky-client.ts +625 -0
  49. package/src/events/events.test.ts +2 -1
  50. package/src/events/index.ts +3 -0
  51. package/src/index.ts +69 -1
  52. package/src/kernel/constants.test.ts +19 -0
  53. package/src/kernel/constants.ts +80 -0
  54. package/src/kernel/effects.test.ts +40 -0
  55. package/src/kernel/effects.ts +169 -0
  56. package/src/kernel/events.ts +57 -0
  57. package/src/kernel/interpreter.test.ts +116 -0
  58. package/src/kernel/interpreter.ts +148 -0
  59. package/src/kernel/saga.test.ts +89 -0
  60. package/src/kernel/saga.ts +73 -0
  61. package/src/live/index.test.ts +512 -0
  62. package/src/live/index.ts +1059 -0
  63. package/src/modules/app-release/index.test.ts +125 -0
  64. package/src/modules/app-release/index.ts +209 -0
  65. package/src/modules/auth/auth.local-first.test.ts +228 -0
  66. package/src/modules/auth/auth.session-recheck.test.ts +140 -0
  67. package/src/modules/auth/events/index.ts +9 -2
  68. package/src/modules/auth/impersonation-banner.ts +443 -0
  69. package/src/modules/auth/impersonation.integration.test.ts +205 -0
  70. package/src/modules/auth/impersonation.test.ts +377 -0
  71. package/src/modules/auth/impersonation.ts +179 -0
  72. package/src/modules/auth/index.ts +543 -22
  73. package/src/modules/crdt/acceptance.ts +75 -0
  74. package/src/modules/crdt/bytes.ts +50 -0
  75. package/src/modules/crdt/crdt-field.ts +125 -0
  76. package/src/modules/crdt/crdt.test.ts +515 -0
  77. package/src/modules/crdt/hash.ts +103 -0
  78. package/src/modules/crdt/index.ts +793 -0
  79. package/src/modules/crdt/loro-loader.ts +25 -0
  80. package/src/modules/crdt/mirror.ts +37 -0
  81. package/src/modules/devtools/flags.ts +349 -0
  82. package/src/modules/devtools/index.ts +748 -95
  83. package/src/modules/devtools/mutations.test.ts +92 -0
  84. package/src/modules/devtools/mutations.ts +160 -0
  85. package/src/modules/devtools/notify-throttle.test.ts +166 -0
  86. package/src/modules/devtools/state-shape.test.ts +263 -0
  87. package/src/modules/devtools/storage-info.test.ts +79 -0
  88. package/src/modules/devtools/storage-info.ts +168 -0
  89. package/src/modules/devtools/versions.test.ts +74 -0
  90. package/src/modules/devtools/versions.ts +110 -0
  91. package/src/modules/feature-flag/index.test.ts +298 -0
  92. package/src/modules/feature-flag/index.ts +316 -0
  93. package/src/modules/ref-tables.test.ts +93 -0
  94. package/src/modules/ref-tables.ts +91 -0
  95. package/src/modules/web-push/index.test.ts +824 -0
  96. package/src/modules/web-push/index.ts +1027 -0
  97. package/src/mutation/ingest-outage.integration.test.ts +400 -0
  98. package/src/mutation/jobs.test.ts +25 -0
  99. package/src/mutation/jobs.ts +41 -0
  100. package/src/mutation/mutation-id.test.ts +37 -0
  101. package/src/mutation/mutation-id.ts +35 -0
  102. package/src/mutation/pending-writes.integration.test.ts +175 -0
  103. package/src/mutation/push.saga.test.ts +361 -0
  104. package/src/mutation/push.saga.ts +245 -0
  105. package/src/mutation/rows.test.ts +278 -0
  106. package/src/mutation/rows.ts +499 -0
  107. package/src/mutation/tray.saga.test.ts +63 -0
  108. package/src/mutation/tray.saga.ts +48 -0
  109. package/src/mutation/write.saga.test.ts +316 -0
  110. package/src/mutation/write.saga.ts +268 -0
  111. package/src/otel/index.ts +10 -7
  112. package/src/pure.ts +66 -0
  113. package/src/push/types.ts +174 -0
  114. package/src/query/anon-query-isolation.integration.test.ts +531 -0
  115. package/src/query/env.test.ts +23 -0
  116. package/src/query/env.ts +65 -0
  117. package/src/query/fetch.saga.test.ts +199 -0
  118. package/src/query/fetch.saga.ts +120 -0
  119. package/src/query/hash.test.ts +14 -0
  120. package/src/query/hash.ts +18 -0
  121. package/src/query/id-set-plan.test.ts +132 -0
  122. package/src/query/lifecycle.saga.test.ts +274 -0
  123. package/src/query/lifecycle.saga.ts +186 -0
  124. package/src/query/list-ref-live.integration.test.ts +203 -0
  125. package/src/query/materialize.saga.test.ts +188 -0
  126. package/src/query/materialize.saga.ts +96 -0
  127. package/src/query/materialize.test.ts +58 -0
  128. package/src/query/materialize.ts +57 -0
  129. package/src/query/membership.saga.test.ts +482 -0
  130. package/src/query/membership.saga.ts +315 -0
  131. package/src/query/membership.test.ts +151 -0
  132. package/src/query/membership.ts +234 -0
  133. package/src/query/register.saga.test.ts +312 -0
  134. package/src/query/register.saga.ts +235 -0
  135. package/src/query/render-set.test.ts +47 -0
  136. package/src/query/render-set.ts +71 -0
  137. package/src/query/session-guard.integration.test.ts +304 -0
  138. package/src/query/sql.test.ts +118 -0
  139. package/src/query/sql.ts +157 -0
  140. package/src/query/window-query.test.ts +70 -0
  141. package/src/query/window-query.ts +208 -0
  142. package/src/services/blobs/blob-cache.test.ts +359 -0
  143. package/src/services/blobs/blob-cache.ts +603 -0
  144. package/src/services/blobs/blob-manifest.ts +227 -0
  145. package/src/services/blobs/blob-store.test.ts +77 -0
  146. package/src/services/blobs/blob-store.ts +359 -0
  147. package/src/services/blobs/blob.fixture.ts +90 -0
  148. package/src/services/blobs/index.ts +70 -0
  149. package/src/services/database/cache-engine.ts +209 -0
  150. package/src/services/database/connection-supervisor.test.ts +318 -0
  151. package/src/services/database/connection-supervisor.ts +422 -0
  152. package/src/services/database/database.query-timeout.test.ts +140 -0
  153. package/src/services/database/database.responses.test.ts +43 -0
  154. package/src/services/database/database.ts +156 -61
  155. package/src/services/database/engine-factory.ts +33 -0
  156. package/src/services/database/errors.ts +34 -0
  157. package/src/services/database/events/index.ts +2 -1
  158. package/src/services/database/index.ts +7 -0
  159. package/src/services/database/landed-body-clear.integration.test.ts +236 -0
  160. package/src/services/database/local-migrator.integration.test.ts +110 -0
  161. package/src/services/database/local-migrator.ts +61 -16
  162. package/src/services/database/local.test.ts +150 -0
  163. package/src/services/database/local.ts +508 -63
  164. package/src/services/database/plan-render.test.ts +159 -0
  165. package/src/services/database/plan-render.ts +108 -0
  166. package/src/services/database/relation-resolver.test.ts +484 -0
  167. package/src/services/database/relation-resolver.ts +0 -0
  168. package/src/services/database/remote.ts +285 -6
  169. package/src/services/database/sqlite-cache-engine.test.ts +628 -0
  170. package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
  171. package/src/services/database/sqlite-cache-engine.ts +1401 -0
  172. package/src/services/database/sqlite-column-reset.integration.test.ts +100 -0
  173. package/src/services/database/sqlite-column-reset.ts +91 -0
  174. package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
  175. package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
  176. package/src/services/database/sqlite-lock-verify.test.ts +33 -0
  177. package/src/services/database/sqlite-lock-verify.ts +45 -0
  178. package/src/services/database/sqlite-open.test.ts +150 -0
  179. package/src/services/database/sqlite-open.ts +164 -0
  180. package/src/services/database/sqlite-plan-sql.test.ts +164 -0
  181. package/src/services/database/sqlite-plan-sql.ts +196 -0
  182. package/src/services/database/sqlite-projection.test.ts +121 -0
  183. package/src/services/database/sqlite-select.integration.test.ts +185 -0
  184. package/src/services/database/sqlite-select.test.ts +261 -0
  185. package/src/services/database/sqlite-select.ts +131 -0
  186. package/src/services/database/sqlite-transport.fixture.ts +30 -0
  187. package/src/services/database/sqlite-transport.ts +224 -0
  188. package/src/services/database/sqlite-worker.ts +437 -0
  189. package/src/services/database/surql-translate.ts +417 -0
  190. package/src/services/database/surreal-cache-engine.ts +163 -0
  191. package/src/services/logger/index.test.ts +132 -0
  192. package/src/services/logger/index.ts +189 -7
  193. package/src/services/persistence/localstorage.ts +2 -2
  194. package/src/services/persistence/resilient.ts +10 -3
  195. package/src/services/persistence/surrealdb.ts +4 -4
  196. package/src/services/stream-processor/index.ts +806 -67
  197. package/src/services/stream-processor/permissions.test.ts +47 -0
  198. package/src/services/stream-processor/permissions.ts +53 -0
  199. package/src/services/stream-processor/stream-processor.batch.test.ts +186 -0
  200. package/src/services/stream-processor/stream-processor.prime.test.ts +234 -0
  201. package/src/services/stream-processor/stream-processor.reset.test.ts +226 -0
  202. package/src/services/stream-processor/stream-processor.test.ts +1 -1
  203. package/src/services/stream-processor/wasm-types.ts +58 -2
  204. package/src/services/tabs/broker-client.ts +284 -0
  205. package/src/services/tabs/broker.test.ts +381 -0
  206. package/src/services/tabs/coordinator.test.ts +386 -0
  207. package/src/services/tabs/coordinator.ts +646 -0
  208. package/src/services/tabs/fake-ports.fixture.ts +112 -0
  209. package/src/services/tabs/leader-locks.ts +75 -0
  210. package/src/services/tabs/protocol.ts +262 -0
  211. package/src/services/tabs/support.ts +36 -0
  212. package/src/services/tabs/tabs-broker-worker.ts +652 -0
  213. package/src/sp00ky.ts +5 -395
  214. package/src/state/client-state.ts +216 -0
  215. package/src/state/lifecycle.test.ts +184 -0
  216. package/src/state/lifecycle.ts +127 -0
  217. package/src/state/reducers.test.ts +319 -0
  218. package/src/state/reducers.ts +550 -0
  219. package/src/state/selectors.test.ts +188 -0
  220. package/src/state/selectors.ts +259 -0
  221. package/src/sw/index.test.ts +582 -0
  222. package/src/sw/index.ts +607 -0
  223. package/src/sw/store.ts +96 -0
  224. package/src/sync/connection.saga.test.ts +97 -0
  225. package/src/sync/connection.saga.ts +89 -0
  226. package/src/sync/live.saga.test.ts +229 -0
  227. package/src/sync/live.saga.ts +116 -0
  228. package/src/sync/policy.health.test.ts +56 -0
  229. package/src/sync/policy.test.ts +663 -0
  230. package/src/sync/policy.ts +515 -0
  231. package/src/sync/poll.saga.test.ts +51 -0
  232. package/src/sync/poll.saga.ts +36 -0
  233. package/src/sync/tabs.saga.test.ts +78 -0
  234. package/src/sync/tabs.saga.ts +96 -0
  235. package/src/testing/adapters.ts +137 -0
  236. package/src/testing/build.ts +63 -0
  237. package/src/testing/fake-services.ts +63 -0
  238. package/src/testing/run-pure.test.ts +92 -0
  239. package/src/testing/run-pure.ts +128 -0
  240. package/src/testing/services.ts +17 -0
  241. package/src/types.ts +661 -10
  242. package/src/utils/blurhash.ts +90 -0
  243. package/src/utils/edges.ts +57 -0
  244. package/src/utils/error-classification.test.ts +105 -0
  245. package/src/utils/error-classification.ts +64 -0
  246. package/src/utils/index.ts +89 -12
  247. package/src/utils/parser.test.ts +67 -102
  248. package/src/utils/parser.ts +64 -2
  249. package/src/utils/semver.test.ts +32 -0
  250. package/src/utils/semver.ts +30 -0
  251. package/src/utils/sha256.ts +15 -0
  252. package/src/utils/surql.ts +29 -18
  253. package/src/utils/utils.test.ts +3 -2
  254. package/tsdown.config.ts +117 -3
  255. package/vitest.config.ts +48 -0
  256. package/src/modules/cache/index.ts +0 -241
  257. package/src/modules/cache/types.ts +0 -19
  258. package/src/modules/data/data.test.ts +0 -58
  259. package/src/modules/data/index.ts +0 -879
  260. package/src/modules/sync/engine.ts +0 -173
  261. package/src/modules/sync/events/index.ts +0 -77
  262. package/src/modules/sync/index.ts +0 -3
  263. package/src/modules/sync/queue/index.ts +0 -2
  264. package/src/modules/sync/queue/queue-down.ts +0 -89
  265. package/src/modules/sync/queue/queue-up.ts +0 -223
  266. package/src/modules/sync/scheduler.ts +0 -84
  267. package/src/modules/sync/sync.ts +0 -421
  268. package/src/modules/sync/utils.test.ts +0 -311
  269. 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
+ }