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

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 RemoteDatabaseService, B as SyncHealthConfig, Bt as ImpersonationTimers, C as QueryStatusCallback, Ct as SIGN_OUT_HOOK_TIMEOUT_MS, D as ReconnectConfig, Dt as AuthEventTypeMap, E as QueryUpdateCallback, Et as AuthEventSystem, F as Sp00kyQueryResultPromise, Ft as EventDefinition, G as PushDeviceKind, Gt as impersonationFromToken, H as TimingPhase, Ht as SESSION_POLL_MS, I as StorageHealth, It as EventSystem, J as WebPushError, K as WebPushConfig, Kt as renewDelayMs, L as StorageHealthStatus, Lt as IMPERSONATION_ACCESS, M as ServerViewMeta, Mt as Logger$1, N as Sp00kyConfig, Nt as LocalStore, O as RecordVersionArray, Ot as AuthEventTypes, P as Sp00kyQueryResult, Pt as SealedQuery, Q as WebPushMessageInput, R as StoreType, Rt as ImpersonationInfo, S as QueryStatus, St as REFUSAL_RECHECK_MIN_MS, T as QueryTimings, Tt as SignOutHook, U as UpdateOptions, Ut as TokenClaims, V as SyncHealthStatus, Vt as ORIGIN_TOKEN_KEY, W as PUSH_ENDPOINT_KEY, Wt as decodeTokenClaims, 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 LogTap, k as RecordVersionDiff, kt as createAuthEventSystem, 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, qt as sameImpersonation, 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 SignOutContext, x as QueryState, xt as ImpersonationCandidate, y as QueryConfigRecord, yt as ActiveImpersonation, z as SyncHealth, zt as ImpersonationTimerHost } 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, REFUSAL_RECHECK_MIN_MS, 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
@@ -551,6 +551,16 @@ function classifySyncError(error) {
551
551
  for (const pattern of NETWORK_ERROR_PATTERNS) if (message.includes(pattern)) return "network";
552
552
  return "application";
553
553
  }
554
+ /**
555
+ * Whether the server refused the SESSION rather than the statement: its own
556
+ * wording for a token it will not accept ("There was a problem with
557
+ * authentication"), which is also what an app's AUTHENTICATE clause and
558
+ * sp00ky's session guard throw, and a session past its `DURATION FOR SESSION`.
559
+ */
560
+ function isRefusedSessionError(error) {
561
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
562
+ return message.includes("problem with authentication") || message.includes("session has expired");
563
+ }
554
564
 
555
565
  //#endregion
556
566
  //#region src/utils/index.ts
