@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 +1 -1
- package/dist/index.d.ts +19 -4
- package/dist/index.js +150 -40
- package/dist/otel/index.d.ts +1 -1
- package/dist/pure.d.ts +16 -0
- package/dist/types.d.ts +29 -11
- package/package.json +3 -3
- package/src/client/router.ts +1 -1
- package/src/client/services.ts +4 -3
- package/src/kernel/events.ts +2 -2
- package/src/kernel/interpreter.ts +3 -3
- package/src/modules/crdt/crdt.test.ts +52 -6
- package/src/modules/crdt/index.ts +59 -23
- package/src/query/membership.saga.test.ts +73 -1
- package/src/query/membership.saga.ts +39 -5
- package/src/query/membership.test.ts +33 -0
- package/src/query/membership.ts +33 -0
- package/src/sync/live.saga.test.ts +51 -0
- package/src/sync/live.saga.ts +17 -8
- package/src/types.ts +29 -10
- package/src/utils/edges.ts +19 -1
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
|
|
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
|
|
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,
|
|
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.
|
|
2192
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
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.
|
|
4859
|
-
*
|
|
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(
|
|
4924
|
-
const outcome = yield* applyMembership(hash,
|
|
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.
|
|
6715
|
-
*
|
|
6716
|
-
*
|
|
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
|
-
/**
|
|
6769
|
-
|
|
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*
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
12994
|
-
|
|
12995
|
-
|
|
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
|
-
},
|
|
13000
|
-
}
|
|
13001
|
-
|
|
13002
|
-
|
|
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
|
-
|
|
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 ??
|
|
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.
|
|
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
|
-
|
|
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
|
},
|
package/dist/otel/index.d.ts
CHANGED
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
|
-
*
|
|
1598
|
-
*
|
|
1599
|
-
*
|
|
1600
|
-
*
|
|
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
|
|
1639
|
-
*
|
|
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.
|
|
1646
|
-
*
|
|
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 {
|
|
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.
|
|
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.
|
|
79
|
-
"@spooky-sync/ssp-wasm": "0.0.1-canary.
|
|
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",
|
package/src/client/router.ts
CHANGED
|
@@ -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':
|
package/src/client/services.ts
CHANGED
|
@@ -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 ??
|
|
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
|
-
|
|
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
|
},
|