@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 +1 -1
- package/dist/index.d.ts +19 -4
- package/dist/index.js +220 -41
- package/dist/otel/index.d.ts +1 -1
- package/dist/pure.d.ts +16 -0
- package/dist/types.d.ts +62 -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/auth/auth.session-recheck.test.ts +140 -0
- package/src/modules/auth/index.ts +68 -1
- package/src/modules/crdt/crdt.test.ts +52 -6
- package/src/modules/crdt/index.ts +59 -23
- package/src/query/anon-query-isolation.integration.test.ts +7 -3
- 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/query/session-guard.integration.test.ts +302 -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/src/utils/error-classification.test.ts +19 -1
- package/src/utils/error-classification.ts +12 -0
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 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,
|
|
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.
|
|
2192
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
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.
|
|
4859
|
-
*
|
|
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(
|
|
4924
|
-
const outcome = yield* applyMembership(hash,
|
|
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.
|
|
6715
|
-
*
|
|
6716
|
-
*
|
|
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
|
-
/**
|
|
6769
|
-
|
|
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*
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
12994
|
-
|
|
12995
|
-
|
|
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
|
-
},
|
|
13000
|
-
}
|
|
13001
|
-
|
|
13002
|
-
|
|
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
|
-
|
|
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 ??
|
|
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.
|
|
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
|
-
|
|
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 };
|
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
|
/**
|