@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 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` 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` / `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 Sp00kyQueryResultPromise, B as LocalStore, C as ReconnectConfig, D as RunOptions, E as RegistrationTimings, F as SyncHealthConfig, G as SyncEventSystem, H as DatabaseEventSystem, I as SyncHealthStatus, K as EventDefinition, L as TimingPhase, M as StorageHealthStatus, N as StoreType, O as Sp00kyConfig, P as SyncHealth, R as UpdateOptions, S as QueryUpdateCallback, T as RecordVersionDiff, U as DatabaseEventTypes, V as SealedQuery, W as Logger$1, _ as QueryState, a as MATERIALIZATION_SAMPLE_WINDOW, b as QueryTimeToLive, c as MutationEventType, d as PinoTransmit, f as PreloadOptions, g as QueryHash, h as QueryConfigRecord, i as Level, j as StorageHealth, k as Sp00kyQueryResult, l as PersistenceClient, m as QueryConfig, n as DebounceOptions, o as MutationCallback, p as PreloadRefresh, q as EventSystem, r as EventSubscriptionOptions, s as MutationEvent, t as ConnectionState, u as PhaseStat, v as QueryStatus, w as RecordVersionArray, x as QueryTimings, y as QueryStatusCallback, z as UpEvent } from "./types.js";
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.rowCount` read in the same round trip; `null` = unknown. */
1121
- serverRowCount?: number | null;
1122
- }): Promise<void>;
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 };