@spooky-sync/core 0.0.1-canary.331 → 0.0.1-canary.332

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
@@ -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. A bucket generated with `backend: 'r2'` (`BACKEND "r2"`) sends its bytes to the SSP over HTTP (`<files.endpoint>/bucket/<name>/<path>`, bearer = the session token from `RemoteDatabaseService.getAuthToken()`) instead of `f"bucket:/path"` statements; the SSP checks `fn::_00_bucket::<name>` as the user first.
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. `subscribe(cb, { immediate? })` calls `cb` at once with the current user unless `immediate: false`, then on every change.
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.
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` (the first edit after a quiet spell waits this long, default 50), `crdtSendIntervalMs` (shortest gap between two sends of one field while typing, default 200). 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`.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as WebPushSubscribeOptions, A as RunOptions, At as Logger$1, B as SyncHealthStatus, Bt as SESSION_POLL_MS, C as QueryTimeToLive, Ct as SignOutHook, D as RecordVersionArray, Dt as createAuthEventSystem, E as ReconnectConfig, Et as AuthEventTypes, F as StorageHealth, Ft as IMPERSONATION_ACCESS, G as WebPushConfig, Gt as sameImpersonation, H as UpdateOptions, Ht as decodeTokenClaims, I as StorageHealthStatus, It as ImpersonationInfo, J as WebPushErrorCode, K as WebPushDevice, L as StoreType, Lt as ImpersonationTimerHost, M as Sp00kyConfig, Mt as SealedQuery, N as Sp00kyQueryResult, Nt as EventDefinition, O as RecordVersionDiff, Ot as RemoteDatabaseService, P as Sp00kyQueryResultPromise, Pt as EventSystem, Q as WebPushModule, R as SyncHealth, Rt as ImpersonationTimers, S as QueryStatusCallback, St as SignOutContext, T as QueryUpdateCallback, Tt as AuthEventTypeMap, U as PUSH_ENDPOINT_KEY, Ut as impersonationFromToken, V as TimingPhase, Vt as TokenClaims, W as PushDeviceKind, Wt as renewDelayMs, X as WebPushMessage, Y as WebPushInfo, Z as WebPushMessageInput, _ as QueryConfig, _t as isPushPayload, a as Level, at as WebPushUpdateOptions, b as QueryState, bt as ImpersonationCandidate, c as MutationCallback, ct as BridgeTokenMessage, d as PersistenceClient, dt as PushNotificationAction, et as WebPushSupport, f as PhaseStat, ft as PushOp, g as QueryAuthorityCallback, gt as WebPushClientMessage, h as PreloadRefresh, ht as PushUrgency, i as InlineRow, it as WebPushUnsupportedReason, j as ServerViewMeta, jt as LocalStore, k as RegistrationTimings, kt as LogTap, l as MutationEvent, lt as PUSH_PAYLOAD_VERSION, m as PreloadOptions, mt as PushPayloadKind, n as DebounceOptions, nt as WebPushSyncStatus, o as MATERIALIZATION_SAMPLE_WINDOW, ot as detectWebPushSupport, p as PinoTransmit, pt as PushPayload, q as WebPushError, r as EventSubscriptionOptions, rt as WebPushUnsubscribeOptions, s as MembershipOutcome, st as BridgeSignOutMessage, t as ConnectionState, tt as WebPushSyncOptions, u as MutationEventType, ut as PushNotification, v as QueryConfigRecord, vt as ActiveImpersonation, w as QueryTimings, wt as AuthEventSystem, x as QueryStatus, xt as SIGN_OUT_HOOK_TIMEOUT_MS, y as QueryHash, yt as AuthService, z as SyncHealthConfig, zt as ORIGIN_TOKEN_KEY } from "./types.js";
1
+ import { $ as WebPushModule, A as RegistrationTimings, At as LogTap, B as SyncHealthConfig, Bt as ORIGIN_TOKEN_KEY, C as QueryStatusCallback, Ct as SignOutContext, D as ReconnectConfig, Dt as AuthEventTypes, E as QueryUpdateCallback, Et as AuthEventTypeMap, F as Sp00kyQueryResultPromise, Ft as EventSystem, G as PushDeviceKind, Gt as renewDelayMs, H as TimingPhase, Ht as TokenClaims, I as StorageHealth, It as IMPERSONATION_ACCESS, J as WebPushError, K as WebPushConfig, Kt as sameImpersonation, L as StorageHealthStatus, Lt as ImpersonationInfo, M as ServerViewMeta, Mt as LocalStore, N as Sp00kyConfig, Nt as SealedQuery, O as RecordVersionArray, Ot as createAuthEventSystem, P as Sp00kyQueryResult, Pt as EventDefinition, Q as WebPushMessageInput, R as StoreType, Rt as ImpersonationTimerHost, S as QueryStatus, St as SIGN_OUT_HOOK_TIMEOUT_MS, T as QueryTimings, Tt as AuthEventSystem, U as UpdateOptions, Ut as decodeTokenClaims, V as SyncHealthStatus, Vt as SESSION_POLL_MS, W as PUSH_ENDPOINT_KEY, Wt as impersonationFromToken, X as WebPushInfo, Y as WebPushErrorCode, Z as WebPushMessage, _ as QueryAuthorityCallback, _t as WebPushClientMessage, a as InlineRow, at as WebPushUnsupportedReason, b as QueryHash, bt as AuthService, c as MembershipOutcome, ct as BridgeSignOutMessage, d as MutationEventType, dt as PushNotification, et as WebPushSubscribeOptions, f as PersistenceClient, ft as PushNotificationAction, g as PreloadRefresh, gt as PushUrgency, h as PreloadOptions, ht as PushPayloadKind, i as EventSubscriptionOptions, it as WebPushUnsubscribeOptions, j as RunOptions, jt as Logger$1, k as RecordVersionDiff, kt as RemoteDatabaseService, l as MutationCallback, lt as BridgeTokenMessage, m as PinoTransmit, mt as PushPayload, n as DebounceOptions, nt as WebPushSyncOptions, o as Level, ot as WebPushUpdateOptions, p as PhaseStat, pt as PushOp, q as WebPushDevice, r as EdgeDelta, rt as WebPushSyncStatus, s as MATERIALIZATION_SAMPLE_WINDOW, st as detectWebPushSupport, t as ConnectionState, tt as WebPushSupport, u as MutationEvent, ut as PUSH_PAYLOAD_VERSION, v as QueryConfig, vt as isPushPayload, w as QueryTimeToLive, wt as SignOutHook, x as QueryState, xt as ImpersonationCandidate, y as QueryConfigRecord, yt as ActiveImpersonation, z as SyncHealth, zt as ImpersonationTimers } from "./types.js";
2
2
  import { BackendNames, BackendRoutes, BucketNames, FinalQuery, GetTable, QueryBuilder, QueryOptions, QueryPlan, RecordId, RoutePayload, SchemaStructure, TableModel, TableNames } from "@spooky-sync/query-builder";
