@spooky-sync/core 0.0.1-canary.235 → 0.0.1-canary.237
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 +3 -2
- package/dist/index.d.ts +95 -30
- package/dist/index.js +356 -111
- package/dist/types.d.ts +5 -0
- package/package.json +3 -3
- package/src/modules/data/index.ts +7 -0
- package/src/modules/sync/sync.health.test.ts +34 -26
- package/src/modules/sync/sync.ts +211 -30
- package/src/modules/sync/utils.test.ts +53 -0
- package/src/modules/sync/utils.ts +77 -0
- package/src/services/database/database.query-timeout.test.ts +58 -1
- package/src/services/database/database.ts +103 -73
- package/src/services/database/remote.ts +8 -0
- package/src/sp00ky.init-query.test.ts +10 -10
- package/src/sp00ky.ts +47 -28
- package/src/types.ts +5 -0
package/AGENTS.md
CHANGED
|
@@ -23,7 +23,7 @@ Local mutations are applied optimistically, ingested into a DBSP layer that driv
|
|
|
23
23
|
|
|
24
24
|
## Key exports (`src/index.ts`)
|
|
25
25
|
|
|
26
|
-
- `Sp00kyClient<S>` — main class. Methods: `init()`, `create(id, payload)`, `update(table, id, payload, options?)`, `delete(table, idOrSelector)`, `query(table, opts?)`, `run(backend, route, payload)`, `bucket(name)`, `useRemote(fn)`, `authenticate(token)`, `signOut()`. Plus `pendingMutationCount` and `subscribeToPendingMutations(cb)`.
|
|
26
|
+
- `Sp00kyClient<S>` — main class. Methods: `init()`, `create(id, payload)`, `update(table, id, payload, options?)`, `delete(table, idOrSelector)`, `query(table, opts?)`, `run(backend, route, payload)`, `bucket(name)`, `remoteQuery(sql, vars?)`, `useRemote(fn)`, `authenticate(token)`, `signOut()`. Plus `pendingMutationCount` and `subscribeToPendingMutations(cb)`.
|
|
27
27
|
- `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.
|
|
28
28
|
- `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`.
|
|
29
29
|
- `AuthService` — token management, sign-in/sign-out events.
|
|
@@ -37,7 +37,8 @@ Local mutations are applied optimistically, ingested into a DBSP layer that driv
|
|
|
37
37
|
- **Mutations are optimistic.** `db.create` / `db.update` / `db.delete` return immediately; the queue drains in the background. Inspect progress via `pendingMutationCount`.
|
|
38
38
|
- **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`).
|
|
39
39
|
- **`db.update` takes `(table, id, payload, options?)`**. The `debounced` option (`{ debounced: true }` or `{ debounced: { delay, key } }`) coalesces rapid updates — use it for CRDT text/title fields.
|
|
40
|
-
- **`db.
|
|
40
|
+
- **`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.
|
|
41
|
+
- **`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`).
|
|
41
42
|
- **`@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.
|
|
42
43
|
|
|
43
44
|
## Module map (under `src/modules/`)
|
package/dist/index.d.ts
CHANGED
|
@@ -30,22 +30,40 @@ declare abstract class AbstractDatabaseService {
|
|
|
30
30
|
getClient(): Surreal$1;
|
|
31
31
|
getEvents(): DatabaseEventSystem;
|
|
32
32
|
tx(): Promise<SurrealTransaction>;
|
|
33
|
-
private queryQueue;
|
|
34
33
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
34
|
+
* How many statements may be in flight at once.
|
|
35
|
+
*
|
|
36
|
+
* `1` serializes every statement behind the previous one. The local engines
|
|
37
|
+
* need that: overlapping ops trip the WASM / SQLite transaction layer. The
|
|
38
|
+
* remote service raises it (`RemoteDatabaseService`): a SurrealDB socket
|
|
39
|
+
* multiplexes requests by id, and serializing them capped the client at one
|
|
40
|
+
* statement per round trip (~5/s at 170ms RTT), so the `_00_list_ref` poll
|
|
41
|
+
* alone could hold every registration behind it and one slow one-shot read
|
|
42
|
+
* stalled the whole client. The scheduler's `MAX_CONCURRENT_DOWN` only means
|
|
43
|
+
* something when this is above 1.
|
|
44
|
+
*/
|
|
45
|
+
protected maxConcurrentQueries: number;
|
|
46
|
+
private inFlightQueries;
|
|
47
|
+
private queryWaiters;
|
|
48
|
+
/** FIFO slot acquisition, so `maxConcurrentQueries = 1` keeps strict order. */
|
|
49
|
+
private acquireQuerySlot;
|
|
50
|
+
private releaseQuerySlot;
|
|
51
|
+
/**
|
|
52
|
+
* Hook run right before a statement is sent, inside its slot. The remote
|
|
53
|
+
* service uses it to hold statements while a connect is in flight, so a
|
|
54
|
+
* query never runs on a socket that is open but has not had its namespace
|
|
55
|
+
* and token applied yet. The default does nothing.
|
|
41
56
|
*/
|
|
57
|
+
protected beforeQuery(): Promise<void>;
|
|
42
58
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
59
|
+
* Execute a statement under {@link maxConcurrentQueries}.
|
|
60
|
+
*
|
|
61
|
+
* A statement that never settles would hold its slot forever, and with a
|
|
62
|
+
* limit of 1 that blocks every later statement behind it. {@link
|
|
63
|
+
* queryTimeoutMs} bounds each one: on expiry this promise rejects and the
|
|
64
|
+
* slot is released, even though the underlying RPC is still parked in the
|
|
65
|
+
* SDK's pending map.
|
|
47
66
|
*/
|
|
48
|
-
protected beforeQuery(): Promise<void>;
|
|
49
67
|
query<T extends unknown[]>(query: string, vars?: Record<string, unknown>): Promise<T>;
|
|
50
68
|
execute<T>(query: SealedQuery<T>, vars?: Record<string, unknown>): Promise<T>;
|
|
51
69
|
close(): Promise<void>;
|
|
@@ -1409,6 +1427,7 @@ declare class Sp00kySync<S extends SchemaStructure> {
|
|
|
1409
1427
|
private listRefPollInFlight;
|
|
1410
1428
|
readonly refSyncIntervalMs: number;
|
|
1411
1429
|
private listRefIdleStreak;
|
|
1430
|
+
private listRefPolledAt;
|
|
1412
1431
|
private stillRemoteStreaks;
|
|
1413
1432
|
private lastLiveEventAt;
|
|
1414
1433
|
private _liveRetryCount;
|
|
@@ -1587,9 +1606,10 @@ declare class Sp00kySync<S extends SchemaStructure> {
|
|
|
1587
1606
|
private startListRefPoll;
|
|
1588
1607
|
private stopListRefPoll;
|
|
1589
1608
|
/**
|
|
1590
|
-
* One poll cycle: refetch `_00_list_ref` for
|
|
1591
|
-
*
|
|
1592
|
-
*
|
|
1609
|
+
* One poll cycle: refetch `_00_list_ref` for the active queries that are due,
|
|
1610
|
+
* one round trip per chunk (see `planListRefPollChunks`). Returns whether ANY
|
|
1611
|
+
* query's remoteArray actually changed — the scheduler uses this to drive the
|
|
1612
|
+
* adaptive idle backoff.
|
|
1593
1613
|
*
|
|
1594
1614
|
* Also the ONLY health signal that runs while the page is idle. Sync health is
|
|
1595
1615
|
* otherwise activity-driven (mutations/registrations via the scheduler,
|
|
@@ -1602,14 +1622,37 @@ declare class Sp00kySync<S extends SchemaStructure> {
|
|
|
1602
1622
|
*/
|
|
1603
1623
|
private pollListRefForActiveQueries;
|
|
1604
1624
|
/**
|
|
1605
|
-
*
|
|
1606
|
-
*
|
|
1607
|
-
*
|
|
1608
|
-
*
|
|
1609
|
-
*
|
|
1610
|
-
*
|
|
1625
|
+
* The server's current `_00_list_ref` state for a chunk of queries in ONE
|
|
1626
|
+
* round trip: primary edges, subquery-child edges and `_00_query.rowCount`.
|
|
1627
|
+
* Every hash whose query still exists gets an entry (empty arrays and
|
|
1628
|
+
* `rowCount: null` when nothing was readable), so a caller can tell "no
|
|
1629
|
+
* edges" from "not asked". A hash is left out only when the select did not
|
|
1630
|
+
* come back as an array at all — the old per-query code skipped the apply in
|
|
1631
|
+
* that case too, since a non-array is not a membership set.
|
|
1632
|
+
*
|
|
1633
|
+
* Guarded against a batch that silently matches nothing: if the select
|
|
1634
|
+
* returned no edge at all while the client holds edges for a query the
|
|
1635
|
+
* server still reports rows for, the batch is not trusted and the chunk is
|
|
1636
|
+
* re-read one query at a time with the single-query select. An empty set
|
|
1637
|
+
* taken at face value would be recorded as the removal of every row.
|
|
1638
|
+
*/
|
|
1639
|
+
private fetchListRefSnapshots;
|
|
1640
|
+
/**
|
|
1641
|
+
* Single-query form of {@link fetchListRefSnapshots}: the selects the
|
|
1642
|
+
* registration path has always used, in one statement batch. `null` when
|
|
1643
|
+
* the primary select did not come back as an array.
|
|
1611
1644
|
*/
|
|
1612
|
-
private
|
|
1645
|
+
private fetchListRefSnapshot;
|
|
1646
|
+
/**
|
|
1647
|
+
* Land one poll snapshot for `queryHash`: diff the server's edges against the
|
|
1648
|
+
* cached `remoteArray`, sync any added/updated rows through the SyncEngine,
|
|
1649
|
+
* persist the new remoteArray and converge the subquery children. This is
|
|
1650
|
+
* the same shape `createRemoteQuery` does for its initial fetch and what
|
|
1651
|
+
* `handleRemoteListRefChange` does per-LIVE-event — we reuse it on a timer
|
|
1652
|
+
* as a fallback for missed LIVE notifications. Returns whether membership
|
|
1653
|
+
* changed.
|
|
1654
|
+
*/
|
|
1655
|
+
private applyListRefSnapshot;
|
|
1613
1656
|
/**
|
|
1614
1657
|
* Resolve the current `_00_list_ref` table name for the active auth
|
|
1615
1658
|
* context. Public so the `createRemoteQuery` initial-fetch path can
|
|
@@ -1785,6 +1828,13 @@ declare class Sp00kySync<S extends SchemaStructure> {
|
|
|
1785
1828
|
* query to `fetching` or skew its DevTools timings.
|
|
1786
1829
|
*/
|
|
1787
1830
|
private syncSubqueryChildren;
|
|
1831
|
+
/**
|
|
1832
|
+
* Converge a `.related()` query's child bodies onto `fresh` (the server's
|
|
1833
|
+
* current child edges). Shared by the registration path above and the poll,
|
|
1834
|
+
* which reads the child edges in its batched round trip. See
|
|
1835
|
+
* {@link syncSubqueryChildren} for the deletion-safety rules.
|
|
1836
|
+
*/
|
|
1837
|
+
private applySubqueryChildren;
|
|
1788
1838
|
heartbeatQuery(queryHash: string): Promise<void>;
|
|
1789
1839
|
private cleanupQuery;
|
|
1790
1840
|
}
|
|
@@ -2711,14 +2761,20 @@ declare class Sp00kyClient<S extends SchemaStructure> {
|
|
|
2711
2761
|
query<Table extends TableNames<S>>(table: Table, options: QueryOptions<TableModel<GetTable<S, Table>>, false>, ttl?: QueryTimeToLive): QueryBuilder<S, Table, Sp00kyQueryResultPromise>;
|
|
2712
2762
|
private initQuery;
|
|
2713
2763
|
/**
|
|
2714
|
-
* Background tail of {@link initQuery}:
|
|
2715
|
-
* `config.instantHydrate`, and only when
|
|
2716
|
-
*
|
|
2717
|
-
*
|
|
2718
|
-
*
|
|
2719
|
-
*
|
|
2720
|
-
*
|
|
2721
|
-
*
|
|
2764
|
+
* Background tail of {@link initQuery}: enqueue the `register` down-event,
|
|
2765
|
+
* then instant-hydrate (opt-in via `config.instantHydrate`, and only when
|
|
2766
|
+
* the query is cold). Never rejects — both halves catch and log, so
|
|
2767
|
+
* `void`-ing the returned promise can't produce an unhandled rejection. By
|
|
2768
|
+
* default (hydrate off) the register lifecycle is the single freshness path;
|
|
2769
|
+
* the one-shot fetch is an optimization apps enable explicitly, and it runs
|
|
2770
|
+
* regardless of preload state — cache-first delivery never depends on WHY
|
|
2771
|
+
* rows are cached.
|
|
2772
|
+
*
|
|
2773
|
+
* Register goes first on purpose. The hydrate used to run strictly before
|
|
2774
|
+
* the enqueue, so a windowed list's one-shot read (a full-table scan when
|
|
2775
|
+
* the app has no matching index) held its own registration back by seconds
|
|
2776
|
+
* per window. Now whichever answer lands first paints; a hydrate that
|
|
2777
|
+
* arrives after the registration is dropped (`applyHydration`).
|
|
2722
2778
|
*/
|
|
2723
2779
|
private finishQueryInit;
|
|
2724
2780
|
/**
|
|
@@ -2789,6 +2845,15 @@ declare class Sp00kyClient<S extends SchemaStructure> {
|
|
|
2789
2845
|
*/
|
|
2790
2846
|
isLocalReady(): boolean;
|
|
2791
2847
|
useRemote<T>(fn: (client: Surreal) => Promise<T> | T): Promise<T>;
|
|
2848
|
+
/**
|
|
2849
|
+
* Run one raw SurrealQL statement on the remote through the client's own
|
|
2850
|
+
* query path: the connect gate (it never runs on a socket whose namespace
|
|
2851
|
+
* and token are not applied yet), the per-statement timeout and the
|
|
2852
|
+
* concurrency limit. Prefer this over {@link useRemote} for one-shot reads
|
|
2853
|
+
* such as counts; `useRemote` hands out the bare SDK client and raced a
|
|
2854
|
+
* reconnect with "Specify a namespace to use".
|
|
2855
|
+
*/
|
|
2856
|
+
remoteQuery<T extends unknown[]>(sql: string, vars?: Record<string, unknown>): Promise<T>;
|
|
2792
2857
|
/**
|
|
2793
2858
|
* Mint the salt used for query-id hashing, so two sessions registering the
|
|
2794
2859
|
* same logical query get distinct `_00_query` rows.
|