@spooky-sync/core 0.0.1-canary.325 → 0.0.1-canary.327
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 +2 -2
- package/dist/index.d.ts +12 -2
- package/dist/index.js +456 -353
- package/package.json +3 -3
- package/src/client/runtime.ts +4 -4
- package/src/client/sp00ky-client.ts +1 -1
- package/src/modules/crdt/crdt.test.ts +49 -1
- package/src/modules/crdt/index.ts +35 -13
- package/src/query/id-set-plan.test.ts +2 -2
- package/src/query/lifecycle.saga.ts +28 -1
- package/src/query/materialize.test.ts +1 -1
- package/src/query/membership.saga.test.ts +34 -0
- package/src/query/membership.saga.ts +4 -0
- package/src/query/window-query.test.ts +6 -6
- package/src/query/window-query.ts +14 -1
- package/src/services/database/sqlite-cache-engine.test.ts +11 -0
- package/src/services/database/surql-translate.ts +4 -3
- package/src/services/database/surreal-cache-engine.ts +3 -1
- package/src/state/selectors.test.ts +13 -0
- package/src/state/selectors.ts +37 -1
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`: 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.
|
|
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; offline, once they are saved on the device, never waiting on the network), `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`.
|
|
@@ -53,7 +53,7 @@ re-materialized from state, never patched in place.
|
|
|
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`;
|
|
56
|
+
- **A `@crdt` field is server-written plain text.** Edit it only through `openCrdtField` / `useCrdtField`; set it only in the `db.create` that makes the record (its initial text; a later 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 (a few seconds 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.
|
|
57
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.
|
|
58
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.
|
|
59
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.
|
package/dist/index.d.ts
CHANGED
|
@@ -596,6 +596,8 @@ declare class CrdtManager {
|
|
|
596
596
|
private debounceMs;
|
|
597
597
|
private sessions;
|
|
598
598
|
private opening;
|
|
599
|
+
/** Closes still saving their field's local state, by session key. */
|
|
600
|
+
private saving;
|
|
599
601
|
private ports;
|
|
600
602
|
private logger;
|
|
601
603
|
private peer;
|
|
@@ -629,7 +631,8 @@ declare class CrdtManager {
|
|
|
629
631
|
createDraft(table: string, field: string): Promise<CrdtField>;
|
|
630
632
|
private adopt;
|
|
631
633
|
/** Close one open. The last close of a field resolves once its unsent
|
|
632
|
-
* edits are in the outbox
|
|
634
|
+
* edits are in the outbox, or once they are saved on the device when the
|
|
635
|
+
* server does not answer in time (offline: at once). */
|
|
633
636
|
close(table: string, recordId: string, field: string): Promise<void>;
|
|
634
637
|
/** Close every field. `flush: false` (a bucket switch) drops unsent edits
|
|
635
638
|
* from this session's memory; they stay in the local snapshot of the
|
|
@@ -640,6 +643,13 @@ declare class CrdtManager {
|
|
|
640
643
|
private fieldSpec;
|
|
641
644
|
private start;
|
|
642
645
|
private stop;
|
|
646
|
+
/**
|
|
647
|
+
* Wait for server-bound `work`, at most {@link SERVER_WAIT_MS}, and not at
|
|
648
|
+
* all while disconnected: a remote call made then queues behind the
|
|
649
|
+
* reconnect for up to the query timeout, and an offline "new thread" form
|
|
650
|
+
* must not hang on it. The work itself goes on either way.
|
|
651
|
+
*/
|
|
652
|
+
private serverWait;
|
|
643
653
|
private ingest;
|
|
644
654
|
private tryImport;
|
|
645
655
|
private refetchCanonical;
|
|
@@ -2121,7 +2131,7 @@ declare class Sp00kyClient<S extends SchemaStructure> {
|
|
|
2121
2131
|
* {@link openCrdtField}. */
|
|
2122
2132
|
createCrdtDraft(table: string, field: string): Promise<CrdtField>;
|
|
2123
2133
|
/** Close a `@crdt` field. The last close resolves once its unsent edits
|
|
2124
|
-
* are queued in the outbox. */
|
|
2134
|
+
* are queued in the outbox; offline, once they are saved on the device. */
|
|
2125
2135
|
closeCrdtField(table: string, recordId: string, field: string): Promise<void>;
|
|
2126
2136
|
bucket<B extends BucketNames<S>>(name: B): BucketHandle;
|
|
2127
2137
|
getBlobCacheStats(): BlobCacheStats;
|