@spooky-sync/core 0.0.1-canary.322 → 0.0.1-canary.324
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 +4 -3
- package/dist/index.d.ts +156 -161
- package/dist/index.js +910 -482
- package/dist/types.d.ts +17 -6
- package/package.json +4 -4
- package/src/boot/preload.saga.test.ts +29 -0
- package/src/boot/preload.saga.ts +11 -2
- package/src/client/services.ts +3 -1
- package/src/client/sp00ky-client.test.ts +4 -1
- package/src/client/sp00ky-client.ts +37 -4
- package/src/modules/crdt/acceptance.ts +75 -0
- package/src/modules/crdt/bytes.ts +50 -0
- package/src/modules/crdt/crdt-field.ts +48 -255
- package/src/modules/crdt/crdt.test.ts +365 -0
- package/src/modules/crdt/hash.ts +103 -0
- package/src/modules/crdt/index.ts +610 -363
- package/src/modules/crdt/mirror.ts +37 -0
- package/src/mutation/push.saga.test.ts +16 -0
- package/src/mutation/push.saga.ts +9 -1
- package/src/query/anon-query-isolation.integration.test.ts +31 -0
- package/src/query/env.ts +9 -0
- package/src/services/database/cache-engine.ts +10 -0
- package/src/services/database/local-migrator.ts +5 -3
- package/src/services/database/sqlite-cache-engine.ts +28 -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/stream-processor/index.ts +24 -0
- package/src/services/stream-processor/stream-processor.prime.test.ts +36 -0
- package/src/testing/fake-services.ts +1 -1
- package/src/types.ts +7 -6
- package/src/utils/surql.ts +3 -4
- package/src/modules/crdt/crdt-hydration.test.ts +0 -210
- package/src/modules/crdt/crdt-reconnect.test.ts +0 -195
package/AGENTS.md
CHANGED
|
@@ -38,7 +38,7 @@ re-materialized from state, never patched in place.
|
|
|
38
38
|
- `BucketHandle` — file storage handle (`put`, `get`, `delete`, `exists`), plus the cache-aware `read`, `url`, `pin`/`unpin`, `evict`, `prefetch`. `get` is always remote; `read`/`url` go through the blob cache.
|
|
39
39
|
- `services/blobs/` — durable cache for bucket bytes: OPFS holds the files, `_00_blob` holds a manifest that reconcile rebuilds from disk (so a wiped local store costs metadata, not the offline cache). Nothing expires on a timer; eviction is LRU under a byte budget only, skipping pinned and on-screen entries. Config: `blobCache` in `types.ts`.
|
|
40
40
|
- `AuthService` — token management, sign-in/sign-out events.
|
|
41
|
-
- `CrdtManager`, `CrdtField`, `cursorColorFromName`, `CURSOR_COLORS`
|
|
41
|
+
- `CrdtManager`, `CrdtField`, `cursorColorFromName`, `CURSOR_COLORS`: collaborative `@crdt` fields on Loro. Client methods: `openCrdtField(table, recordId, field, draft?)` -> `Promise<CrdtField>` (every open of one field in a tab shares one `LoroDoc`; pair each with a close), `closeCrdtField(table, recordId, field)` -> `Promise<void>` (resolves once unsent edits are in the outbox), `createCrdtDraft(table, field)` -> a detached `CrdtField` for a record not created yet (`db.create` first, then pass it to `openCrdtField` as `draft`). `CrdtField`: `getDoc()`, `hasContent()`, `pushCursorState(bytes)`, `onCursorUpdate = cb` (a setter). Config: `crdt: true` preloads Loro, `crdtDebounceMs` (default 300, max wait 2 s). Design: `docs/crdt-v2.md` at the repo root.
|
|
42
42
|
- Types: `Sp00kyConfig` (client-solid wraps it as `SyncedDbConfig`), `QueryTimeToLive`, `PersistenceClient`, `StoreType`, `UpdateOptions`, `RunOptions`, `PreloadOptions`.
|
|
43
43
|
- Subpath: `@spooky-sync/core/otel` — `createOtelTransmit(endpoint)` for piping pino logs to OpenTelemetry.
|
|
44
44
|
- `client.webPush` (`WebPushModule`, `db.webPush` in the Solid bindings) - Web Push on the page: `support()`, `info()`, `subscribe(opts)` (call from a click), `unsubscribe(opts)`, `sync()`, `isSubscribed()`, `update(opts)`, `devices()`, `notify(msg)`, `cancel(id)`, `test()`, `bridge()`, `onMessage(cb)`. Server calls are `fn::push::*`; typed `WebPushError`; config `webPush` in `types.ts`. Sign-out runs through `AuthService.onBeforeSignOut`.
|
|
@@ -49,12 +49,13 @@ re-materialized from state, never patched in place.
|
|
|
49
49
|
- **Use UI hooks, not the client directly.** In a Solid app, call `useDb()` from `@spooky-sync/client-solid`. Touch `Sp00kyClient` only inside `provider.client` for advanced flows.
|
|
50
50
|
- **Mutations are optimistic.** `db.create` / `db.update` / `db.delete` return immediately; the queue drains in the background. Inspect progress via `pendingMutationCount`.
|
|
51
51
|
- **Record IDs are full strings.** `db.create('thread:abc123', {...})` — *not* `db.create('thread', { id: 'abc123' })`. The first arg is `'<table>:<id>'`. Generate IDs with `Uuid` from `surrealdb` (re-exported by `client-solid`).
|
|
52
|
-
- **`db.update` takes `(table, id, payload, options?)`**. The `debounced` option (`{ debounced: true }` or `{ debounced: { delay, key, flushOnHide } }`) coalesces rapid updates
|
|
52
|
+
- **`db.update` takes `(table, id, payload, options?)`**. The `debounced` option (`{ debounced: true }` or `{ debounced: { delay, key, flushOnHide } }`) coalesces rapid updates, e.g. a plain text input saved as you type (never for `@crdt` fields, which are not written with `db.update`). With `flushOnHide` (default `true`) the merged patch is also kept in the local `_00_pending_writes` table and pushed as soon as the page is hidden, so a reload or closed tab inside the delay does not lose it (the next boot moves a leftover patch into the outbox). `flushOnHide: false` keeps the patch in memory only.
|
|
53
53
|
- **`db.remoteQuery(sql, vars?)` is the one-shot remote read.** It runs one SurrealQL statement through the client's remote path (connect gate, per-statement timeout, concurrency limit) and returns the SurrealDB result array; results are not synced into the local cache. Use it for counts and other reads the query builder can't express.
|
|
54
54
|
- **`db.useRemote(fn)` hands out the bare SDK client.** Same cache bypass, but it skips the connect gate (a call during a reconnect fails with "Specify a namespace to use"). Reserve it for SDK features `remoteQuery` cannot reach (transactions, `live`).
|
|
55
55
|
- **Raw remote calls are gated by the query allowlist.** When the generated schema carries `policy.queryAllowlist: 'warn' | 'enforce'` (from `sync.queryAllowlist` in `sp00ky.yml`), `remoteQuery`, `useRemote` and `queryRaw` throw unless the client config sets `allowRawRemote: true`; builder queries are unaffected. A registration the SSP refuses as not allowlisted (403 `not_allowlisted`) is marked `remote: 'failed'` after one attempt with an error-level log carrying the surql and a `spky generate` hint; the view stays cold (`membershipKnown` false), it is never read as confirmed empty.
|
|
56
|
+
- **A `@crdt` field is server-written plain text.** Edit it only through `openCrdtField` / `useCrdtField`; leave it out of `db.create` / `db.update` payloads (a client write keeps the old value). Reading it (lists, search, `where`) is fine, but it trails live edits until the SSP folds the edit log (about a minute after the last edit by default). Edits travel as `<table>__crdt_log` rows through the ordinary outbox; each tab claims a random Loro peer in `_00_crdt_peer`, and log rows whose blob is not an update made by that peer are refused everywhere (`modules/crdt/acceptance.ts`, mirrored in Rust). Unsent edits survive reloads in `_00_crdt_local`. Dart (`spooky_core`) does not implement CRDT fields yet.
|
|
56
57
|
- **`@parent` columns are auto-populated.** Defined in the `.surql` schema as `record<user>` with the `-- @parent` annotation; never write them yourself — they're set from the auth context server-side.
|
|
57
|
-
- **`db.preload` is a registered query.** Resolved before on this device (a `_00_view` row exists) it returns at once; never resolved it waits until the server's membership and every body are local. It holds no subscriber, so it is evicted a ttl after registration unless a view mounts the same query. `refresh`/`staleTime` are ignored; pass `signal` to abort a cold wait.
|
|
58
|
+
- **`db.preload` is a registered query.** Resolved before on this device (a `_00_view` row exists) it returns at once; never resolved it waits until the server's membership and every body are local. A signed-out session without `enableAnonymousLiveQueries` is never sent membership, so its preload returns at once instead of blocking. It holds no subscriber, so it is evicted a ttl after registration unless a view mounts the same query. `refresh`/`staleTime` are ignored; pass `signal` to abort a cold wait.
|
|
58
59
|
- **A never-resolved query paints only vouched rows.** Before the server answers (`cold`), the local predicate scan shows a cached body only if some `_00_view` row names it (as a member or subquery child) or a write for it is in flight. A body no view names is an orphan (deleted upstream while nothing watched it): it stays hidden, and the GC deletes it 30 s after boot and then hourly. A `_00_view` row no query has resolved for 14 days is retired by the same sweep, so that query paints cold again.
|
|
59
60
|
- **A rejected write is not lost.** Application errors roll the local row back (create/update/delete) and move the mutation to `_00_failed_mutations`; the tray API above lists, retries (as a fresh write) or discards it. Network errors keep the row queued.
|
|
60
61
|
|
package/dist/index.d.ts
CHANGED
|
@@ -303,6 +303,10 @@ interface CircuitPrimeContext {
|
|
|
303
303
|
/** Encoded ids with an unsettled local mutation (their `_00_rv` was bumped
|
|
304
304
|
* locally and may exceed the server's next version). */
|
|
305
305
|
pendingIds: Set<string>;
|
|
306
|
+
/** Each table's client column set (`columnFingerprints`). An engine that
|
|
307
|
+
* keeps bodies across schema changes resets the versions of a table whose
|
|
308
|
+
* set changed before they are read, so its rows are fetched again whole. */
|
|
309
|
+
columnFingerprints?: Record<string, string>;
|
|
306
310
|
/** Receives every `(id, rv)` the prime put into the circuit, per table, so
|
|
307
311
|
* the sync layer can skip re-downloading bodies it already has. */
|
|
308
312
|
onVersions?: (table: string, entries: [string, number][]) => void;
|
|
@@ -494,189 +498,185 @@ declare class StreamProcessorService {
|
|
|
494
498
|
private normalizeValue;
|
|
495
499
|
}
|
|
496
500
|
//#endregion
|
|
501
|
+
//#region src/utils/index.d.ts
|
|
502
|
+
declare function fileToUint8Array(file: File | Blob): Promise<Uint8Array>;
|
|
503
|
+
/**
|
|
504
|
+
* Convert plain text to simple HTML paragraphs.
|
|
505
|
+
* Useful for seeding a rich-text editor (e.g. TipTap/ProseMirror) with fallback content.
|
|
506
|
+
*/
|
|
507
|
+
declare function textToHtml(text: string): string;
|
|
508
|
+
/**
|
|
509
|
+
* Helper for retrying DB operations with exponential backoff
|
|
510
|
+
*/
|
|
511
|
+
//#endregion
|
|
512
|
+
//#region src/modules/crdt/mirror.d.ts
|
|
513
|
+
/**
|
|
514
|
+
* The plain text a `@crdt` field shows as its column value, rendered the same
|
|
515
|
+
* way the SSP fold renders it (`packages/ssp-node/src/crdt/mirror.rs`).
|
|
516
|
+
*/
|
|
517
|
+
type CrdtKind = 'text' | 'prosemirror';
|
|
518
|
+
//#endregion
|
|
497
519
|
//#region src/modules/crdt/crdt-field.d.ts
|
|
498
520
|
declare const CURSOR_COLORS: string[];
|
|
499
521
|
declare function cursorColorFromName(name: string): string;
|
|
522
|
+
/** What a field handle needs from the manager that owns its session. */
|
|
523
|
+
interface CrdtFieldHost {
|
|
524
|
+
pushPresence(state: Uint8Array): void;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* An open collaborative field: the Loro document an editor binds to. Edits
|
|
528
|
+
* made on it are sent as the client's own edit-log rows; other clients' rows
|
|
529
|
+
* are imported into it as they arrive. Every open of the same field in one tab
|
|
530
|
+
* shares this document.
|
|
531
|
+
*/
|
|
500
532
|
declare class CrdtField {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
private
|
|
507
|
-
private
|
|
508
|
-
private unsubscribe;
|
|
509
|
-
private lastPushTime;
|
|
510
|
-
private lastCursorPushTime;
|
|
511
|
-
private loadedFromCrdt;
|
|
512
|
-
private pushRetryCount;
|
|
513
|
-
private logger;
|
|
514
|
-
private cursorsEnabled;
|
|
515
|
-
/** Remote-push debounce. Local writes happen immediately on every Loro
|
|
516
|
-
* update; the remote UPSERT is coalesced over this window. Configured
|
|
517
|
-
* via `Sp00kyConfig.crdtDebounceMs`, default 500. */
|
|
518
|
-
private remoteDebounceMs;
|
|
533
|
+
readonly table: string;
|
|
534
|
+
readonly recordId: string;
|
|
535
|
+
readonly fieldName: string;
|
|
536
|
+
readonly kind: CrdtKind;
|
|
537
|
+
readonly cursorsEnabled: boolean;
|
|
538
|
+
private readonly doc;
|
|
539
|
+
private readonly host;
|
|
519
540
|
private _onCursorUpdate;
|
|
520
541
|
private pendingCursorUpdate;
|
|
521
|
-
|
|
522
|
-
* Any cursor data that arrived before this callback was set will be replayed. */
|
|
523
|
-
set onCursorUpdate(cb: ((data: Uint8Array) => void) | null);
|
|
524
|
-
get onCursorUpdate(): ((data: Uint8Array) => void) | null;
|
|
525
|
-
/**
|
|
526
|
-
* @param LoroDocClass the `LoroDoc` constructor, injected by the caller after
|
|
527
|
-
* awaiting {@link loadLoro} — keeps `loro-crdt` out of this module's static
|
|
528
|
-
* import graph so it only ships to apps that use CRDT fields.
|
|
529
|
-
*/
|
|
530
|
-
constructor(fieldName: string, cursorsEnabled: boolean, LoroDocClass: typeof LoroDoc, initialState?: Uint8Array, logger?: Logger$1 | null);
|
|
542
|
+
constructor(table: string, recordId: string, fieldName: string, kind: CrdtKind, cursorsEnabled: boolean, doc: LoroDoc, host: CrdtFieldHost);
|
|
531
543
|
getDoc(): LoroDoc;
|
|
532
|
-
/** Whether the
|
|
544
|
+
/** Whether the document holds anything yet (local state or edits). */
|
|
533
545
|
hasContent(): boolean;
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
* the
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}): void;
|
|
544
|
-
importRemote(state: Uint8Array): void;
|
|
545
|
-
exportSnapshot(): Uint8Array;
|
|
546
|
-
/** Push this session's cursor blob into the parent row at
|
|
547
|
-
* `<field>.cursors[$sid]`. No-op when cursors aren't enabled on this
|
|
548
|
-
* field — the editor still calls this method optimistically, but
|
|
549
|
-
* without `@cursor` on the schema there's nowhere to store the blob.
|
|
550
|
-
* The UPDATE itself fires the parent table's LIVE feed, so other
|
|
551
|
-
* browsers receive the cursor change without a separate `_00_rv` bump. */
|
|
552
|
-
pushCursorState(encoded: Uint8Array): Promise<void>;
|
|
553
|
-
/** Import remote cursor state (called by CrdtManager from LIVE SELECT) */
|
|
554
|
-
importRemoteCursor(base64State: string): void;
|
|
555
|
-
private scheduleRemotePush;
|
|
556
|
-
/** SET path inside a parent row for the current snapshot. `@crdt`-only
|
|
557
|
-
* fields hold the snapshot directly (`<field>`); `@crdt @cursor`
|
|
558
|
-
* fields hold a `{ state, cursors }` object so the snapshot lives at
|
|
559
|
-
* `<field>.state` next to per-session cursor blobs. */
|
|
560
|
-
private statePath;
|
|
561
|
-
/** Mirror the LoroDoc snapshot into the parent row locally. Runs on
|
|
562
|
-
* every local update and every remote import so reloads (online or
|
|
563
|
-
* offline) see the freshest content immediately. Failures are
|
|
564
|
-
* swallowed — a stale local write must never block user input. */
|
|
565
|
-
private persistLocal;
|
|
566
|
-
private pushToRemote;
|
|
546
|
+
/** Publish this session's presence (an `EphemeralStore` encoding, e.g. the
|
|
547
|
+
* editor's cursor). No-op unless the field is `@cursor`. */
|
|
548
|
+
pushCursorState(encoded: Uint8Array): void;
|
|
549
|
+
/** Called with every other session's presence blob. A blob that arrives
|
|
550
|
+
* before the editor sets this is replayed when it does. */
|
|
551
|
+
set onCursorUpdate(cb: ((data: Uint8Array) => void) | null);
|
|
552
|
+
get onCursorUpdate(): ((data: Uint8Array) => void) | null;
|
|
553
|
+
/** @internal Deliver one remote presence blob. */
|
|
554
|
+
deliverPresence(data: Uint8Array): void;
|
|
567
555
|
}
|
|
568
556
|
//#endregion
|
|
569
557
|
//#region src/modules/crdt/index.d.ts
|
|
570
558
|
/**
|
|
571
|
-
*
|
|
559
|
+
* Collaborative (`@crdt`) fields.
|
|
572
560
|
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
561
|
+
* Each open field is a Loro document. The client never sends the document,
|
|
562
|
+
* only its own edits: every batch of local edits becomes one row of the
|
|
563
|
+
* generated `<table>__crdt_log` table, written through the ordinary outbox
|
|
564
|
+
* (so it survives offline, reloads and retries), and every client receives
|
|
565
|
+
* the other rows through an ordinary query and merges them. Nobody writes the
|
|
566
|
+
* canonical state but the SSP, which folds the log into `_00_crdt_doc` and the
|
|
567
|
+
* parent field's plain text (see `packages/ssp-node/src/crdt`).
|
|
578
568
|
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
* collide on the parent's last-write-wins semantics.
|
|
569
|
+
* A row is imported only if it passes the acceptance rule the fold applies
|
|
570
|
+
* (`./acceptance`), so this tab and the server always hold the same ops.
|
|
582
571
|
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
* via subquery. Permission inheritance happens server-side via
|
|
589
|
-
* `record_id.id != NONE` (SELECT) and `fn::can_update_record` (UPDATE).
|
|
572
|
+
* Edits carry this tab's Loro peer id, claimed for the signed-in user in
|
|
573
|
+
* `_00_crdt_peer` before the first row is sent; the database refuses a row
|
|
574
|
+
* whose peer its author does not own. A tab picks a fresh peer per session.
|
|
575
|
+
* Offline, edits stay local (and in the local snapshot) until the claim and
|
|
576
|
+
* the outbox can go through.
|
|
590
577
|
*/
|
|
578
|
+
/** A followed query: stop following it, and know when the server answered. */
|
|
579
|
+
interface Watch {
|
|
580
|
+
stop: () => void;
|
|
581
|
+
/** Resolves once the server's membership for the query is known and its
|
|
582
|
+
* rows have landed locally (never rejects; may never resolve offline). */
|
|
583
|
+
settled: Promise<void>;
|
|
584
|
+
}
|
|
585
|
+
/** The runtime pieces the manager needs from the client facade. */
|
|
586
|
+
interface CrdtPorts {
|
|
587
|
+
/** Register a query on `table` filtered by `where` and follow its rows. */
|
|
588
|
+
watch(table: string, where: Record<string, unknown>, onRows: (rows: Record<string, unknown>[]) => void): Promise<Watch>;
|
|
589
|
+
/** Optimistic create through the outbox. */
|
|
590
|
+
create(id: string, data: Record<string, unknown>): Promise<unknown>;
|
|
591
|
+
}
|
|
591
592
|
declare class CrdtManager {
|
|
592
593
|
private schema;
|
|
593
594
|
private local;
|
|
594
595
|
private remote;
|
|
595
596
|
private debounceMs;
|
|
596
|
-
private
|
|
597
|
-
private
|
|
598
|
-
private
|
|
599
|
-
private staleTables;
|
|
600
|
-
private connectionGeneration;
|
|
601
|
-
private connectionUnsubscribes;
|
|
597
|
+
private sessions;
|
|
598
|
+
private opening;
|
|
599
|
+
private ports;
|
|
602
600
|
private logger;
|
|
603
|
-
private
|
|
601
|
+
private peer;
|
|
602
|
+
/** The current peer's claim: settled once the database holds it for us. */
|
|
603
|
+
private claim;
|
|
604
|
+
private claimRetry;
|
|
605
|
+
private connectionUnsubscribes;
|
|
604
606
|
constructor(schema: SchemaStructure, local: LocalStore, remote: RemoteDatabaseService, logger: Logger$1, debounceMs?: number);
|
|
607
|
+
/** Hand over the runtime ports. Called once by the client facade. */
|
|
608
|
+
attach(ports: CrdtPorts): void;
|
|
605
609
|
/**
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
* `ensureTableSubscription` is memoized on `liveByTable` — so without this,
|
|
610
|
-
* the first reconnect leaves CRDT realtime permanently dead: the map still
|
|
611
|
-
* holds a uuid for a subscription the server has forgotten, so every later
|
|
612
|
-
* `open()` short-circuits and no LIVE is ever re-issued.
|
|
613
|
-
*
|
|
614
|
-
* Both drop events matter: the SDK publishes `reconnecting` (not
|
|
615
|
-
* `disconnected`) when it intends to recover on its own, and `disconnected`
|
|
616
|
-
* only once it has given up.
|
|
610
|
+
* A new signed-in identity (boot, sign-in, sign-out): edits from here on
|
|
611
|
+
* belong to it, under a peer it claims. Edits already made stay with the
|
|
612
|
+
* peer they were made under.
|
|
617
613
|
*/
|
|
618
|
-
|
|
619
|
-
/** Stop observing transport events. Separate from {@link closeAll}, which also
|
|
620
|
-
* runs on a bucket switch where the manager keeps being used. */
|
|
614
|
+
setSessionId(_salt: string): void;
|
|
621
615
|
dispose(): void;
|
|
622
|
-
private hasOpenFieldFor;
|
|
623
|
-
/** Set the session id that scopes this client's cursor entries. Must be
|
|
624
|
-
* called before `open()` for cursors to be pushed under a stable key.
|
|
625
|
-
* Passed in from `sp00ky.ts` at boot (it already fetches `session::id()`
|
|
626
|
-
* for the data-module salt). */
|
|
627
|
-
setSessionId(sessionId: string): void;
|
|
628
616
|
/**
|
|
629
|
-
* Open a
|
|
617
|
+
* Open a collaborative field. Every open of the same field shares one
|
|
618
|
+
* document; close each open with {@link close}.
|
|
630
619
|
*
|
|
631
|
-
* @
|
|
632
|
-
*
|
|
633
|
-
* @param field - Field name (e.g., "title", "content")
|
|
634
|
-
* @param fallbackText - Current plain text from the record, used to seed the
|
|
635
|
-
* LoroDoc if no CRDT state exists yet (migration path)
|
|
620
|
+
* `draft` (from {@link createDraft}) hands over edits made before the record
|
|
621
|
+
* existed: they are merged in and sent like this tab's own.
|
|
636
622
|
*/
|
|
637
|
-
open(table: string, recordId: string, field: string,
|
|
638
|
-
close(table: string, recordId: string, field: string): void;
|
|
623
|
+
open(table: string, recordId: string, field: string, draft?: CrdtField): Promise<CrdtField>;
|
|
639
624
|
/**
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
625
|
+
* A detached document for a field of a record that does not exist yet (a
|
|
626
|
+
* "new thread" form). Nothing is sent; pass it to {@link open} once the
|
|
627
|
+
* record is created and its edits become the record's first ones.
|
|
643
628
|
*/
|
|
629
|
+
createDraft(table: string, field: string): Promise<CrdtField>;
|
|
630
|
+
private adopt;
|
|
631
|
+
/** Close one open. The last close of a field resolves once its unsent
|
|
632
|
+
* edits are in the outbox (or are left for a later session, offline). */
|
|
633
|
+
close(table: string, recordId: string, field: string): Promise<void>;
|
|
634
|
+
/** Close every field. `flush: false` (a bucket switch) drops unsent edits
|
|
635
|
+
* from this session's memory; they stay in the local snapshot of the
|
|
636
|
+
* bucket they belong to. */
|
|
644
637
|
closeAll(options?: {
|
|
645
638
|
flush?: boolean;
|
|
646
639
|
}): void;
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
private
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
applyRow(table: string, row: Record<string, unknown>): void;
|
|
655
|
-
/** Dispatch a parent-row LIVE event to every open CrdtField on that
|
|
656
|
-
* record. Each open field reads its slice of the row directly — the
|
|
657
|
-
* CRDT snapshot is a column on the parent now, so there is no
|
|
658
|
-
* follow-up subquery. */
|
|
659
|
-
private dispatchRow;
|
|
660
|
-
/** One-shot remote fetch for a row whose CRDT field hasn't synced
|
|
661
|
-
* locally yet (fresh device, memory-backed local DB after reload, …).
|
|
662
|
-
* Used by `open()` when the local read came up empty. Subsequent
|
|
663
|
-
* cross-browser updates ride `dispatchRow` via the parent LIVE feed. */
|
|
664
|
-
private fetchAndDispatchRow;
|
|
665
|
-
/** Schema lookup: does `<table>.<field>` carry a `@cursor` annotation?
|
|
666
|
-
* Determines the on-disk shape (plain snapshot vs. `{ state, cursors }`). */
|
|
667
|
-
private fieldHasCursor;
|
|
668
|
-
/** Pull the LoroDoc snapshot bytes out of a row slice. For `@crdt`-only
|
|
669
|
-
* the slice IS the snapshot (Uint8Array); for `@crdt @cursor` it's
|
|
670
|
-
* `{ state, cursors }` where `state` carries the snapshot bytes. */
|
|
671
|
-
private extractSnapshot;
|
|
672
|
-
private killTableSubscription;
|
|
673
|
-
private makeKey;
|
|
640
|
+
private fieldSpec;
|
|
641
|
+
private start;
|
|
642
|
+
private stop;
|
|
643
|
+
private ingest;
|
|
644
|
+
private tryImport;
|
|
645
|
+
private refetchCanonical;
|
|
646
|
+
private scheduleVerify;
|
|
674
647
|
/**
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
648
|
+
* After a fold: compare this device's document with the one the server
|
|
649
|
+
* published, at the version the server published. Missing ops (rows this
|
|
650
|
+
* device never got before they were folded away) are repaired by merging
|
|
651
|
+
* the published snapshot in. The same ops giving a different state cannot
|
|
652
|
+
* be merged away; that is logged.
|
|
678
653
|
*/
|
|
679
|
-
private
|
|
654
|
+
private verify;
|
|
655
|
+
private scheduleFlush;
|
|
656
|
+
private flush;
|
|
657
|
+
/** Send every own peer's ops that are not in the outbox yet, one row per
|
|
658
|
+
* peer. Only the peer's own ops travel (`updates-in-range`): a plain
|
|
659
|
+
* `from: version` export would carry ops imported from others too, and
|
|
660
|
+
* the acceptance rule refuses such a row. */
|
|
661
|
+
private sendOwnEdits;
|
|
662
|
+
private retryLater;
|
|
663
|
+
/** Claim this session's peer once; a failure (offline, signed out) is retried. */
|
|
664
|
+
private ensureClaimed;
|
|
665
|
+
private claimPeer;
|
|
666
|
+
private localId;
|
|
667
|
+
private readLocal;
|
|
668
|
+
private schedulePersist;
|
|
669
|
+
/** Save the merged document. Another tab may have saved too: merge with
|
|
670
|
+
* its snapshot rather than overwrite it, and keep the further of the two
|
|
671
|
+
* outbox marks per peer. */
|
|
672
|
+
private persist;
|
|
673
|
+
private pushPresence;
|
|
674
|
+
/** Presence is the latest state only: no outbox, no offline queue. */
|
|
675
|
+
private sendPresence;
|
|
676
|
+
/** Remove this session's cursor when the field closes; the server's TTL
|
|
677
|
+
* sweep covers a tab that never gets here. */
|
|
678
|
+
private dropPresence;
|
|
679
|
+
private ingestPresence;
|
|
680
680
|
}
|
|
681
681
|
//#endregion
|
|
682
682
|
//#region src/modules/feature-flag/index.d.ts
|
|
@@ -2114,8 +2114,15 @@ declare class Sp00kyClient<S extends SchemaStructure> {
|
|
|
2114
2114
|
clearFeatureOverrides(): void;
|
|
2115
2115
|
getFeatureOverrides(): Record<string, FeatureFlagOverride>;
|
|
2116
2116
|
appRelease(app: string, options?: AppReleaseOptions): AppReleaseHandle;
|
|
2117
|
-
|
|
2118
|
-
|
|
2117
|
+
/** Open a `@crdt` field for editing. `draft` (from {@link createCrdtDraft})
|
|
2118
|
+
* hands over edits made before the record existed. */
|
|
2119
|
+
openCrdtField(table: string, recordId: string, field: string, draft?: CrdtField): Promise<CrdtField>;
|
|
2120
|
+
/** A detached `@crdt` document for a record not created yet; see
|
|
2121
|
+
* {@link openCrdtField}. */
|
|
2122
|
+
createCrdtDraft(table: string, field: string): Promise<CrdtField>;
|
|
2123
|
+
/** Close a `@crdt` field. The last close resolves once its unsent edits
|
|
2124
|
+
* are queued in the outbox. */
|
|
2125
|
+
closeCrdtField(table: string, recordId: string, field: string): Promise<void>;
|
|
2119
2126
|
bucket<B extends BucketNames<S>>(name: B): BucketHandle;
|
|
2120
2127
|
getBlobCacheStats(): BlobCacheStats;
|
|
2121
2128
|
}
|
|
@@ -2153,17 +2160,5 @@ declare class LocalOpTimeoutError extends Error {
|
|
|
2153
2160
|
/** Default deadline for one local-store operation. Generous: a cold 4k-row
|
|
2154
2161
|
* select on a throttled tab is seconds, not tens of seconds. */
|
|
2155
2162
|
declare const DEFAULT_LOCAL_OP_TIMEOUT_MS = 30000;
|
|
2156
|
-
//#endregion
|
|
2157
|
-
//#region src/utils/index.d.ts
|
|
2158
|
-
declare function fileToUint8Array(file: File | Blob): Promise<Uint8Array>;
|
|
2159
|
-
/**
|
|
2160
|
-
* Convert plain text to simple HTML paragraphs.
|
|
2161
|
-
* Useful for seeding a rich-text editor (e.g. TipTap/ProseMirror) with fallback content.
|
|
2162
|
-
*/
|
|
2163
|
-
declare function textToHtml(text: string): string;
|
|
2164
|
-
/**
|
|
2165
|
-
* Helper for retrying DB operations with exponential backoff
|
|
2166
|
-
*/
|
|
2167
|
-
|
|
2168
2163
|
//#endregion
|
|
2169
2164
|
export { ActiveImpersonation, AppReleaseHandle, AppReleaseModule, type AppReleaseOptions, type AppReleaseSnapshot, AuthEventSystem, AuthEventTypeMap, AuthEventTypes, AuthService, BLURHASH_IMAGE_EXTENSIONS, type BlobCacheStats, type BlobEntry, type BlobKey, type BlobReadOptions, type BlobUrlLease, type BlurhashEncodeOptions, type BlurhashSetting, type BridgeSignOutMessage, type BridgeTokenMessage, BucketHandle, BucketPutOptions, BucketPutResult, CURSOR_COLORS, ConnectionState, CrdtField, CrdtManager, DEFAULT_LOCAL_OP_TIMEOUT_MS, DebounceOptions, EventSubscriptionOptions, type FailedMutationRow as FailedMutation, FeatureFlagHandle, FeatureFlagModule, type FeatureFlagOptions, type FeatureFlagOverride, type FeatureFlagSnapshot, IMPERSONATION_ACCESS, ImpersonationCandidate, ImpersonationInfo, ImpersonationTimerHost, ImpersonationTimers, InlineRow, Level, LocalOpTimeoutError, MATERIALIZATION_SAMPLE_WINDOW, MembershipOutcome, MutationCallback, MutationEvent, MutationEventType, ORIGIN_TOKEN_KEY, PUSH_ENDPOINT_KEY, PUSH_PAYLOAD_VERSION, PersistenceClient, PhaseStat, PinoTransmit, PreloadOptions, PreloadRefresh, type PushDeviceKind, type PushNotification, type PushNotificationAction, type PushOp, type PushPayload, type PushPayloadKind, type PushUrgency, QueryAuthorityCallback, QueryConfig, QueryConfigRecord, QueryHash, QueryState, QueryStatus, QueryStatusCallback, QueryTimeToLive, QueryTimings, QueryUpdateCallback, ReconnectConfig, RecordVersionArray, RecordVersionDiff, RegistrationTimings, RunOptions, SESSION_POLL_MS, SIGN_OUT_HOOK_TIMEOUT_MS, ServerViewMeta, SignOutContext, SignOutHook, Sp00kyClient, Sp00kyClientDeps, Sp00kyConfig, Sp00kyQueryResult, Sp00kyQueryResultPromise, StorageHealth, StorageHealthStatus, StoreType, SyncHealth, SyncHealthConfig, SyncHealthStatus, TimingPhase, TokenClaims, UpdateOptions, type WebPushClientMessage, type WebPushConfig, type WebPushDevice, WebPushError, type WebPushErrorCode, type WebPushInfo, type WebPushMessage, type WebPushMessageInput, WebPushModule, type WebPushSubscribeOptions, type WebPushSupport, type WebPushSyncOptions, type WebPushSyncStatus, type WebPushUnsubscribeOptions, type WebPushUnsupportedReason, type WebPushUpdateOptions, blurhashSidecarPath, bucketContentToBlob, createAuthEventSystem, cursorColorFromName, decode as decodeBlurhash, decodeTokenClaims, detectWebPushSupport, encode as encodeBlurhash, encodeImageToBlurhash, fileToUint8Array, impersonationFromToken, isBlurhashValid, isImagePath, isPushPayload, renewDelayMs, sameImpersonation, semverGt, textToHtml };
|