@@ -2188,8 +2198,8 @@ function trimHistory(history, cap) {
2188
2198
 
2189
2199
  //#endregion
2190
2200
  //#region src/modules/devtools/index.ts
2191
- const CORE_VERSION = "0.0.1-canary.331";
2192
- const WASM_VERSION = "0.0.1-canary.331";
2201
+ const CORE_VERSION = "0.0.1-canary.333";
2202
+ const WASM_VERSION = "0.0.1-canary.333";
2193
2203
  const SURREAL_VERSION = "3.1.0";
2194
2204
  var DevToolsService = class DevToolsService {
2195
2205
  version = CORE_VERSION;
@@ -4168,6 +4178,35 @@ function decideMembershipOutcome(input) {
4168
4178
  return meta.rowCount === 0 && meta.state !== "materializing" ? "applied" : "ignored";
4169
4179
  }
4170
4180
  /**
4181
+ * Commit LIVE edge deltas onto a held id-set: each names a row that joined
4182
+ * (or moved to a newer version). Returns `held` itself when nothing moves.
4183
+ */
4184
+ function withDeltas(held, deltas) {
4185
+ const next = new Map(held);
4186
+ let moved = false;
4187
+ for (const { id, version } of deltas) {
4188
+ if ((next.get(id) ?? -1) >= version) continue;
4189
+ next.set(id, version);
4190
+ moved = true;
4191
+ }
4192
+ return moved ? Array.from(next.entries()) : held;
4193
+ }
4194
+ /**
4195
+ * A membership read answers for the moment the server ran it, which may be
4196
+ * before a LIVE delta that was committed while the read was in flight. Carry
4197
+ * what changed in state since the read was sent (`before` -> `now`) over onto
4198
+ * the read, so a late answer never takes back a row that just joined.
4199
+ */
4200
+ function rebaseRead(read, before, now) {
4201
+ if (before === now) return read;
4202
+ const was = new Map(before);
4203
+ const is = new Map(now);
4204
+ const out = new Map(read);
4205
+ for (const [id] of before) if (!is.has(id)) out.delete(id);
4206
+ for (const [id, version] of now) if (version > (was.get(id) ?? -1) && version > (out.get(id) ?? -1)) out.set(id, version);
4207
+ return Array.from(out.entries());
4208
+ }
4209
+ /**
4171
4210
  * A statement that did not answer (an ERR, or no result at all). Kept apart
4172
4211
  * from a `null` result, which is the server saying "no such row": reading a
4173
4212
  * failed statement as a missing row turned every timeout under load into a
@@ -4854,9 +4893,38 @@ const stmt$1 = (results, i) => {
4854
4893
  return r && r.status === "OK" ? r.result : FAILED;
4855
4894
  };
4856
4895
  /**
4896
+ * Commit LIVE edge deltas (rows joining a view, or moving to a newer
4897
+ * version) straight to membership, without re-reading the edges: the
4898
+ * notification already says what the read would. Returns the hashes it
4899
+ * settled; any other change still goes through {@link readMembership}.
4900
+ *
4901
+ * Only for a query whose membership is known and current (`live`, not
4902
+ * materializing, no view-lost recovery pending): before that the read decides
4903
+ * what the set is. Removals are never applied here, so a view whose edges
4904
+ * are being rewritten never loses (and garbage-collects) bodies mid-rewrite.
4905
+ * Nor for a query with a `LIMIT`: a row joining it pushes another out, and
4906
+ * rendering the joiner before the read removes the leaver would show one row
4907
+ * too many (the id-set render drops the `LIMIT`).
4908
+ */
4909
+ function* applyEdgeDeltas(deltas) {
4910
+ const settled = /* @__PURE__ */ new Set();
4911
+ const byHash = /* @__PURE__ */ new Map();
4912
+ for (const d of deltas) byHash.set(d.hash, [...byHash.get(d.hash) ?? [], d]);
4913
+ for (const [hash, list] of byHash) {
4914
+ const entry = yield fx.state.read((s) => s.queries.get(hash));
4915
+ if (!entry || entry.lifecycle.phase !== "live" || entry.serverState === "materializing" || entry.viewLostRetryAt !== null) continue;
4916
+ if (entry.def.plan ? entry.def.plan.limit != null : /\bLIMIT\b/i.test(entry.def.surql)) continue;
4917
+ const next = withDeltas(entry.remoteArray, list);
4918
+ if (next !== entry.remoteArray) yield* applyMembership(hash, next);
4919
+ settled.add(hash);
4920
+ }
4921
+ return settled;
4922
+ }
4923
+ /**
4857
4924
  * 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
4925
+ * row budget allows, then apply each changed set. Every server id-set enters
4926
+ * state here, except the rows LIVE reports joining (see
4927
+ * {@link applyEdgeDeltas}): registration, the poll, LIVE dirt and view-lost
4860
4928
  * recovery all come through here.
4861
4929
  */
4862
4930
  function* readMembership(env, hashes, opts = {}) {
@@ -4919,9 +4987,10 @@ function* readMembership(env, hashes, opts = {}) {
4919
4987
  for (const [hash, snap] of snapshots) {
4920
4988
  const current = yield fx.state.read((s) => s.queries.get(hash));
4921
4989
  if (!current) continue;
4990
+ const primary = rebaseRead(snap.primary, byHash.get(hash).remoteArray, current.remoteArray);
4922
4991
  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);
4992
+ if (!recordVersionArraysEqual(primary, current.remoteArray) || metaChanged || current.lifecycle.phase === "cold" || current.lifecycle.phase === "cached") {
4993
+ const outcome = yield* applyMembership(hash, primary, snap.meta);
4925
4994
  if (outcome === "applied") changed = true;
4926
4995
  if (outcome === "ignored" && snap.meta.present && snap.meta.state === "materializing") stillMaterializing.push(hash);
4927
4996
  }
@@ -6158,10 +6227,11 @@ function createInterpreter(adapters, host) {
6158
6227
  const pending = adapters.remote.queryResponses(effect.sql, effect.vars);
6159
6228
  return effect.timeoutMs ? withTimeout(pending, effect.timeoutMs, () => /* @__PURE__ */ new Error(`Remote request timed out after ${effect.timeoutMs}ms`)) : pending;
6160
6229
  }
6161
- case "remote.live": return adapters.remote.live(effect.table, (hashes, rows) => host.dispatch({
6230
+ case "remote.live": return adapters.remote.live(effect.table, (hashes, rows, deltas) => host.dispatch({
6162
6231
  type: "LiveChange",
6163
6232
  hashes,
6164
- rows
6233
+ rows,
6234
+ deltas
6165
6235
  }));
6166
6236
  case "remote.kill": return adapters.remote.kill(effect.uuid);
6167
6237
  case "ssp.register": {
@@ -6711,9 +6781,10 @@ function* pollTick(env) {
6711
6781
  //#endregion
6712
6782
  //#region src/sync/live.saga.ts
6713
6783
  /**
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.
6784
+ * LIVE on the session's `_00_list_ref` table. A row joining a live view is
6785
+ * committed straight from its edge; any other event marks its query's
6786
+ * membership dirty and the batched re-read decides. Followers get the dirt
6787
+ * relayed by the leader.
6717
6788
  *
6718
6789
  * When the subscription was opened with the body joined on
6719
6790
  * (`liveInlineBodies`) the event ALSO carries the changed row. That is an
@@ -6765,8 +6836,12 @@ function* liveInvalidate() {
6765
6836
  liveTable: null
6766
6837
  }));
6767
6838
  }
6768
- /** One or more edges of these queries changed on the server. */
6769
- function* liveChange(env, hashes, rows) {
6839
+ /**
6840
+ * One or more edges of these queries changed on the server. A row joining a
6841
+ * live view (`deltas`) is committed at once; anything else marks the query
6842
+ * dirty for the batched re-read.
6843
+ */
6844
+ function* liveChange(env, hashes, rows, deltas) {
6770
6845
  const [known, role, streak] = yield fx.state.read((s) => [
6771
6846
  hashes.filter((h) => s.queries.has(h)),
6772
6847
  s.tabRole,
@@ -6778,7 +6853,9 @@ function* liveChange(env, hashes, rows) {
6778
6853
  yield fx.timer.set("poll", env.pollBaseMs, { type: "PollTick" });
6779
6854
  }
6780
6855
  if (rows && rows.length > 0) yield* landInlineRows(env, rows);
6781
- yield* markMembershipDirty(known);
6856
+ const settled = deltas && deltas.length > 0 ? yield* applyEdgeDeltas(deltas.filter((d) => known.includes(d.hash))) : /* @__PURE__ */ new Set();
6857
+ const dirty = known.filter((h) => !settled.has(h));
6858
+ if (dirty.length > 0) yield* markMembershipDirty(dirty);
6782
6859
  if (role === "leader") yield fx.emit({
6783
6860
  type: "tabs:broadcast",
6784
6861
  message: {
@@ -7326,7 +7403,7 @@ function route(env, event) {
7326
7403
  lane: serial("live")
7327
7404
  };
7328
7405
  case "LiveChange": return {
7329
- saga: liveChange(env, event.hashes, event.rows),
7406
+ saga: liveChange(env, event.hashes, event.rows, event.deltas),
7330
7407
  lane: serial("live-change")
7331
7408
  };
7332
7409
  case "TabRole": return {
@@ -11834,6 +11911,12 @@ var ImpersonationTimers = class {
11834
11911
  //#region src/modules/auth/index.ts
11835
11912
  /** Upper bound on the before-sign-out hooks, so a dead socket never holds a sign-out. */
11836
11913
  const SIGN_OUT_HOOK_TIMEOUT_MS = 2e3;
11914
+ /**
11915
+ * Floor between two session re-checks started by refused statements. A
11916
+ * refused session fails every statement at once, and a check that keeps the
11917
+ * session (the server was unreachable) must not be re-run by each of them.
11918
+ */
11919
+ const REFUSAL_RECHECK_MIN_MS = 3e4;
11837
11920
  function decodeAccessFromToken(token) {
11838
11921
  return decodeTokenClaims(token).access;
11839
11922
  }
@@ -11863,6 +11946,11 @@ var AuthService = class {
11863
11946
  stopping = null;
11864
11947
  /** A token renewal in flight, which a stop waits out (see `renewImpersonation`). */
11865
11948
  renewing = null;
11949
+ /** The session re-check in flight (see `watchSession`), shared by every trigger. */
11950
+ reverifying = null;
11951
+ /** When a refused statement last started a re-check. */
11952
+ lastRefusalCheckAt = Number.NEGATIVE_INFINITY;
11953
+ watchingSession = false;
11866
11954
  impersonationTimers = new ImpersonationTimers({
11867
11955
  current: () => this.impersonation,
11868
11956
  renew: () => this.renewImpersonation(),
@@ -11890,6 +11978,54 @@ var AuthService = class {
11890
11978
  }
11891
11979
  async init() {
11892
11980
  await this.check();
11981
+ this.watchSession();
11982
+ }
11983
+ /**
11984
+ * Check the session again whenever the socket comes back, and whenever the
11985
+ * server refuses a statement because of the session.
11986
+ *
11987
+ * The server judges a token only when a socket authenticates: the SDK
11988
+ * replays `authenticate` on its own reconnects, `RemoteDatabaseService` on a
11989
+ * socket it rebuilds. When the replay is refused (the app ended the session,
11990
+ * say with a password reset) the SDK drops to an anonymous session, and the
11991
+ * supervisor swallows the rebuild's error, so the socket reports `connected`
11992
+ * while this service still announces the user. Every view registered from
11993
+ * then on carried no identity: the page looked signed in and showed nothing
11994
+ * of the user's. `check()` applies the token again and signs out when the
11995
+ * server refuses it.
11996
+ *
11997
+ * A socket that never reconnects is ended by the app's session guard
11998
+ * (`fn::sp00ky::session_live`) instead: its heartbeats stop finding rows,
11999
+ * and the re-registration that follows is refused with "There was a problem
12000
+ * with authentication", which starts a check here too.
12001
+ */
12002
+ watchSession() {
12003
+ if (this.watchingSession) return;
12004
+ this.watchingSession = true;
12005
+ this.remote.subscribeConnection("connected", () => this.reverify());
12006
+ this.remote.getEvents().subscribe(DatabaseEventTypes.RemoteQuery, (event) => {
12007
+ const { success, error } = event.payload;
12008
+ if (success || !isRefusedSessionError(error ?? "")) return;
12009
+ const now = Date.now();
12010
+ if (now - this.lastRefusalCheckAt < REFUSAL_RECHECK_MIN_MS) return;
12011
+ this.lastRefusalCheckAt = now;
12012
+ this.reverify();
12013
+ });
12014
+ }
12015
+ /** One `check()` for every trigger that lands while it runs. */
12016
+ reverify() {
12017
+ if (!this.isAuthenticated || this.reverifying) return;
12018
+ this.reverifying = (async () => {
12019
+ await this.remote.whenConnected();
12020
+ if (this.isAuthenticated) await this.check();
12021
+ })().catch((error) => {
12022
+ this.logger.warn({
12023
+ error,
12024
+ Category: "sp00ky-client::AuthService::reverify"
12025
+ }, "Session re-check failed");
12026
+ }).finally(() => {
12027
+ this.reverifying = null;
12028
+ });
11893
12029
  }
11894
12030
  getAccessDefinition(name) {
11895
12031
  return this.schema.access?.[name];
@@ -12625,8 +12761,6 @@ function loadLoro() {
12625
12761
  const LOCAL_TABLE = "_00_crdt_local";
12626
12762
  const LOG_SUFFIX = "__crdt_log";
12627
12763
  const PRESENCE_SUFFIX = "__crdt_presence";
12628
- /** Longest a burst of typing waits before its edits go out. */
12629
- const MAX_WAIT_MS = 2e3;
12630
12764
  const PERSIST_DEBOUNCE_MS = 400;
12631
12765
  const PRESENCE_THROTTLE_MS = 250;
12632
12766
  const CLAIM_RETRY_MS = 5e3;
@@ -12648,11 +12782,12 @@ var CrdtManager = class {
12648
12782
  claim = null;
12649
12783
  claimRetry = null;
12650
12784
  connectionUnsubscribes = [];
12651
- constructor(schema, local, remote, logger, debounceMs = 300) {
12785
+ constructor(schema, local, remote, logger, debounceMs = 50, sendIntervalMs = 200) {
12652
12786
  this.schema = schema;
12653
12787
  this.local = local;
12654
12788
  this.remote = remote;
12655
12789
  this.debounceMs = debounceMs;
12790
+ this.sendIntervalMs = sendIntervalMs;
12656
12791
  this.logger = logger.child({ service: "CrdtManager" });
12657
12792
  this.connectionUnsubscribes.push(this.remote.subscribeConnection("connected", () => {
12658
12793
  for (const s of this.sessions.values()) {
@@ -12773,9 +12908,11 @@ var CrdtManager = class {
12773
12908
  verifyTimer: null,
12774
12909
  disposers: [],
12775
12910
  flushTimer: null,
12776
- firstDirtyAt: 0,
12911
+ flushDueAt: 0,
12912
+ lastSentAt: 0,
12777
12913
  persistTimer: null,
12778
12914
  flushing: null,
12915
+ flushQueued: null,
12779
12916
  refetching: false,
12780
12917
  presenceTimer: null,
12781
12918
  presencePending: null,
@@ -12788,31 +12925,31 @@ var CrdtManager = class {
12788
12925
  if (local.snapshot || fresh) session.handle.markLoaded();
12789
12926
  this.sessions.set(key, session);
12790
12927
  this.ensureClaimed();
12791
- if (!local.snapshot && !fresh) await this.serverWait(this.refetchCanonical(session));
12928
+ const canonical = !local.snapshot && !fresh ? this.refetchCanonical(session) : null;
12792
12929
  session.disposers.push(doc.subscribeLocalUpdates(() => {
12793
12930
  session.local.peers[this.peer] ??= { enqueued: 0 };
12794
12931
  this.scheduleFlush(session);
12795
12932
  this.schedulePersist(session);
12796
12933
  }));
12797
12934
  const ports = this.ports;
12798
- const log = await ports.watch(`${table}${LOG_SUFFIX}`, {
12935
+ const logWatch = ports.watch(`${table}${LOG_SUFFIX}`, {
12799
12936
  doc: recordId,
12800
12937
  field
12801
12938
  }, (rows) => {
12802
12939
  session.ingesting = session.ingesting.then(() => this.ingest(session, rows));
12803
12940
  });
12941
+ const presenceWatch = cursor ? ports.watch(`${table}${PRESENCE_SUFFIX}`, {
12942
+ doc: recordId,
12943
+ field
12944
+ }, (rows) => this.ingestPresence(session, rows)) : null;
12945
+ const [log, presence] = await Promise.all([logWatch, presenceWatch]);
12804
12946
  session.disposers.push(log.stop);
12947
+ if (presence) session.disposers.push(presence.stop);
12805
12948
  log.settled.then(() => {
12806
12949
  session.logSettled = true;
12807
12950
  session.ingesting.then(() => this.checkLoaded(session));
12808
12951
  });
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
- }
12952
+ if (canonical) await this.serverWait(canonical);
12816
12953
  if (!session.handle.hasContent() && !fresh) await this.serverWait(log.settled);
12817
12954
  await session.ingesting;
12818
12955
  this.scheduleFlush(session, 0);
@@ -12988,18 +13125,36 @@ var CrdtManager = class {
12988
13125
  fork?.free();
12989
13126
  }
12990
13127
  }
12991
- scheduleFlush(s, delay = this.debounceMs) {
13128
+ /**
13129
+ * Arm the send. Edits go out on a steady cadence while they keep coming:
13130
+ * the first one after a quiet spell waits `debounceMs` (one row for the
13131
+ * burst), later ones at least `sendIntervalMs` after the previous send. An
13132
+ * armed timer is never pushed back, so continuous typing cannot hold the
13133
+ * edits; it is only brought forward (`delay`, e.g. 0 on reconnect).
13134
+ */
13135
+ scheduleFlush(s, delay) {
12992
13136
  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));
13137
+ const due = delay !== void 0 ? now + delay : Math.max(now + this.debounceMs, s.lastSentAt + this.sendIntervalMs);
13138
+ if (s.flushTimer) {
13139
+ if (s.flushDueAt <= due) return;
13140
+ clearTimeout(s.flushTimer);
13141
+ }
13142
+ s.flushDueAt = due;
12996
13143
  s.flushTimer = setTimeout(() => {
12997
13144
  s.flushTimer = null;
12998
13145
  this.flush(s);
12999
- }, wait);
13000
- }
13001
- async flush(s) {
13002
- if (s.flushing) return s.flushing;
13146
+ }, due - now);
13147
+ }
13148
+ /** Send what is unsent. A call while a send runs queues one more after it,
13149
+ * so edits made during that send are not left waiting for the next edit. */
13150
+ flush(s) {
13151
+ if (s.flushing) {
13152
+ s.flushQueued ??= s.flushing.then(() => {
13153
+ s.flushQueued = null;
13154
+ return this.flush(s);
13155
+ });
13156
+ return s.flushQueued;
13157
+ }
13003
13158
  s.flushing = this.sendOwnEdits(s).finally(() => {
13004
13159
  s.flushing = null;
13005
13160
  });
@@ -13017,6 +13172,7 @@ var CrdtManager = class {
13017
13172
  from: s.local.peers[peer]?.enqueued ?? 0
13018
13173
  })).filter((p) => p.end > p.from);
13019
13174
  if (due.length === 0 || !this.ports) return;
13175
+ s.lastSentAt = Date.now();
13020
13176
  if (!await this.ensureClaimed()) {
13021
13177
  this.retryLater(s);
13022
13178
  return;
@@ -13051,7 +13207,8 @@ var CrdtManager = class {
13051
13207
  }
13052
13208
  s.local.peers[peer] = { enqueued: end };
13053
13209
  }
13054
- await this.persist(s);
13210
+ if (this.sessions.get(s.key) === s) this.schedulePersist(s);
13211
+ else await this.persist(s);
13055
13212
  }
13056
13213
  retryLater(s) {
13057
13214
  if (this.sessions.get(s.key) === s) this.scheduleFlush(s, CLAIM_RETRY_MS);
@@ -15084,6 +15241,27 @@ function rowOfEdge(value) {
15084
15241
  record: out
15085
15242
  };
15086
15243
  }
15244
+ /**
15245
+ * The membership change a `CREATE` or `UPDATE` notification reports: the
15246
+ * edge's target joined the view (or its version moved). Null for anything a
15247
+ * re-read must settle instead: a `DELETE` (a row leaving, or every edge of a
15248
+ * view being rewritten), a subquery child (`parent` set), or an edge missing
15249
+ * its target or version. With `FETCH out` the target is the joined row, whose
15250
+ * `id` is the record id.
15251
+ */
15252
+ function deltaOfEdge(action, value) {
15253
+ if (action !== "CREATE" && action !== "UPDATE") return null;
15254
+ const hash = hashOfEdge(value);
15255
+ const edge = value;
15256
+ if (!hash || edge.parent != null || typeof edge.version !== "number") return null;
15257
+ const out = edge.out instanceof RecordId$1 ? edge.out : edge.out?.id;
15258
+ if (!(out instanceof RecordId$1)) return null;
15259
+ return {
15260
+ hash,
15261
+ id: encodeRecordId(out),
15262
+ version: edge.version
15263
+ };
15264
+ }
15087
15265
 
15088
15266
  //#endregion
15089
15267
  //#region src/utils/blurhash.ts
@@ -15541,7 +15719,7 @@ function createServices(config) {
15541
15719
  streamProcessor.configureCircuitPersistence(config.persistCircuit ?? config.localEngine === "sqlite", config.circuitCheckpointMs);
15542
15720
  streamProcessor.configureProjection(config.circuitProjection ?? true);
15543
15721
  const migrator = new LocalMigrator(local, logger);
15544
- const crdt = new CrdtManager(config.schema, local, remote, logger, config.crdtDebounceMs ?? 300);
15722
+ const crdt = new CrdtManager(config.schema, local, remote, logger, config.crdtDebounceMs ?? 50, config.crdtSendIntervalMs ?? 200);
15545
15723
  const auth = new AuthService(config.schema, remote, persistence, logger);
15546
15724
  const tabId = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `tab_${Math.random().toString(36).slice(2)}`;
15547
15725
  return {
@@ -15567,7 +15745,7 @@ function createTabsCoordinator(config, s, host) {
15567
15745
  return new TabsCoordinator({
15568
15746
  tabId: s.tabId,
15569
15747
  fingerprint: computeTabsFingerprint({
15570
- coreVersion: "0.0.1-canary.331",
15748
+ coreVersion: "0.0.1-canary.333",
15571
15749
  schemaHash: hash53(config.schemaSurql),
15572
15750
  endpoint: config.database.endpoint ?? "",
15573
15751
  namespace: config.database.namespace,
@@ -15787,7 +15965,8 @@ function createAdapters(config, s, host, lateModules) {
15787
15965
  const hash = hashOfEdge(message.value);
15788
15966
  if (!hash) return;
15789
15967
  const row = message.action === "DELETE" ? null : rowOfEdge(message.value);
15790
- onChange([hash], row ? [row] : void 0);
15968
+ const delta = deltaOfEdge(message.action, message.value);
15969
+ onChange([hash], row ? [row] : void 0, delta ? [delta] : void 0);
15791
15970
  });
15792
15971
  return String(uuid);
15793
15972
  },
@@ -16686,4 +16865,4 @@ var Sp00kyClient = class {
16686
16865
  };
16687
16866
 
16688
16867
  //#endregion
16689
- export { AppReleaseHandle, AppReleaseModule, AuthEventTypes, AuthService, BLURHASH_IMAGE_EXTENSIONS, BucketHandle, CURSOR_COLORS, CrdtField, CrdtManager, DEFAULT_LOCAL_OP_TIMEOUT_MS, FeatureFlagHandle, FeatureFlagModule, IMPERSONATION_ACCESS, ImpersonationTimers, LocalOpTimeoutError, MATERIALIZATION_SAMPLE_WINDOW, ORIGIN_TOKEN_KEY, PUSH_ENDPOINT_KEY, PUSH_PAYLOAD_VERSION, SESSION_POLL_MS, SIGN_OUT_HOOK_TIMEOUT_MS, Sp00kyClient, WebPushError, WebPushModule, blurhashSidecarPath, bucketContentToBlob, createAuthEventSystem, cursorColorFromName, decode as decodeBlurhash, decodeTokenClaims, detectWebPushSupport, encode as encodeBlurhash, encodeImageToBlurhash, fileToUint8Array, impersonationFromToken, isBlurhashValid, isImagePath, isPushPayload, renewDelayMs, sameImpersonation, semverGt, textToHtml };
16868
+ export { AppReleaseHandle, AppReleaseModule, AuthEventTypes, AuthService, BLURHASH_IMAGE_EXTENSIONS, BucketHandle, CURSOR_COLORS, CrdtField, CrdtManager, DEFAULT_LOCAL_OP_TIMEOUT_MS, FeatureFlagHandle, FeatureFlagModule, IMPERSONATION_ACCESS, ImpersonationTimers, LocalOpTimeoutError, MATERIALIZATION_SAMPLE_WINDOW, ORIGIN_TOKEN_KEY, PUSH_ENDPOINT_KEY, PUSH_PAYLOAD_VERSION, REFUSAL_RECHECK_MIN_MS, SESSION_POLL_MS, SIGN_OUT_HOOK_TIMEOUT_MS, Sp00kyClient, WebPushError, WebPushModule, blurhashSidecarPath, bucketContentToBlob, createAuthEventSystem, cursorColorFromName, decode as decodeBlurhash, decodeTokenClaims, detectWebPushSupport, encode as encodeBlurhash, encodeImageToBlurhash, fileToUint8Array, impersonationFromToken, isBlurhashValid, isImagePath, isPushPayload, renewDelayMs, sameImpersonation, semverGt, textToHtml };
@@ -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
  /**