@spooky-sync/core 0.0.1-canary.237 → 0.0.1-canary.239
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 +1 -1
- package/dist/index.d.ts +134 -5
- package/dist/index.js +498 -83
- package/dist/otel/index.d.ts +1 -1
- package/dist/types.d.ts +40 -27
- package/package.json +3 -3
- package/src/modules/data/data.activity.test.ts +109 -0
- package/src/modules/data/data.authority.test.ts +122 -0
- package/src/modules/data/data.heartbeat-wake.test.ts +115 -0
- package/src/modules/data/data.membership.test.ts +168 -68
- package/src/modules/data/index.ts +248 -83
- package/src/modules/devtools/index.ts +3 -2
- package/src/modules/sync/engine.ts +12 -7
- package/src/modules/sync/sync.health.test.ts +13 -3
- package/src/modules/sync/sync.list-ref-meta.test.ts +193 -0
- package/src/modules/sync/sync.live-removal.test.ts +137 -23
- package/src/modules/sync/sync.ts +326 -60
- package/src/modules/sync/utils.test.ts +1 -1
- package/src/modules/sync/utils.ts +10 -2
- package/src/sp00ky.ts +64 -1
- package/src/types.ts +41 -26
- package/src/utils/error-classification.test.ts +10 -0
- package/src/utils/error-classification.ts +18 -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)`, `remoteQuery(sql, vars?)`, `useRemote(fn)`, `authenticate(token)`, `signOut()`. Plus `pendingMutationCount`
|
|
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` / `subscribeToPendingMutations(cb)` (outbox depth) and `fetchingQueryCount` / `subscribeToFetchActivity(cb)` (queries mid-fetch, aggregate) for traffic indicators.
|
|
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.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as ServerViewMeta, B as TimingPhase, C as QueryTimings, D as RecordVersionDiff, E as RecordVersionArray, F as StorageHealthStatus, G as DatabaseEventSystem, H as UpEvent, I as StoreType, J as SyncEventSystem, K as DatabaseEventTypes, L as SyncHealth, M as Sp00kyQueryResult, N as Sp00kyQueryResultPromise, O as RegistrationTimings, P as StorageHealth, R as SyncHealthConfig, S as QueryTimeToLive, T as ReconnectConfig, U as LocalStore, V as UpdateOptions, W as SealedQuery, X as EventSystem, Y as EventDefinition, _ as QueryConfigRecord, a as MATERIALIZATION_SAMPLE_WINDOW, b as QueryStatus, c as MutationEvent, d as PhaseStat, f as PinoTransmit, g as QueryConfig, h as QueryAuthorityCallback, i as Level, j as Sp00kyConfig, k as RunOptions, l as MutationEventType, m as PreloadRefresh, n as DebounceOptions, o as MembershipOutcome, p as PreloadOptions, q as Logger$1, r as EventSubscriptionOptions, s as MutationCallback, t as ConnectionState, u as PersistenceClient, v as QueryHash, w as QueryUpdateCallback, x as QueryStatusCallback, y as QueryState, z as SyncHealthStatus } from "./types.js";
|
|
2
2
|
import * as surrealdb0 from "surrealdb";
|
|
3
3
|
import { Duration, RecordId, Surreal as Surreal$1, SurrealEvents, SurrealTransaction } from "surrealdb";
|
|
4
4
|
import { AccessDefinition, BackendNames, BackendRoutes, BucketNames, ColumnSchema, FinalQuery, GetTable, QueryBuilder, QueryOptions, QueryPlan, RoutePayload, SchemaStructure, TableModel, TableNames, TypeNameToTypeMap } from "@spooky-sync/query-builder";
|
|
@@ -733,10 +733,18 @@ declare class DataModule<S extends SchemaStructure> {
|
|
|
733
733
|
private pendingQueries;
|
|
734
734
|
private subscriptions;
|
|
735
735
|
private statusSubscriptions;
|
|
736
|
+
/** Observers of a query's authority (`membershipKnown`) flips, keyed like `statusSubscriptions`. */
|
|
737
|
+
private authoritySubscriptions;
|
|
736
738
|
private mutationCallbacks;
|
|
737
739
|
private debounceTimers;
|
|
738
740
|
private pendingStreamUpdates;
|
|
739
741
|
private fetchDepth;
|
|
742
|
+
/**
|
|
743
|
+
* Observers of the AGGREGATE fetch activity: how many queries are mid-fetch
|
|
744
|
+
* right now. What a "downloading" indicator in the app chrome hangs off,
|
|
745
|
+
* instead of subscribing to every query's status one by one.
|
|
746
|
+
*/
|
|
747
|
+
private activitySubscriptions;
|
|
740
748
|
private logger;
|
|
741
749
|
/**
|
|
742
750
|
* Optional observer notified whenever a query's fetch status changes.
|
|
@@ -754,6 +762,15 @@ declare class DataModule<S extends SchemaStructure> {
|
|
|
754
762
|
* DataModule is constructed — mirrors `onQueryStatusChange`.
|
|
755
763
|
*/
|
|
756
764
|
onHeartbeat?: (hash: QueryHash) => void;
|
|
765
|
+
/**
|
|
766
|
+
* Fired once per episode when the server's `_00_query` row for a query we
|
|
767
|
+
* hold membership for is found missing (see `updateQueryRemoteArray`). Wired
|
|
768
|
+
* by Sp00kyClient to enqueue a `register` down-event so the SSP rebuilds the
|
|
769
|
+
* view; the client keeps rendering what it has meanwhile.
|
|
770
|
+
*/
|
|
771
|
+
onViewLost?: (hash: QueryHash) => void;
|
|
772
|
+
/** DevTools observer for authority flips; mirrors `onQueryStatusChange`. */
|
|
773
|
+
onQueryAuthorityChange?: (hash: QueryHash, known: boolean) => void;
|
|
757
774
|
/**
|
|
758
775
|
* Optional hook fired by {@link deregisterQuery} when an opt-in query (e.g. a
|
|
759
776
|
* viewport-windowed list cancelling an off-screen window) loses its last
|
|
@@ -806,6 +823,22 @@ declare class DataModule<S extends SchemaStructure> {
|
|
|
806
823
|
subscribeStatus(queryHash: string, callback: QueryStatusCallback, options?: {
|
|
807
824
|
immediate?: boolean;
|
|
808
825
|
}): () => void;
|
|
826
|
+
/**
|
|
827
|
+
* Subscribe to a query's authority flips: whether server membership is known
|
|
828
|
+
* for it. With `{ immediate: true }` the callback fires synchronously with
|
|
829
|
+
* the current value (`false` for an unknown query).
|
|
830
|
+
*/
|
|
831
|
+
subscribeAuthority(queryHash: string, callback: QueryAuthorityCallback, options?: {
|
|
832
|
+
immediate?: boolean;
|
|
833
|
+
}): () => void;
|
|
834
|
+
/** Whether server membership is known for `queryHash` (see `QueryConfig.membershipKnown`). */
|
|
835
|
+
isAuthoritative(queryHash: string): boolean;
|
|
836
|
+
/**
|
|
837
|
+
* The single writer of `config.membershipKnown` for a live query. Fans out
|
|
838
|
+
* to `subscribeAuthority` listeners and DevTools on a change; no-op when
|
|
839
|
+
* unchanged or the query is unknown.
|
|
840
|
+
*/
|
|
841
|
+
private setMembershipKnown;
|
|
809
842
|
/**
|
|
810
843
|
* Set a query's fetch status and notify status observers (DevTools +
|
|
811
844
|
* `subscribeStatus` listeners). No-op when the status is unchanged or the
|
|
@@ -821,6 +854,18 @@ declare class DataModule<S extends SchemaStructure> {
|
|
|
821
854
|
beginFetching(queryHash: string): void;
|
|
822
855
|
/** Leave a fetch cycle started with {@link beginFetching}; emits `idle` on the last exit. */
|
|
823
856
|
endFetching(queryHash: string): void;
|
|
857
|
+
/** How many queries are mid-fetch right now (see {@link subscribeActivity}). */
|
|
858
|
+
get fetchingQueryCount(): number;
|
|
859
|
+
/**
|
|
860
|
+
* Observe the number of queries currently fetching. Fires on every change of
|
|
861
|
+
* that number (a query entering or leaving its outermost fetch cycle), and
|
|
862
|
+
* synchronously with the current value when `immediate` is set. One
|
|
863
|
+
* subscription per indicator, however many queries the page holds.
|
|
864
|
+
*/
|
|
865
|
+
subscribeActivity(callback: (fetching: number) => void, options?: {
|
|
866
|
+
immediate?: boolean;
|
|
867
|
+
}): () => void;
|
|
868
|
+
private notifyActivity;
|
|
824
869
|
/**
|
|
825
870
|
* Subscribe to mutations (for sync)
|
|
826
871
|
*/
|
|
@@ -1117,9 +1162,15 @@ declare class DataModule<S extends SchemaStructure> {
|
|
|
1117
1162
|
getActiveQueryHashes(): QueryHash[];
|
|
1118
1163
|
updateQueryLocalArray(id: string, localArray: RecordVersionArray): Promise<void>;
|
|
1119
1164
|
updateQueryRemoteArray(hash: string, remoteArray: RecordVersionArray, opts?: {
|
|
1120
|
-
/** `_00_query
|
|
1121
|
-
|
|
1122
|
-
|
|
1165
|
+
/** The `_00_query` row as read in the same round trip as the edges. */
|
|
1166
|
+
meta?: ServerViewMeta;
|
|
1167
|
+
/**
|
|
1168
|
+
* The caller verified upstream that the ids missing from `remoteArray`
|
|
1169
|
+
* no longer exist (see `Sp00kySync` LIVE removal handling); an empty set
|
|
1170
|
+
* is then a real answer even without a readable row.
|
|
1171
|
+
*/
|
|
1172
|
+
verifiedRemoval?: boolean;
|
|
1173
|
+
}): Promise<MembershipOutcome>;
|
|
1123
1174
|
/**
|
|
1124
1175
|
* Cancel every armed timer ahead of a local-bucket switch: stream-update
|
|
1125
1176
|
* debounce timers (their pending updates carry the OLD bucket's id-sets) and
|
|
@@ -1205,6 +1256,26 @@ declare class DataModule<S extends SchemaStructure> {
|
|
|
1205
1256
|
private calculateMembershipKey;
|
|
1206
1257
|
private sha256;
|
|
1207
1258
|
private startTTLHeartbeat;
|
|
1259
|
+
/**
|
|
1260
|
+
* Heartbeat every watched query NOW and re-anchor its timer.
|
|
1261
|
+
*
|
|
1262
|
+
* The TTL timer is the only thing keeping a view alive on the server, and
|
|
1263
|
+
* browsers throttle timers in hidden tabs (Chrome aligns them to one-minute
|
|
1264
|
+
* ticks after five minutes hidden, and suspends them outright on some
|
|
1265
|
+
* mobile platforms). A tab that comes back after a long stretch may have
|
|
1266
|
+
* missed its beat by more than the server's slack, and the sweep will have
|
|
1267
|
+
* taken the row and its edges. The client does not blank on that any more
|
|
1268
|
+
* (`updateQueryRemoteArray` treats a missing row as "view lost"), but the
|
|
1269
|
+
* sooner it finds out, the sooner the view is rebuilt: `heartbeatQuery`
|
|
1270
|
+
* re-registers a query whose row is gone. Called on `visibilitychange` (to
|
|
1271
|
+
* visible) and `online`.
|
|
1272
|
+
*
|
|
1273
|
+
* Queries beaten within the last `minGapMs` are skipped, so a flurry of
|
|
1274
|
+
* wake events costs one beat per query. There is deliberately no local
|
|
1275
|
+
* expiry anywhere in this module: the TTL is the server's, and the client's
|
|
1276
|
+
* only duty is to keep asserting liveness while something is watching.
|
|
1277
|
+
*/
|
|
1278
|
+
heartbeatOnWake(minGapMs?: number): number;
|
|
1208
1279
|
private replaceRecordInQueries;
|
|
1209
1280
|
}
|
|
1210
1281
|
/**
|
|
@@ -1653,6 +1724,25 @@ declare class Sp00kySync<S extends SchemaStructure> {
|
|
|
1653
1724
|
* changed.
|
|
1654
1725
|
*/
|
|
1655
1726
|
private applyListRefSnapshot;
|
|
1727
|
+
/**
|
|
1728
|
+
* Re-read ONE query's server state now (edges + `_00_query` row) and land
|
|
1729
|
+
* it through {@link applyListRefSnapshot}. The targeted form of the poll,
|
|
1730
|
+
* for the moments that cannot wait for the next tick: a registration that
|
|
1731
|
+
* answered `materializing`, and a LIVE removal that has to be checked
|
|
1732
|
+
* against the row before it may empty a painted list.
|
|
1733
|
+
*/
|
|
1734
|
+
private refetchListRefForQuery;
|
|
1735
|
+
/** Per-query pending targeted re-reads: one timer and the ladder still to walk. */
|
|
1736
|
+
private listRefRefetches;
|
|
1737
|
+
/**
|
|
1738
|
+
* Schedule a targeted re-read of `queryHash` after `delays[0]` ms. If the
|
|
1739
|
+
* read still says the server is publishing (`materializing`), the next rung
|
|
1740
|
+
* of the ladder is armed; any other outcome ends it. Re-scheduling a hash
|
|
1741
|
+
* that already has a timer replaces it (latest wins), so bursts coalesce.
|
|
1742
|
+
*/
|
|
1743
|
+
private scheduleListRefRefetch;
|
|
1744
|
+
private clearListRefRefetch;
|
|
1745
|
+
private clearListRefRefetches;
|
|
1656
1746
|
/**
|
|
1657
1747
|
* Resolve the current `_00_list_ref` table name for the active auth
|
|
1658
1748
|
* context. Public so the `createRemoteQuery` initial-fetch path can
|
|
@@ -1708,6 +1798,26 @@ declare class Sp00kySync<S extends SchemaStructure> {
|
|
|
1708
1798
|
private refetchAfterReconnect;
|
|
1709
1799
|
private startRefLiveQueries;
|
|
1710
1800
|
private handleRemoteListRefChange;
|
|
1801
|
+
/** LIVE removals awaiting verification, per query, with their coalescing timer. */
|
|
1802
|
+
private pendingLiveRemovals;
|
|
1803
|
+
/**
|
|
1804
|
+
* Hold a LIVE `_00_list_ref` DELETE for {@link LIVE_REMOVAL_COALESCE_MS},
|
|
1805
|
+
* then verify the batch (see {@link flushLiveRemovals}). A sweep or a
|
|
1806
|
+
* republish delivers a view's whole set as one burst, so one verification
|
|
1807
|
+
* round covers it.
|
|
1808
|
+
*/
|
|
1809
|
+
private queueLiveRemoval;
|
|
1810
|
+
/**
|
|
1811
|
+
* Verify a coalesced batch of LIVE removals against the upstream records.
|
|
1812
|
+
* Ids that no longer exist upstream are dropped from membership (the body
|
|
1813
|
+
* delete already happened inside `syncRecords`) and the query re-renders.
|
|
1814
|
+
* Anything still present upstream - or a verification that could not run -
|
|
1815
|
+
* is settled by a targeted re-read of the server's set for this query,
|
|
1816
|
+
* where the `_00_query` row decides: gone = view lost (keep rows,
|
|
1817
|
+
* re-register), `materializing` = wait, `ready` = adopt the smaller set.
|
|
1818
|
+
*/
|
|
1819
|
+
private flushLiveRemovals;
|
|
1820
|
+
private clearLiveRemovals;
|
|
1711
1821
|
/**
|
|
1712
1822
|
* Handle a LIVE change to a SUBQUERY child edge (a `_00_list_ref` row with
|
|
1713
1823
|
* `parent` set) for a `.related()` query. Unlike primary rows, child rows
|
|
@@ -2623,6 +2733,15 @@ declare class Sp00kyClient<S extends SchemaStructure> {
|
|
|
2623
2733
|
* suite can guard the pre-emptive path against regression. */
|
|
2624
2734
|
get liveRetryCount(): number;
|
|
2625
2735
|
subscribeToPendingMutations(cb: (count: number) => void): () => void;
|
|
2736
|
+
/** Number of queries mid-fetch right now. See {@link subscribeToFetchActivity}. */
|
|
2737
|
+
get fetchingQueryCount(): number;
|
|
2738
|
+
/**
|
|
2739
|
+
* Observe how many queries are fetching from the server. Fires immediately
|
|
2740
|
+
* with the current count and again on every change, so one subscription can
|
|
2741
|
+
* drive a "downloading" indicator for the whole app. Pair with
|
|
2742
|
+
* {@link subscribeToPendingMutations} for the "uploading" half.
|
|
2743
|
+
*/
|
|
2744
|
+
subscribeToFetchActivity(cb: (fetching: number) => void): () => void;
|
|
2626
2745
|
/** Current sync-health snapshot. See {@link Sp00kyConfig.syncHealth}. */
|
|
2627
2746
|
get syncHealth(): SyncHealth;
|
|
2628
2747
|
/**
|
|
@@ -2821,6 +2940,16 @@ declare class Sp00kyClient<S extends SchemaStructure> {
|
|
|
2821
2940
|
subscribeQueryStatus(queryHash: string, callback: QueryStatusCallback, options?: {
|
|
2822
2941
|
immediate?: boolean;
|
|
2823
2942
|
}): () => void;
|
|
2943
|
+
/**
|
|
2944
|
+
* Subscribe to a query's authority flips: `true` once server membership is
|
|
2945
|
+
* known for it (a registration or poll landed, or a durable `_00_window`
|
|
2946
|
+
* seed was read on boot), `false` when it is reset (bucket switch). With
|
|
2947
|
+
* `{ immediate: true }` the callback fires synchronously with the current
|
|
2948
|
+
* value. Powers the bindings' `isAuthoritative()` / cold-only `isLoading()`.
|
|
2949
|
+
*/
|
|
2950
|
+
subscribeQueryAuthority(queryHash: string, callback: QueryAuthorityCallback, options?: {
|
|
2951
|
+
immediate?: boolean;
|
|
2952
|
+
}): () => void;
|
|
2824
2953
|
/**
|
|
2825
2954
|
* Report the frontend processing time (ms) a client framework spent applying
|
|
2826
2955
|
* an update for a query (e.g. `useQuery`'s `reconcile()`), so DevTools/MCP can
|
|
@@ -2936,4 +3065,4 @@ declare function textToHtml(text: string): string;
|
|
|
2936
3065
|
*/
|
|
2937
3066
|
|
|
2938
3067
|
//#endregion
|
|
2939
|
-
export { 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, BucketHandle, BucketPutOptions, BucketPutResult, CURSOR_COLORS, ConnectionState, CrdtField, CrdtManager, DEFAULT_LOCAL_OP_TIMEOUT_MS, DebounceOptions, EventSubscriptionOptions, FeatureFlagHandle, FeatureFlagModule, type FeatureFlagOptions, type FeatureFlagOverride, type FeatureFlagSnapshot, Level, LocalOpTimeoutError, MATERIALIZATION_SAMPLE_WINDOW, MutationCallback, MutationEvent, MutationEventType, PersistenceClient, PhaseStat, PinoTransmit, PreloadOptions, PreloadRefresh, QueryConfig, QueryConfigRecord, QueryHash, QueryState, QueryStatus, QueryStatusCallback, QueryTimeToLive, QueryTimings, QueryUpdateCallback, ReconnectConfig, RecordVersionArray, RecordVersionDiff, RegistrationTimings, RunOptions, Sp00kyClient, Sp00kyConfig, Sp00kyQueryResult, Sp00kyQueryResultPromise, StorageHealth, StorageHealthStatus, StoreType, SyncHealth, SyncHealthConfig, SyncHealthStatus, TimingPhase, UpdateOptions, blurhashSidecarPath, bucketContentToBlob, createAuthEventSystem, cursorColorFromName, decode as decodeBlurhash, encode as encodeBlurhash, encodeImageToBlurhash, fileToUint8Array, isBlurhashValid, isImagePath, semverGt, textToHtml };
|
|
3068
|
+
export { 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, BucketHandle, BucketPutOptions, BucketPutResult, CURSOR_COLORS, ConnectionState, CrdtField, CrdtManager, DEFAULT_LOCAL_OP_TIMEOUT_MS, DebounceOptions, EventSubscriptionOptions, FeatureFlagHandle, FeatureFlagModule, type FeatureFlagOptions, type FeatureFlagOverride, type FeatureFlagSnapshot, Level, LocalOpTimeoutError, MATERIALIZATION_SAMPLE_WINDOW, MembershipOutcome, MutationCallback, MutationEvent, MutationEventType, PersistenceClient, PhaseStat, PinoTransmit, PreloadOptions, PreloadRefresh, QueryAuthorityCallback, QueryConfig, QueryConfigRecord, QueryHash, QueryState, QueryStatus, QueryStatusCallback, QueryTimeToLive, QueryTimings, QueryUpdateCallback, ReconnectConfig, RecordVersionArray, RecordVersionDiff, RegistrationTimings, RunOptions, ServerViewMeta, Sp00kyClient, Sp00kyConfig, Sp00kyQueryResult, Sp00kyQueryResultPromise, StorageHealth, StorageHealthStatus, StoreType, SyncHealth, SyncHealthConfig, SyncHealthStatus, TimingPhase, UpdateOptions, blurhashSidecarPath, bucketContentToBlob, createAuthEventSystem, cursorColorFromName, decode as decodeBlurhash, encode as encodeBlurhash, encodeImageToBlurhash, fileToUint8Array, isBlurhashValid, isImagePath, semverGt, textToHtml };
|