3
3
  import * as surrealdb0 from "surrealdb";
4
4
  import { Duration, RecordId as RecordId$1, Surreal } from "surrealdb";
@@ -608,7 +608,10 @@ declare class CrdtManager {
608
608
  private schema;
609
609
  private local;
610
610
  private remote;
611
+ /** First edit after a quiet spell: wait this long to batch the burst. */
611
612
  private debounceMs;
613
+ /** Shortest gap between two sends of one field, while edits keep coming. */
614
+ private sendIntervalMs;
612
615
  private sessions;
613
616
  private opening;
614
617
  /** Closes still saving their field's local state, by session key. */
@@ -620,7 +623,9 @@ declare class CrdtManager {
620
623
  private claim;
621
624
  private claimRetry;
622
625
  private connectionUnsubscribes;
623
- constructor(schema: SchemaStructure, local: LocalStore, remote: RemoteDatabaseService, logger: Logger$1, debounceMs?: number);
626
+ constructor(schema: SchemaStructure, local: LocalStore, remote: RemoteDatabaseService, logger: Logger$1, /** First edit after a quiet spell: wait this long to batch the burst. */
627
+ debounceMs?: number, /** Shortest gap between two sends of one field, while edits keep coming. */
628
+ sendIntervalMs?: number);
624
629
  /** Hand over the runtime ports. Called once by the client facade. */
625
630
  attach(ports: CrdtPorts): void;
626
631
  /**
@@ -680,7 +685,16 @@ declare class CrdtManager {
680
685
  * be merged away; that is logged.
681
686
  */
682
687
  private verify;
688
+ /**
689
+ * Arm the send. Edits go out on a steady cadence while they keep coming:
690
+ * the first one after a quiet spell waits `debounceMs` (one row for the
691
+ * burst), later ones at least `sendIntervalMs` after the previous send. An
692
+ * armed timer is never pushed back, so continuous typing cannot hold the
693
+ * edits; it is only brought forward (`delay`, e.g. 0 on reconnect).
694
+ */
683
695
  private scheduleFlush;
696
+ /** Send what is unsent. A call while a send runs queues one more after it,
697
+ * so edits made during that send are not left waiting for the next edit. */
684
698
  private flush;
685
699
  /** Send every own peer's ops that are not in the outbox yet, one row per
686
700
  * peer. Only the peer's own ops travel (`updates-in-range`): a plain
@@ -961,6 +975,7 @@ type RuntimeEvent = {
961
975
  type: 'LiveChange';
962
976
  hashes: QueryHash[];
963
977
  rows?: InlineRow[];
978
+ deltas?: EdgeDelta[];
964
979
  } | {
965
980
  type: 'TabRole';
966
981
  role: 'solo' | 'leader' | 'follower';
@@ -1442,7 +1457,7 @@ interface Adapters {
1442
1457
  * changed query hashes, plus the changed rows themselves when the
1443
1458
  * subscription was opened with the body joined on (`liveInlineBodies`).
1444
1459
  */
1445
- live(table: string, onChange: (hashes: string[], rows?: InlineRow[]) => void): Promise<string>;
1460
+ live(table: string, onChange: (hashes: string[], rows?: InlineRow[], deltas?: EdgeDelta[]) => void): Promise<string>;
1446
1461
  kill(uuid: string): Promise<void>;
1447
1462
  };
1448
1463
  ssp: {
@@ -2197,4 +2212,4 @@ declare class LocalOpTimeoutError extends Error {
2197
2212
  * select on a throttled tab is seconds, not tens of seconds. */
2198
2213
  declare const DEFAULT_LOCAL_OP_TIMEOUT_MS = 30000;
2199
2214
  //#endregion
2200
- 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 };
2215
+ 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, EdgeDelta, 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 };
package/dist/index.js CHANGED
@@ -2188,8 +2188,8 @@ function trimHistory(history, cap) {
2188
2188
 
2189
2189
  //#endregion
2190
2190
  //#region src/modules/devtools/index.ts
2191
- const CORE_VERSION = "0.0.1-canary.331";
2192
- const WASM_VERSION = "0.0.1-canary.331";
2191
+ const CORE_VERSION = "0.0.1-canary.332";
2192
+ const WASM_VERSION = "0.0.1-canary.332";
2193
2193
  const SURREAL_VERSION = "3.1.0";
2194
2194
  var DevToolsService = class DevToolsService {
2195
2195
  version = CORE_VERSION;
@@ -4168,6 +4168,35 @@ function decideMembershipOutcome(input) {
4168
4168
  return meta.rowCount === 0 && meta.state !== "materializing" ? "applied" : "ignored";
4169
4169
  }
4170
4170
  /**
4171
+ * Commit LIVE edge deltas onto a held id-set: each names a row that joined
4172
+ * (or moved to a newer version). Returns `held` itself when nothing moves.
4173
+ */
4174
+ function withDeltas(held, deltas) {
4175
+ const next = new Map(held);
4176
+ let moved = false;
4177
+ for (const { id, version } of deltas) {
4178
+ if ((next.get(id) ?? -1) >= version) continue;
4179
+ next.set(id, version);
4180
+ moved = true;
4181
+ }
4182
+ return moved ? Array.from(next.entries()) : held;
4183
+ }
4184
+ /**
4185
+ * A membership read answers for the moment the server ran it, which may be
4186
+ * before a LIVE delta that was committed while the read was in flight. Carry
4187
+ * what changed in state since the read was sent (`before` -> `now`) over onto
4188
+ * the read, so a late answer never takes back a row that just joined.
4189
+ */
4190
+ function rebaseRead(read, before, now) {
4191
+ if (before === now) return read;
4192
+ const was = new Map(before);
4193
+ const is = new Map(now);
4194
+ const out = new Map(read);
4195
+ for (const [id] of before) if (!is.has(id)) out.delete(id);
4196
+ for (const [id, version] of now) if (version > (was.get(id) ?? -1) && version > (out.get(id) ?? -1)) out.set(id, version);
4197
+ return Array.from(out.entries());
4198
+ }
4199
+ /**
4171
4200
  * A statement that did not answer (an ERR, or no result at all). Kept apart
4172
4201
  * from a `null` result, which is the server saying "no such row": reading a
4173
4202
  * failed statement as a missing row turned every timeout under load into a
@@ -4854,9 +4883,38 @@ const stmt$1 = (results, i) => {
4854
4883
  return r && r.status === "OK" ? r.result : FAILED;
4855
4884
  };
4856
4885
  /**
4886
+ * Commit LIVE edge deltas (rows joining a view, or moving to a newer
4887
+ * version) straight to membership, without re-reading the edges: the
4888
+ * notification already says what the read would. Returns the hashes it
4889
+ * settled; any other change still goes through {@link readMembership}.
4890
+ *
4891
+ * Only for a query whose membership is known and current (`live`, not
4892
+ * materializing, no view-lost recovery pending): before that the read decides
4893
+ * what the set is. Removals are never applied here, so a view whose edges
4894
+ * are being rewritten never loses (and garbage-collects) bodies mid-rewrite.
4895
+ * Nor for a query with a `LIMIT`: a row joining it pushes another out, and
4896
+ * rendering the joiner before the read removes the leaver would show one row
4897
+ * too many (the id-set render drops the `LIMIT`).
4898
+ */
4899
+ function* applyEdgeDeltas(deltas) {
4900
+ const settled = /* @__PURE__ */ new Set();
4901
+ const byHash = /* @__PURE__ */ new Map();
4902
+ for (const d of deltas) byHash.set(d.hash, [...byHash.get(d.hash) ?? [], d]);
4903
+ for (const [hash, list] of byHash) {
4904
+ const entry = yield fx.state.read((s) => s.queries.get(hash));
4905
+ if (!entry || entry.lifecycle.phase !== "live" || entry.serverState === "materializing" || entry.viewLostRetryAt !== null) continue;
4906
+ if (entry.def.plan ? entry.def.plan.limit != null : /\bLIMIT\b/i.test(entry.def.surql)) continue;
4907
+ const next = withDeltas(entry.remoteArray, list);
4908
+ if (next !== entry.remoteArray) yield* applyMembership(hash, next);
4909
+ settled.add(hash);
4910
+ }
4911
+ return settled;
4912
+ }
4913
+ /**
4857
4914
  * Read the server membership of many queries in as few round trips as the
4858
- * row budget allows, then apply each changed set. This is the ONLY place a
4859
- * server id-set enters state: registration, the poll, LIVE dirt and view-lost
4915
+ * row budget allows, then apply each changed set. Every server id-set enters
4916
+ * state here, except the rows LIVE reports joining (see
4917
+ * {@link applyEdgeDeltas}): registration, the poll, LIVE dirt and view-lost
4860
4918
  * recovery all come through here.
4861
4919
  */
4862
4920
  function* readMembership(env, hashes, opts = {}) {
@@ -4919,9 +4977,10 @@ function* readMembership(env, hashes, opts = {}) {
4919
4977
  for (const [hash, snap] of snapshots) {
4920
4978
  const current = yield fx.state.read((s) => s.queries.get(hash));
4921
4979
  if (!current) continue;
4980
+ const primary = rebaseRead(snap.primary, byHash.get(hash).remoteArray, current.remoteArray);
4922
4981
  const metaChanged = current.serverState !== (snap.meta.present ? snap.meta.state : null);
4923
- if (!recordVersionArraysEqual(snap.primary, current.remoteArray) || metaChanged || current.lifecycle.phase === "cold" || current.lifecycle.phase === "cached") {
4924
- const outcome = yield* applyMembership(hash, snap.primary, snap.meta);
4982
+ if (!recordVersionArraysEqual(primary, current.remoteArray) || metaChanged || current.lifecycle.phase === "cold" || current.lifecycle.phase === "cached") {
4983
+ const outcome = yield* applyMembership(hash, primary, snap.meta);
4925
4984
  if (outcome === "applied") changed = true;
4926
4985
  if (outcome === "ignored" && snap.meta.present && snap.meta.state === "materializing") stillMaterializing.push(hash);
4927
4986
  }
@@ -6158,10 +6217,11 @@ function createInterpreter(adapters, host) {
6158
6217
  const pending = adapters.remote.queryResponses(effect.sql, effect.vars);
6159
6218
  return effect.timeoutMs ? withTimeout(pending, effect.timeoutMs, () => /* @__PURE__ */ new Error(`Remote request timed out after ${effect.timeoutMs}ms`)) : pending;
6160
6219
  }
6161
- case "remote.live": return adapters.remote.live(effect.table, (hashes, rows) => host.dispatch({
6220
+ case "remote.live": return adapters.remote.live(effect.table, (hashes, rows, deltas) => host.dispatch({
6162
6221
  type: "LiveChange",
6163
6222
  hashes,
6164
- rows
6223
+ rows,
6224
+ deltas
6165
6225
  }));
6166
6226
  case "remote.kill": return adapters.remote.kill(effect.uuid);
6167
6227
  case "ssp.register": {
@@ -6711,9 +6771,10 @@ function* pollTick(env) {
6711
6771
  //#endregion
6712
6772
  //#region src/sync/live.saga.ts
6713
6773
  /**
6714
- * LIVE on the session's `_00_list_ref` table. An event marks its query's
6715
- * membership dirty and the batched re-read decides; followers get the same
6716
- * dirt relayed by the leader.
6774
+ * LIVE on the session's `_00_list_ref` table. A row joining a live view is
6775
+ * committed straight from its edge; any other event marks its query's
6776
+ * membership dirty and the batched re-read decides. Followers get the dirt
6777
+ * relayed by the leader.
6717
6778
  *
6718
6779
  * When the subscription was opened with the body joined on
6719
6780
  * (`liveInlineBodies`) the event ALSO carries the changed row. That is an
@@ -6765,8 +6826,12 @@ function* liveInvalidate() {
6765
6826
  liveTable: null
6766
6827
  }));
6767
6828
  }
6768
- /** One or more edges of these queries changed on the server. */
6769
- function* liveChange(env, hashes, rows) {
6829
+ /**
6830
+ * One or more edges of these queries changed on the server. A row joining a
6831
+ * live view (`deltas`) is committed at once; anything else marks the query
6832
+ * dirty for the batched re-read.
6833
+ */
6834
+ function* liveChange(env, hashes, rows, deltas) {
6770
6835
  const [known, role, streak] = yield fx.state.read((s) => [
6771
6836
  hashes.filter((h) => s.queries.has(h)),
6772
6837
  s.tabRole,
@@ -6778,7 +6843,9 @@ function* liveChange(env, hashes, rows) {
6778
6843
  yield fx.timer.set("poll", env.pollBaseMs, { type: "PollTick" });
6779
6844
  }
6780
6845
  if (rows && rows.length > 0) yield* landInlineRows(env, rows);
6781
- yield* markMembershipDirty(known);
6846
+ const settled = deltas && deltas.length > 0 ? yield* applyEdgeDeltas(deltas.filter((d) => known.includes(d.hash))) : /* @__PURE__ */ new Set();
6847
+ const dirty = known.filter((h) => !settled.has(h));
6848
+ if (dirty.length > 0) yield* markMembershipDirty(dirty);
6782
6849
  if (role === "leader") yield fx.emit({
6783
6850
  type: "tabs:broadcast",
6784
6851
  message: {
@@ -7326,7 +7393,7 @@ function route(env, event) {
7326
7393
  lane: serial("live")
7327
7394
  };
7328
7395
  case "LiveChange": return {
7329
- saga: liveChange(env, event.hashes, event.rows),
7396
+ saga: liveChange(env, event.hashes, event.rows, event.deltas),
7330
7397
  lane: serial("live-change")
7331
7398
  };
7332
7399
  case "TabRole": return {
@@ -12625,8 +12692,6 @@ function loadLoro() {
12625
12692
  const LOCAL_TABLE = "_00_crdt_local";
12626
12693
  const LOG_SUFFIX = "__crdt_log";
12627
12694
  const PRESENCE_SUFFIX = "__crdt_presence";
12628
- /** Longest a burst of typing waits before its edits go out. */
12629
- const MAX_WAIT_MS = 2e3;
12630
12695
  const PERSIST_DEBOUNCE_MS = 400;
12631
12696
  const PRESENCE_THROTTLE_MS = 250;
12632
12697
  const CLAIM_RETRY_MS = 5e3;
@@ -12648,11 +12713,12 @@ var CrdtManager = class {
12648
12713
  claim = null;
12649
12714
  claimRetry = null;
12650
12715
  connectionUnsubscribes = [];
12651
- constructor(schema, local, remote, logger, debounceMs = 300) {
12716
+ constructor(schema, local, remote, logger, debounceMs = 50, sendIntervalMs = 200) {
12652
12717
  this.schema = schema;
12653
12718
  this.local = local;
12654
12719
  this.remote = remote;
12655
12720
  this.debounceMs = debounceMs;
12721
+ this.sendIntervalMs = sendIntervalMs;
12656
12722
  this.logger = logger.child({ service: "CrdtManager" });
12657
12723
  this.connectionUnsubscribes.push(this.remote.subscribeConnection("connected", () => {
12658
12724
  for (const s of this.sessions.values()) {
@@ -12773,9 +12839,11 @@ var CrdtManager = class {
12773
12839
  verifyTimer: null,
12774
12840
  disposers: [],
12775
12841
  flushTimer: null,
12776
- firstDirtyAt: 0,
12842
+ flushDueAt: 0,
12843
+ lastSentAt: 0,
12777
12844
  persistTimer: null,
12778
12845
  flushing: null,
12846
+ flushQueued: null,
12779
12847
  refetching: false,
12780
12848
  presenceTimer: null,
12781
12849
  presencePending: null,
@@ -12788,31 +12856,31 @@ var CrdtManager = class {
12788
12856
  if (local.snapshot || fresh) session.handle.markLoaded();
12789
12857
  this.sessions.set(key, session);
12790
12858
  this.ensureClaimed();
12791
- if (!local.snapshot && !fresh) await this.serverWait(this.refetchCanonical(session));
12859
+ const canonical = !local.snapshot && !fresh ? this.refetchCanonical(session) : null;
12792
12860
  session.disposers.push(doc.subscribeLocalUpdates(() => {
12793
12861
  session.local.peers[this.peer] ??= { enqueued: 0 };
12794
12862
  this.scheduleFlush(session);
12795
12863
  this.schedulePersist(session);
12796
12864
  }));
12797
12865
  const ports = this.ports;
12798
- const log = await ports.watch(`${table}${LOG_SUFFIX}`, {
12866
+ const logWatch = ports.watch(`${table}${LOG_SUFFIX}`, {
12799
12867
  doc: recordId,
12800
12868
  field
12801
12869
  }, (rows) => {
12802
12870
  session.ingesting = session.ingesting.then(() => this.ingest(session, rows));
12803
12871
  });
12872
+ const presenceWatch = cursor ? ports.watch(`${table}${PRESENCE_SUFFIX}`, {
12873
+ doc: recordId,
12874
+ field
12875
+ }, (rows) => this.ingestPresence(session, rows)) : null;
12876
+ const [log, presence] = await Promise.all([logWatch, presenceWatch]);
12804
12877
  session.disposers.push(log.stop);
12878
+ if (presence) session.disposers.push(presence.stop);
12805
12879
  log.settled.then(() => {
12806
12880
  session.logSettled = true;
12807
12881
  session.ingesting.then(() => this.checkLoaded(session));
12808
12882
  });
12809
- if (cursor) {
12810
- const presence = await ports.watch(`${table}${PRESENCE_SUFFIX}`, {
12811
- doc: recordId,
12812
- field
12813
- }, (rows) => this.ingestPresence(session, rows));
12814
- session.disposers.push(presence.stop);
12815
- }
12883
+ if (canonical) await this.serverWait(canonical);
12816
12884
  if (!session.handle.hasContent() && !fresh) await this.serverWait(log.settled);
12817
12885
  await session.ingesting;
12818
12886
  this.scheduleFlush(session, 0);
@@ -12988,18 +13056,36 @@ var CrdtManager = class {
12988
13056
  fork?.free();
12989
13057
  }
12990
13058
  }
12991
- scheduleFlush(s, delay = this.debounceMs) {
13059
+ /**
13060
+ * Arm the send. Edits go out on a steady cadence while they keep coming:
13061
+ * the first one after a quiet spell waits `debounceMs` (one row for the
13062
+ * burst), later ones at least `sendIntervalMs` after the previous send. An
13063
+ * armed timer is never pushed back, so continuous typing cannot hold the
13064
+ * edits; it is only brought forward (`delay`, e.g. 0 on reconnect).
13065
+ */
13066
+ scheduleFlush(s, delay) {
12992
13067
  const now = Date.now();
12993
- if (!s.flushTimer) s.firstDirtyAt = now;
12994
- else clearTimeout(s.flushTimer);
12995
- const wait = Math.max(0, Math.min(delay, s.firstDirtyAt + MAX_WAIT_MS - now));
13068
+ const due = delay !== void 0 ? now + delay : Math.max(now + this.debounceMs, s.lastSentAt + this.sendIntervalMs);
13069
+ if (s.flushTimer) {
13070
+ if (s.flushDueAt <= due) return;
13071
+ clearTimeout(s.flushTimer);
13072
+ }
13073
+ s.flushDueAt = due;
12996
13074
  s.flushTimer = setTimeout(() => {
12997
13075
  s.flushTimer = null;
12998
13076
  this.flush(s);
12999
- }, wait);
13000
- }
13001
- async flush(s) {
13002
- if (s.flushing) return s.flushing;
13077
+ }, due - now);
13078
+ }
13079
+ /** Send what is unsent. A call while a send runs queues one more after it,
13080
+ * so edits made during that send are not left waiting for the next edit. */
13081
+ flush(s) {
13082
+ if (s.flushing) {
13083
+ s.flushQueued ??= s.flushing.then(() => {
13084
+ s.flushQueued = null;
13085
+ return this.flush(s);
13086
+ });
13087
+ return s.flushQueued;
13088
+ }
13003
13089
  s.flushing = this.sendOwnEdits(s).finally(() => {
13004
13090
  s.flushing = null;
13005
13091
  });
@@ -13017,6 +13103,7 @@ var CrdtManager = class {
13017
13103
  from: s.local.peers[peer]?.enqueued ?? 0
13018
13104
  })).filter((p) => p.end > p.from);
13019
13105
  if (due.length === 0 || !this.ports) return;
13106
+ s.lastSentAt = Date.now();
13020
13107
  if (!await this.ensureClaimed()) {
13021
13108
  this.retryLater(s);
13022
13109
  return;
@@ -13051,7 +13138,8 @@ var CrdtManager = class {
13051
13138
  }
13052
13139
  s.local.peers[peer] = { enqueued: end };
13053
13140
  }
13054
- await this.persist(s);
13141
+ if (this.sessions.get(s.key) === s) this.schedulePersist(s);
13142
+ else await this.persist(s);
13055
13143
  }
13056
13144
  retryLater(s) {
13057
13145
  if (this.sessions.get(s.key) === s) this.scheduleFlush(s, CLAIM_RETRY_MS);
@@ -15084,6 +15172,27 @@ function rowOfEdge(value) {
15084
15172
  record: out
15085
15173
  };
15086
15174
  }
15175
+ /**
15176
+ * The membership change a `CREATE` or `UPDATE` notification reports: the
15177
+ * edge's target joined the view (or its version moved). Null for anything a
15178
+ * re-read must settle instead: a `DELETE` (a row leaving, or every edge of a
15179
+ * view being rewritten), a subquery child (`parent` set), or an edge missing
15180
+ * its target or version. With `FETCH out` the target is the joined row, whose
15181
+ * `id` is the record id.
15182
+ */
15183
+ function deltaOfEdge(action, value) {
15184
+ if (action !== "CREATE" && action !== "UPDATE") return null;
15185
+ const hash = hashOfEdge(value);
15186
+ const edge = value;
15187
+ if (!hash || edge.parent != null || typeof edge.version !== "number") return null;
15188
+ const out = edge.out instanceof RecordId$1 ? edge.out : edge.out?.id;
15189
+ if (!(out instanceof RecordId$1)) return null;
15190
+ return {
15191
+ hash,
15192
+ id: encodeRecordId(out),
15193
+ version: edge.version
15194
+ };
15195
+ }
15087
15196
 
15088
15197
  //#endregion
15089
15198
  //#region src/utils/blurhash.ts
@@ -15541,7 +15650,7 @@ function createServices(config) {
15541
15650
  streamProcessor.configureCircuitPersistence(config.persistCircuit ?? config.localEngine === "sqlite", config.circuitCheckpointMs);
15542
15651
  streamProcessor.configureProjection(config.circuitProjection ?? true);
15543
15652
  const migrator = new LocalMigrator(local, logger);
15544
- const crdt = new CrdtManager(config.schema, local, remote, logger, config.crdtDebounceMs ?? 300);
15653
+ const crdt = new CrdtManager(config.schema, local, remote, logger, config.crdtDebounceMs ?? 50, config.crdtSendIntervalMs ?? 200);
15545
15654
  const auth = new AuthService(config.schema, remote, persistence, logger);
15546
15655
  const tabId = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `tab_${Math.random().toString(36).slice(2)}`;
15547
15656
  return {
@@ -15567,7 +15676,7 @@ function createTabsCoordinator(config, s, host) {
15567
15676
  return new TabsCoordinator({
15568
15677
  tabId: s.tabId,
15569
15678
  fingerprint: computeTabsFingerprint({
15570
- coreVersion: "0.0.1-canary.331",
15679
+ coreVersion: "0.0.1-canary.332",
15571
15680
  schemaHash: hash53(config.schemaSurql),
15572
15681
  endpoint: config.database.endpoint ?? "",
15573
15682
  namespace: config.database.namespace,
@@ -15787,7 +15896,8 @@ function createAdapters(config, s, host, lateModules) {
15787
15896
  const hash = hashOfEdge(message.value);
15788
15897
  if (!hash) return;
15789
15898
  const row = message.action === "DELETE" ? null : rowOfEdge(message.value);
15790
- onChange([hash], row ? [row] : void 0);
15899
+ const delta = deltaOfEdge(message.action, message.value);
15900
+ onChange([hash], row ? [row] : void 0, delta ? [delta] : void 0);
15791
15901
  });
15792
15902
  return String(uuid);
15793
15903
  },
@@ -1,4 +1,4 @@
1
- import { p as PinoTransmit } from "../types.js";
1
+ import { m as PinoTransmit } from "../types.js";
2
2
  import { Level } from "pino";
3
3
 
4
4
  //#region src/otel/index.d.ts
package/dist/pure.d.ts CHANGED
@@ -139,6 +139,13 @@ interface InlineRow {
139
139
  version: number;
140
140
  record: Record<string, unknown>;
141
141
  }
142
+ /**
143
+ * A row that joined a query's view, or moved to a newer version, as one LIVE
144
+ * notification on `_00_list_ref` reports it: the edge's `out` and `version`.
145
+ * The client commits it to the query's membership straight away instead of
146
+ * re-reading the edges (see `sync/live.saga.ts`).
147
+ */
148
+
142
149
  type RecordVersionArray = Array<[string, number]>;
143
150
  /**
144
151
  * Represents the difference between two record version sets.
@@ -329,6 +336,15 @@ declare function hashOfEdge(value: unknown): string | null;
329
336
  * `out: null`) and for an edge whose target has been deleted.
330
337
  */
331
338
  declare function rowOfEdge(value: unknown): InlineRow | null;
339
+ /**
340
+ * The membership change a `CREATE` or `UPDATE` notification reports: the
341
+ * edge's target joined the view (or its version moved). Null for anything a
342
+ * re-read must settle instead: a `DELETE` (a row leaving, or every edge of a
343
+ * view being rewritten), a subquery child (`parent` set), or an edge missing
344
+ * its target or version. With `FETCH out` the target is the joined row, whose
345
+ * `id` is the record id.
346
+ */
347
+
332
348
  //#endregion
333
349
  //#region src/utils/parser.d.ts
334
350
  /**
package/dist/types.d.ts CHANGED
@@ -1594,14 +1594,20 @@ interface Sp00kyConfig<S extends SchemaStructure> {
1594
1594
  */
1595
1595
  streamDebounceTime?: number;
1596
1596
  /**
1597
- * Debounce time in milliseconds for syncing collaborative (CRDT) field
1598
- * edits to the server. Local state is kept on every change (so reload and
1599
- * offline work), but a field's own new ops go out as one edit-log row per
1600
- * pause of this length (at most 2 s apart while typing continues).
1601
- * Lower = snappier propagation and more rows; higher = fewer rows and more
1602
- * lag for other collaborators. Defaults to 300ms.
1597
+ * How long in milliseconds the first edit to a collaborative (CRDT) field
1598
+ * after a quiet spell waits before it goes out, so a burst of keystrokes
1599
+ * travels as one edit-log row. Local state is kept on every change (so
1600
+ * reload and offline work). Defaults to 50ms.
1603
1601
  */
1604
1602
  crdtDebounceMs?: number;
1603
+ /**
1604
+ * The shortest gap in milliseconds between two edit-log rows of one CRDT
1605
+ * field: while someone keeps typing, their edits go out on this cadence
1606
+ * instead of waiting for a pause. Lower = other collaborators see the text
1607
+ * sooner and more rows are written; higher = fewer rows and more lag.
1608
+ * Defaults to 200ms.
1609
+ */
1610
+ crdtSendIntervalMs?: number;
1605
1611
  /**
1606
1612
  * Enable collaborative CRDT fields. When `true`, the `loro-crdt` engine is
1607
1613
  * preloaded at client startup (fetched as a separate chunk on page load) so
@@ -1635,15 +1641,16 @@ interface Sp00kyConfig<S extends SchemaStructure> {
1635
1641
  * back for it.
1636
1642
  *
1637
1643
  * The client holds exactly one live subscription, on its `_00_list_ref`
1638
- * table, and today it is a doorbell: the notification is reduced to the
1639
- * query hash and the row is then pulled with `SELECT * FROM $ids`. The edge
1644
+ * table. Without this, the notification names the row and the client then
1645
+ * pulls it with `SELECT * FROM $ids`. The edge
1640
1646
  * is a real graph edge, so `FETCH out` makes SurrealDB resolve the row into
1641
1647
  * the notification and the body fetch disappears.
1642
1648
  *
1643
1649
  * The push stays advisory: the body is landed exactly the way a fetched body
1644
1650
  * is (store write, circuit ingest, version recorded), so a notification the
1645
- * server drops costs nothing but the old round trip. Membership is still
1646
- * read back; only the body fetch is saved.
1651
+ * server drops costs nothing but the old round trip. A row joining a live
1652
+ * view is committed to membership from the notification either way (no
1653
+ * re-read); with this on, the body fetch is saved as well.
1647
1654
  *
1648
1655
  * Default `false`. The cost is wire bytes: one edit bumps the edge in every
1649
1656
  * view holding the row, and under `FETCH` each of those notifications
@@ -1873,6 +1880,17 @@ interface InlineRow {
1873
1880
  version: number;
1874
1881
  record: Record<string, unknown>;
1875
1882
  }
1883
+ /**
1884
+ * A row that joined a query's view, or moved to a newer version, as one LIVE
1885
+ * notification on `_00_list_ref` reports it: the edge's `out` and `version`.
1886
+ * The client commits it to the query's membership straight away instead of
1887
+ * re-reading the edges (see `sync/live.saga.ts`).
1888
+ */
1889
+ interface EdgeDelta {
1890
+ hash: QueryHash;
1891
+ id: string;
1892
+ version: number;
1893
+ }
1876
1894
  type RecordVersionArray = Array<[string, number]>;
1877
1895
  /**
1878
1896
  * Represents the difference between two record version sets.
@@ -2175,4 +2193,4 @@ interface DebounceOptions {
2175
2193
  flushOnHide?: boolean;
2176
2194
  }
2177
2195
  //#endregion
2178
- export { WebPushSubscribeOptions as $, RunOptions as A, Logger$1 as At, SyncHealthStatus as B, SESSION_POLL_MS as Bt, QueryTimeToLive as C, SignOutHook as Ct, RecordVersionArray as D, createAuthEventSystem as Dt, ReconnectConfig as E, AuthEventTypes as Et, StorageHealth as F, IMPERSONATION_ACCESS as Ft, WebPushConfig as G, sameImpersonation as Gt, UpdateOptions as H, decodeTokenClaims as Ht, StorageHealthStatus as I, ImpersonationInfo as It, WebPushErrorCode as J, WebPushDevice as K, StoreType as L, ImpersonationTimerHost as Lt, Sp00kyConfig as M, SealedQuery as Mt, Sp00kyQueryResult as N, EventDefinition as Nt, RecordVersionDiff as O, RemoteDatabaseService as Ot, Sp00kyQueryResultPromise as P, EventSystem as Pt, WebPushModule as Q, SyncHealth as R, ImpersonationTimers as Rt, QueryStatusCallback as S, SignOutContext as St, QueryUpdateCallback as T, AuthEventTypeMap as Tt, PUSH_ENDPOINT_KEY as U, impersonationFromToken as Ut, TimingPhase as V, TokenClaims as Vt, PushDeviceKind as W, renewDelayMs as Wt, WebPushMessage as X, WebPushInfo as Y, WebPushMessageInput as Z, QueryConfig as _, isPushPayload as _t, Level$1 as a, WebPushUpdateOptions as at, QueryState as b, ImpersonationCandidate as bt, MutationCallback as c, BridgeTokenMessage as ct, PersistenceClient as d, PushNotificationAction as dt, WebPushSupport as et, PhaseStat as f, PushOp as ft, QueryAuthorityCallback as g, WebPushClientMessage as gt, PreloadRefresh as h, PushUrgency as ht, InlineRow as i, WebPushUnsupportedReason as it, ServerViewMeta as j, LocalStore as jt, RegistrationTimings as k, LogTap as kt, MutationEvent as l, PUSH_PAYLOAD_VERSION as lt, PreloadOptions as m, PushPayloadKind as mt, DebounceOptions as n, WebPushSyncStatus as nt, MATERIALIZATION_SAMPLE_WINDOW as o, detectWebPushSupport as ot, PinoTransmit as p, PushPayload as pt, WebPushError as q, EventSubscriptionOptions as r, WebPushUnsubscribeOptions as rt, MembershipOutcome as s, BridgeSignOutMessage as st, ConnectionState as t, WebPushSyncOptions as tt, MutationEventType as u, PushNotification as ut, QueryConfigRecord as v, ActiveImpersonation as vt, QueryTimings as w, AuthEventSystem as wt, QueryStatus as x, SIGN_OUT_HOOK_TIMEOUT_MS as xt, QueryHash as y, AuthService as yt, SyncHealthConfig as z, ORIGIN_TOKEN_KEY as zt };
2196
+ export { WebPushModule as $, RegistrationTimings as A, LogTap as At, SyncHealthConfig as B, ORIGIN_TOKEN_KEY as Bt, QueryStatusCallback as C, SignOutContext as Ct, ReconnectConfig as D, AuthEventTypes as Dt, QueryUpdateCallback as E, AuthEventTypeMap as Et, Sp00kyQueryResultPromise as F, EventSystem as Ft, PushDeviceKind as G, renewDelayMs as Gt, TimingPhase as H, TokenClaims as Ht, StorageHealth as I, IMPERSONATION_ACCESS as It, WebPushError as J, WebPushConfig as K, sameImpersonation as Kt, StorageHealthStatus as L, ImpersonationInfo as Lt, ServerViewMeta as M, LocalStore as Mt, Sp00kyConfig as N, SealedQuery as Nt, RecordVersionArray as O, createAuthEventSystem as Ot, Sp00kyQueryResult as P, EventDefinition as Pt, WebPushMessageInput as Q, StoreType as R, ImpersonationTimerHost as Rt, QueryStatus as S, SIGN_OUT_HOOK_TIMEOUT_MS as St, QueryTimings as T, AuthEventSystem as Tt, UpdateOptions as U, decodeTokenClaims as Ut, SyncHealthStatus as V, SESSION_POLL_MS as Vt, PUSH_ENDPOINT_KEY as W, impersonationFromToken as Wt, WebPushInfo as X, WebPushErrorCode as Y, WebPushMessage as Z, QueryAuthorityCallback as _, WebPushClientMessage as _t, InlineRow as a, WebPushUnsupportedReason as at, QueryHash as b, AuthService as bt, MembershipOutcome as c, BridgeSignOutMessage as ct, MutationEventType as d, PushNotification as dt, WebPushSubscribeOptions as et, PersistenceClient as f, PushNotificationAction as ft, PreloadRefresh as g, PushUrgency as gt, PreloadOptions as h, PushPayloadKind as ht, EventSubscriptionOptions as i, WebPushUnsubscribeOptions as it, RunOptions as j, Logger$1 as jt, RecordVersionDiff as k, RemoteDatabaseService as kt, MutationCallback as l, BridgeTokenMessage as lt, PinoTransmit as m, PushPayload as mt, DebounceOptions as n, WebPushSyncOptions as nt, Level$1 as o, WebPushUpdateOptions as ot, PhaseStat as p, PushOp as pt, WebPushDevice as q, EdgeDelta as r, WebPushSyncStatus as rt, MATERIALIZATION_SAMPLE_WINDOW as s, detectWebPushSupport as st, ConnectionState as t, WebPushSupport as tt, MutationEvent as u, PUSH_PAYLOAD_VERSION as ut, QueryConfig as v, isPushPayload as vt, QueryTimeToLive as w, SignOutHook as wt, QueryState as x, ImpersonationCandidate as xt, QueryConfigRecord as y, ActiveImpersonation as yt, SyncHealth as z, ImpersonationTimers as zt };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spooky-sync/core",
3
- "version": "0.0.1-canary.331",
3
+ "version": "0.0.1-canary.332",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -75,8 +75,8 @@
75
75
  }
76
76
  },
77
77
  "dependencies": {
78
- "@spooky-sync/query-builder": "0.0.1-canary.331",
79
- "@spooky-sync/ssp-wasm": "0.0.1-canary.331",
78
+ "@spooky-sync/query-builder": "0.0.1-canary.332",
79
+ "@spooky-sync/ssp-wasm": "0.0.1-canary.332",
80
80
  "@sqlite.org/sqlite-wasm": "3.53.0-build1",
81
81
  "@surrealdb/wasm": "^3.1.0",
82
82
  "@surrealdb/wasm-native": "^3.3.4",
@@ -77,7 +77,7 @@ export function route(env: SagaEnv, event: RuntimeEvent): RouteTarget {
77
77
  case 'LiveStart':
78
78
  return { saga: liveStart(env), lane: serial('live') };
79
79
  case 'LiveChange':
80
- return { saga: liveChange(env, event.hashes, event.rows), lane: serial('live-change') };
80
+ return { saga: liveChange(env, event.hashes, event.rows, event.deltas), lane: serial('live-change') };
81
81
  case 'TabRole':
82
82
  return { saga: setRole(env, event.role), lane: serial('tabs') };
83
83
  case 'TabMessage':
@@ -30,7 +30,7 @@ import type { ServiceCalls } from '../kernel/effects';
30
30
  import { encodeRecordId, mintOwnerSecret } from '../utils/index';
31
31
  import { sha256Hex } from '../utils/sha256';
32
32
  import { leaveViewsBeacon, listRefLive } from '../query/sql';
33
- import { hashOfEdge, rowOfEdge } from '../utils/edges';
33
+ import { deltaOfEdge, hashOfEdge, rowOfEdge } from '../utils/edges';
34
34
  import { mintMutationId } from '../mutation/mutation-id';
35
35
  import { BucketHandle, bucketContentToBlob, bucketSettings } from './bucket-handle';
36
36
 
@@ -176,7 +176,7 @@ export function createServices<S extends SchemaStructure>(config: Sp00kyConfig<S
176
176
  streamProcessor.configureCircuitPersistence(config.persistCircuit ?? config.localEngine === 'sqlite', config.circuitCheckpointMs);
177
177
  streamProcessor.configureProjection(config.circuitProjection ?? true);
178
178
  const migrator = new LocalMigrator(local, logger);
179
- const crdt = new CrdtManager(config.schema, local, remote, logger, config.crdtDebounceMs ?? 300);
179
+ const crdt = new CrdtManager(config.schema, local, remote, logger, config.crdtDebounceMs ?? 50, config.crdtSendIntervalMs ?? 200);
180
180
  const auth = new AuthService(config.schema, remote, persistence, logger);
181
181
  const tabId = typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : `tab_${Math.random().toString(36).slice(2)}`;
182
182
  return {
@@ -395,7 +395,8 @@ export function createAdapters<S extends SchemaStructure>(config: Sp00kyConfig<S
395
395
  // that was actually deleted the join yields nothing anyway. Landing
396
396
  // a body there would only ever write back what is going away.
397
397
  const row = message.action === 'DELETE' ? null : rowOfEdge(message.value);
398
- onChange([hash], row ? [row] : undefined);
398
+ const delta = deltaOfEdge(message.action, message.value);
399
+ onChange([hash], row ? [row] : undefined, delta ? [delta] : undefined);
399
400
  });
400
401
  return String(uuid);
401
402
  },