@spooky-sync/core 0.0.1-canary.320 → 0.0.1-canary.322
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 +17 -7
- package/dist/index.js +133 -38
- package/dist/live.d.ts +9 -1
- package/dist/live.js +36 -6
- package/dist/pure.d.ts +38 -2
- package/dist/pure.js +2 -2
- package/dist/types.d.ts +10 -0
- package/dist/worker-edges.js +108 -67
- package/dist/worker-ref-tables.d.ts +3 -1
- package/package.json +3 -3
- package/src/boot/boot.saga.test.ts +30 -7
- package/src/boot/boot.saga.ts +10 -6
- package/src/client/services.test.ts +25 -2
- package/src/client/services.ts +41 -13
- package/src/kernel/effects.ts +8 -4
- package/src/kernel/interpreter.ts +3 -3
- package/src/live/index.test.ts +38 -4
- package/src/live/index.ts +52 -6
- package/src/modules/ref-tables.test.ts +3 -1
- package/src/modules/ref-tables.ts +4 -1
- package/src/pure.ts +1 -0
- package/src/query/anon-query-isolation.integration.test.ts +496 -0
- package/src/query/list-ref-live.integration.test.ts +203 -0
- package/src/query/membership.saga.test.ts +5 -5
- package/src/query/register.saga.ts +7 -1
- package/src/query/sql.test.ts +37 -0
- package/src/query/sql.ts +48 -3
- package/src/services/database/remote.ts +20 -2
- package/src/state/client-state.ts +8 -0
- package/src/state/reducers.ts +4 -1
- package/src/sync/connection.saga.test.ts +13 -2
- package/src/sync/connection.saga.ts +17 -13
- package/src/sync/policy.test.ts +13 -9
- package/src/sync/policy.ts +16 -3
- package/src/testing/adapters.ts +5 -1
- package/src/utils/index.ts +11 -0
package/AGENTS.md
CHANGED
|
@@ -42,7 +42,7 @@ re-materialized from state, never patched in place.
|
|
|
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`.
|
|
45
|
-
- Subpaths for service workers, workers and Node (no wasm, no pino, no DOM; `scripts/check-worker-entries.mjs` enforces it in `build`): `@spooky-sync/core/pure` (protocol builders), `@spooky-sync/core/live` (`createLiveFeed`, the live subscriber over the pure-JS SDK), `@spooky-sync/core/sw` (`installPushHandlers`, `getBridgedToken`). Built by the second tsdown config; chunks are prefixed `worker-`. See `docs/web-push-design.md` at the repo root.
|
|
45
|
+
- Subpaths for service workers, workers and Node (no wasm, no pino, no DOM; `scripts/check-worker-entries.mjs` enforces it in `build`): `@spooky-sync/core/pure` (protocol builders), `@spooky-sync/core/live` (`createLiveFeed`, the live subscriber over the pure-JS SDK; a feed that shares views with another through the same `sessionId` must also pass the same `ownerSecret`, or a signed-out feed's second registration is refused 409 `owner_mismatch`), `@spooky-sync/core/sw` (`installPushHandlers`, `getBridgedToken`). Built by the second tsdown config; chunks are prefixed `worker-`. See `docs/web-push-design.md` at the repo root.
|
|
46
46
|
|
|
47
47
|
## Common gotchas
|
|
48
48
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1123,7 +1123,7 @@ type Effect = {
|
|
|
1123
1123
|
kind: 'now';
|
|
1124
1124
|
} | {
|
|
1125
1125
|
kind: 'id';
|
|
1126
|
-
scope:
|
|
1126
|
+
scope: IdScope;
|
|
1127
1127
|
} | {
|
|
1128
1128
|
kind: 'hash';
|
|
1129
1129
|
input: string;
|
|
@@ -1141,6 +1141,8 @@ type Effect = {
|
|
|
1141
1141
|
name: ServiceName;
|
|
1142
1142
|
args: unknown[];
|
|
1143
1143
|
};
|
|
1144
|
+
/** `mutation`: an outbox row id. `salt`: a query-key salt. `owner`: a page's anonymous-view owner secret. */
|
|
1145
|
+
type IdScope = 'mutation' | 'salt' | 'owner';
|
|
1144
1146
|
/**
|
|
1145
1147
|
* Calls into the legacy services that are adapters, not state: auth, tabs,
|
|
1146
1148
|
* blobs, migrator, crdt, persistence, the SSP lifecycle, the remote socket.
|
|
@@ -1149,8 +1151,12 @@ type Effect = {
|
|
|
1149
1151
|
interface ServiceCalls {
|
|
1150
1152
|
'hint.read': () => string | null;
|
|
1151
1153
|
'hint.write': (bucketId: string) => void;
|
|
1152
|
-
|
|
1153
|
-
'salt.
|
|
1154
|
+
/** The salt (and owner secret) this tab's previous page left, if minted for `authId`. */
|
|
1155
|
+
'salt.take': (authId: string | null) => {
|
|
1156
|
+
salt: string;
|
|
1157
|
+
owner: string | null;
|
|
1158
|
+
} | null;
|
|
1159
|
+
'salt.leave': (salt: string, authId: string | null, owner: string | null) => void;
|
|
1154
1160
|
'local.connect': (bucketId: string) => Promise<void>;
|
|
1155
1161
|
'local.switchStore': (bucketId: string) => Promise<void>;
|
|
1156
1162
|
'local.beginSwitch': () => () => void;
|
|
@@ -1309,6 +1315,13 @@ interface ClientState {
|
|
|
1309
1315
|
readonly userId: string | null;
|
|
1310
1316
|
/** The principal the current session salt was minted for. */
|
|
1311
1317
|
readonly saltUserId: string | null;
|
|
1318
|
+
/**
|
|
1319
|
+
* This page's secret, sent with every registration (`$config.owner`). An
|
|
1320
|
+
* anonymous view answers only to the secret it was registered with, so it
|
|
1321
|
+
* lives as long as the page's views: a reconnect keeps it, and a reload
|
|
1322
|
+
* that keeps the salt keeps it too.
|
|
1323
|
+
*/
|
|
1324
|
+
readonly ownerSecret: string | null;
|
|
1312
1325
|
/** Latest bucket-switch target; an older switch that wakes up to a newer target skips. */
|
|
1313
1326
|
readonly pendingBucket: string | null;
|
|
1314
1327
|
readonly tabId: string;
|
|
@@ -1428,10 +1441,7 @@ interface Adapters {
|
|
|
1428
1441
|
clock: {
|
|
1429
1442
|
now(): number;
|
|
1430
1443
|
};
|
|
1431
|
-
ids:
|
|
1432
|
-
mutation(): string;
|
|
1433
|
-
salt(): string;
|
|
1434
|
-
};
|
|
1444
|
+
ids: Record<IdScope, () => string>;
|
|
1435
1445
|
hash(input: string): Promise<string>;
|
|
1436
1446
|
services: ServiceCalls;
|
|
1437
1447
|
}
|
package/dist/index.js
CHANGED
|
@@ -569,6 +569,16 @@ function generateId() {
|
|
|
569
569
|
return Uuid.v4().toString().replace(/-/g, "");
|
|
570
570
|
}
|
|
571
571
|
/**
|
|
572
|
+
* 32 random bytes, hex: the secret a page proves it owns its anonymous views
|
|
573
|
+
* with (`$config.owner` on `fn::query::register`). `getRandomValues` rather
|
|
574
|
+
* than `randomUUID`, which a page served over plain http does not have.
|
|
575
|
+
*/
|
|
576
|
+
function mintOwnerSecret() {
|
|
577
|
+
const bytes = new Uint8Array(32);
|
|
578
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
579
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
572
582
|
* Read the millisecond count off a surrealdb `Duration`. Different `surrealdb`
|
|
573
583
|
* releases expose it under `milliseconds` (current) or the older private
|
|
574
584
|
* `_milliseconds` field, so read whichever is set; returns 0 when neither is.
|
|
@@ -2178,8 +2188,8 @@ function trimHistory(history, cap) {
|
|
|
2178
2188
|
|
|
2179
2189
|
//#endregion
|
|
2180
2190
|
//#region src/modules/devtools/index.ts
|
|
2181
|
-
const CORE_VERSION = "0.0.1-canary.
|
|
2182
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
2191
|
+
const CORE_VERSION = "0.0.1-canary.322";
|
|
2192
|
+
const WASM_VERSION = "0.0.1-canary.322";
|
|
2183
2193
|
const SURREAL_VERSION = "3.0.3";
|
|
2184
2194
|
var DevToolsService = class DevToolsService {
|
|
2185
2195
|
version = CORE_VERSION;
|
|
@@ -3726,6 +3736,8 @@ function phaseTimings(e) {
|
|
|
3726
3736
|
* both sides resolve it to the shared `_00_list_ref_anon` table.
|
|
3727
3737
|
*/
|
|
3728
3738
|
const ANON_USER_ID = "anon";
|
|
3739
|
+
/** The one `_00_list_ref` table every signed-out session shares. */
|
|
3740
|
+
const ANON_LIST_REF_TABLE = "_00_list_ref_anon";
|
|
3729
3741
|
/**
|
|
3730
3742
|
* Default ref-storage mode for this client build. Mirrors the SSP's
|
|
3731
3743
|
* default (`RefMode::Dedicated`) so cross-session sync works out of the
|
|
@@ -3774,7 +3786,7 @@ function bucketIdForUser(userId) {
|
|
|
3774
3786
|
* single mode.
|
|
3775
3787
|
*/
|
|
3776
3788
|
function listRefTableFor(mode, userId) {
|
|
3777
|
-
if (userId === ANON_USER_ID) return
|
|
3789
|
+
if (userId === ANON_USER_ID) return ANON_LIST_REF_TABLE;
|
|
3778
3790
|
if (mode === "single") return "_00_list_ref";
|
|
3779
3791
|
const uid = sanitizeUserId(userId);
|
|
3780
3792
|
return uid ? `_00_list_ref_user_${uid}` : "_00_list_ref";
|
|
@@ -3987,6 +3999,7 @@ function emptyState(init) {
|
|
|
3987
3999
|
sessionId: null,
|
|
3988
4000
|
userId: null,
|
|
3989
4001
|
saltUserId: null,
|
|
4002
|
+
ownerSecret: null,
|
|
3990
4003
|
pendingBucket: null,
|
|
3991
4004
|
tabId: init.tabId,
|
|
3992
4005
|
tabRole: "solo",
|
|
@@ -4350,15 +4363,44 @@ const readViewRows = () => `SELECT * FROM ${VIEW_TABLE}`;
|
|
|
4350
4363
|
/** Copy every legacy `_00_window` row into `_00_view` once. */
|
|
4351
4364
|
const readLegacyViewRows = () => `SELECT * FROM ${LEGACY_VIEW_TABLE}`;
|
|
4352
4365
|
const countViewRows = () => `SELECT count() FROM ${VIEW_TABLE} GROUP ALL`;
|
|
4353
|
-
const listRefSelect = (table) => `SELECT out, version FROM ${table} WHERE
|
|
4354
|
-
const subqueryListRefSelect = (table) => `SELECT out, version FROM ${table} WHERE
|
|
4366
|
+
const listRefSelect = (table) => `SELECT out, version FROM $in->${table} WHERE parent IS NONE`;
|
|
4367
|
+
const subqueryListRefSelect = (table) => `SELECT out, version FROM $in->${table} WHERE parent IS NOT NONE`;
|
|
4355
4368
|
const queryRowCountSelect = () => "SELECT VALUE { rowCount: rowCount, state: state } FROM ONLY $in";
|
|
4356
|
-
const listRefBatchSelect = (table) => `SELECT in, out, version, parent FROM ${table}
|
|
4369
|
+
const listRefBatchSelect = (table) => `SELECT in, out, version, parent FROM $ins->${table}`;
|
|
4357
4370
|
const queryRowCountBatchSelect = () => "SELECT VALUE { id: id, rowCount: rowCount, state: state } FROM $ins";
|
|
4358
4371
|
/** The single-query read: edges, meta, subquery children in one request. */
|
|
4359
4372
|
const singleSnapshotSelect = (table) => `${listRefSelect(table)};\n${queryRowCountSelect()};\n${subqueryListRefSelect(table)}`;
|
|
4360
4373
|
/** The many-query read: all edges + all metas in one request. */
|
|
4361
4374
|
const batchSnapshotSelect = (table) => `${listRefBatchSelect(table)};\n${queryRowCountBatchSelect()}`;
|
|
4375
|
+
/**
|
|
4376
|
+
* The session's one LIVE subscription on its `_00_list_ref` table.
|
|
4377
|
+
*
|
|
4378
|
+
* `inlineBodies` adds `FETCH out`, which resolves the edge's target into the
|
|
4379
|
+
* notification so the body arrives with the doorbell.
|
|
4380
|
+
*
|
|
4381
|
+
* On the anon table it is filtered to this client's own edges. Every
|
|
4382
|
+
* signed-out session shares that table, so unfiltered each one received (and
|
|
4383
|
+
* the server fetched a body for) every anonymous edge change of every other
|
|
4384
|
+
* session, and the client then dropped all but its own. An edge carries the
|
|
4385
|
+
* `session::id()` of whichever socket last registered its view, which is a
|
|
4386
|
+
* socket of this client but not necessarily the current one: a view stays
|
|
4387
|
+
* registered across a reconnect, and a reload keeps its salt and its views,
|
|
4388
|
+
* so their older edges still name an earlier socket. `sessionIds` are those
|
|
4389
|
+
* earlier sockets. The current one is matched by `session::id()` itself,
|
|
4390
|
+
* which SurrealDB evaluates in the subscriber's own session, so the filter
|
|
4391
|
+
* holds before the client has learned its new id.
|
|
4392
|
+
*
|
|
4393
|
+
* The cast is load-bearing: `session::id()` is a uuid, `clientId` a string,
|
|
4394
|
+
* and without it nothing matches. Per-user tables need no filter.
|
|
4395
|
+
*/
|
|
4396
|
+
function listRefLive(table, opts) {
|
|
4397
|
+
const join = opts.inlineBodies ? " FETCH out" : "";
|
|
4398
|
+
if (table !== ANON_LIST_REF_TABLE) return { sql: `LIVE SELECT * FROM ${table}${join}` };
|
|
4399
|
+
return {
|
|
4400
|
+
sql: `LIVE SELECT * FROM ${table} WHERE clientId = <string>session::id() OR clientId IN $sids${join}`,
|
|
4401
|
+
vars: { sids: [...opts.sessionIds] }
|
|
4402
|
+
};
|
|
4403
|
+
}
|
|
4362
4404
|
/** Register + read back edges/meta/children in ONE request. */
|
|
4363
4405
|
const registerSelect = (table) => `fn::query::register($config);\n${singleSnapshotSelect(table)}`;
|
|
4364
4406
|
const registerVars = (payload) => ({
|
|
@@ -4794,7 +4836,8 @@ function* registerRemote(env, hash, retry = false) {
|
|
|
4794
4836
|
id: entry.def.id,
|
|
4795
4837
|
surql: entry.def.surql,
|
|
4796
4838
|
params: { ...entry.def.params },
|
|
4797
|
-
ttl: entry.def.ttl
|
|
4839
|
+
ttl: entry.def.ttl,
|
|
4840
|
+
...state.ownerSecret ? { owner: state.ownerSecret } : {}
|
|
4798
4841
|
}), env.remoteTimeoutMs);
|
|
4799
4842
|
if (!(yield fx.state.read((s) => s.queries.has(hash)))) return;
|
|
4800
4843
|
const register = stmt(results, 0);
|
|
@@ -5842,11 +5885,14 @@ function* boot(env, opts) {
|
|
|
5842
5885
|
yield fx.dispatch({ type: "PrimeCircuit" });
|
|
5843
5886
|
const restoredUserId = yield fx.service("auth.restoreSession");
|
|
5844
5887
|
const authId = yield fx.service("auth.sessionAuthId");
|
|
5845
|
-
const
|
|
5888
|
+
const left = yield fx.service("salt.take", authId);
|
|
5889
|
+
const salt = left?.salt ?? (yield fx.id("salt"));
|
|
5890
|
+
const ownerSecret = left?.owner ?? (yield fx.id("owner"));
|
|
5846
5891
|
yield fx.state.update(setIdentity({
|
|
5847
5892
|
sessionId: salt,
|
|
5848
5893
|
userId: restoredUserId,
|
|
5849
|
-
saltUserId: authId
|
|
5894
|
+
saltUserId: authId,
|
|
5895
|
+
ownerSecret
|
|
5850
5896
|
}));
|
|
5851
5897
|
yield fx.service("crdt.setSessionId", salt);
|
|
5852
5898
|
if (restoredUserId) yield fx.service("ssp.setSessionAuth", authId, yield fx.service("auth.access"));
|
|
@@ -5941,12 +5987,13 @@ function* versionsPrimed(entries) {
|
|
|
5941
5987
|
* grace finds its views still warm.
|
|
5942
5988
|
*/
|
|
5943
5989
|
function* pageHide() {
|
|
5944
|
-
const [salt, saltUserId, ids] = yield fx.state.read((s) => [
|
|
5990
|
+
const [salt, saltUserId, ownerSecret, ids] = yield fx.state.read((s) => [
|
|
5945
5991
|
s.sessionId,
|
|
5946
5992
|
s.saltUserId,
|
|
5993
|
+
s.ownerSecret,
|
|
5947
5994
|
[...s.queries.values()].filter((e) => e.lifecycle.remote === "registered").map((e) => e.def.id)
|
|
5948
5995
|
]);
|
|
5949
|
-
if (salt) yield fx.service("salt.leave", salt, saltUserId);
|
|
5996
|
+
if (salt) yield fx.service("salt.leave", salt, saltUserId, ownerSecret);
|
|
5950
5997
|
if (ids.length > 0) yield fx.service("remote.releaseViews", ids);
|
|
5951
5998
|
}
|
|
5952
5999
|
function* warmBlobs() {
|
|
@@ -6053,7 +6100,7 @@ function createInterpreter(adapters, host) {
|
|
|
6053
6100
|
}
|
|
6054
6101
|
case "state.wait": return host.waitFor(effect.until, ctx?.signal);
|
|
6055
6102
|
case "now": return adapters.clock.now();
|
|
6056
|
-
case "id": return
|
|
6103
|
+
case "id": return adapters.ids[effect.scope]();
|
|
6057
6104
|
case "hash": return adapters.hash(effect.input);
|
|
6058
6105
|
case "emit": return host.emit(effect.event);
|
|
6059
6106
|
case "dispatch": return host.dispatch(effect.event);
|
|
@@ -6657,9 +6704,9 @@ function* landInlineRows(env, rows) {
|
|
|
6657
6704
|
//#region src/sync/connection.saga.ts
|
|
6658
6705
|
/**
|
|
6659
6706
|
* Transport state changes. A drop invalidates the live query and arms a
|
|
6660
|
-
* resubscribe
|
|
6661
|
-
*
|
|
6662
|
-
* `$auth.id` gate
|
|
6707
|
+
* resubscribe. The next `connected` restarts LIVE and drains the outbox;
|
|
6708
|
+
* outside the burst cooldown it also drops every remote registration so
|
|
6709
|
+
* `ensureRegistered` rebuilds them behind the `$auth.id` gate.
|
|
6663
6710
|
*/
|
|
6664
6711
|
function* connectionChanged(env, connection) {
|
|
6665
6712
|
const state = yield fx.state.read((s) => s);
|
|
@@ -6672,18 +6719,17 @@ function* connectionChanged(env, connection) {
|
|
|
6672
6719
|
if (connection !== "connected" || !state.sync.needsResubscribe) return;
|
|
6673
6720
|
const now = yield fx.now();
|
|
6674
6721
|
const last = state.sync.lastReconnectRefetchAt;
|
|
6675
|
-
if (last !== null && now - last < RECONNECT_REFETCH_COOLDOWN_MS) {
|
|
6676
|
-
|
|
6677
|
-
|
|
6722
|
+
if (last !== null && now - last < RECONNECT_REFETCH_COOLDOWN_MS) yield fx.state.update(patchSync({ needsResubscribe: false }));
|
|
6723
|
+
else {
|
|
6724
|
+
yield fx.state.update(compose(patchSync({
|
|
6725
|
+
needsResubscribe: false,
|
|
6726
|
+
lastReconnectRefetchAt: now
|
|
6727
|
+
}), ...[...state.queries.values()].filter((e) => e.lifecycle.remote !== "unregistered").map((e) => applyLifecycle(e.def.hash, { type: "remote-dropped" }))));
|
|
6728
|
+
yield fx.dispatch({
|
|
6729
|
+
type: "EnsureRegistered",
|
|
6730
|
+
requireAuth: true
|
|
6731
|
+
});
|
|
6678
6732
|
}
|
|
6679
|
-
yield fx.state.update(compose(patchSync({
|
|
6680
|
-
needsResubscribe: false,
|
|
6681
|
-
lastReconnectRefetchAt: now
|
|
6682
|
-
}), ...[...state.queries.values()].filter((e) => e.lifecycle.remote !== "unregistered").map((e) => applyLifecycle(e.def.hash, { type: "remote-dropped" }))));
|
|
6683
|
-
yield fx.dispatch({
|
|
6684
|
-
type: "EnsureRegistered",
|
|
6685
|
-
requireAuth: true
|
|
6686
|
-
});
|
|
6687
6733
|
yield fx.dispatch({ type: "LiveStart" });
|
|
6688
6734
|
yield fx.dispatch({ type: "Drain" });
|
|
6689
6735
|
}
|
|
@@ -8213,8 +8259,12 @@ function resolveReconnectConfig(input) {
|
|
|
8213
8259
|
*/
|
|
8214
8260
|
const REMOTE_MAX_CONCURRENT_QUERIES = 6;
|
|
8215
8261
|
/**
|
|
8216
|
-
* Session ids a page remembers for its pagehide release
|
|
8217
|
-
*
|
|
8262
|
+
* Session ids a page remembers for its pagehide release and its anonymous LIVE
|
|
8263
|
+
* filter. The release only needs the sessions of the last ttl; this many
|
|
8264
|
+
* reconnects inside one ttl is a broken network. The LIVE filter would like
|
|
8265
|
+
* every session that stamped an edge still held, but the SSP restamps an edge
|
|
8266
|
+
* whenever its row changes, so an id that falls off costs at most the LIVE
|
|
8267
|
+
* removal of a row untouched since, which the membership poll still catches.
|
|
8218
8268
|
*/
|
|
8219
8269
|
const MAX_SESSION_IDS = 16;
|
|
8220
8270
|
var RemoteDatabaseService = class extends AbstractDatabaseService {
|
|
@@ -8256,6 +8306,10 @@ var RemoteDatabaseService = class extends AbstractDatabaseService {
|
|
|
8256
8306
|
* whose own session id matches none of them, so it names these instead. Kept
|
|
8257
8307
|
* for the page, not the socket: a view registered before a reconnect still
|
|
8258
8308
|
* carries the old id until its next heartbeat.
|
|
8309
|
+
*
|
|
8310
|
+
* The anonymous LIVE filters on the same ids (see `listRefLive`): its edges
|
|
8311
|
+
* name the socket that registered their view, which may be an earlier one.
|
|
8312
|
+
* A reload hands its ids to the next page with the salt.
|
|
8259
8313
|
*/
|
|
8260
8314
|
sessionIdsSeen = [];
|
|
8261
8315
|
constructor(config, logger) {
|
|
@@ -8281,6 +8335,14 @@ var RemoteDatabaseService = class extends AbstractDatabaseService {
|
|
|
8281
8335
|
return this.sessionIdsSeen;
|
|
8282
8336
|
}
|
|
8283
8337
|
/**
|
|
8338
|
+
* Take on the session ids of the page this one reloaded from. It kept that
|
|
8339
|
+
* page's salt, so the views it re-registers still carry edges stamped by
|
|
8340
|
+
* those sockets. Older than anything this page has seen, so they go first.
|
|
8341
|
+
*/
|
|
8342
|
+
adoptSessionIds(ids) {
|
|
8343
|
+
this.sessionIdsSeen = [...ids.filter((sid) => typeof sid === "string" && sid !== "" && !this.sessionIdsSeen.includes(sid)), ...this.sessionIdsSeen].slice(-MAX_SESSION_IDS);
|
|
8344
|
+
}
|
|
8345
|
+
/**
|
|
8284
8346
|
* Ask the socket that just connected for its session id. Straight to the
|
|
8285
8347
|
* SDK: it needs no namespace or identity, so it need not wait behind an
|
|
8286
8348
|
* in-flight connect or take a slot from the query queue.
|
|
@@ -14696,6 +14758,11 @@ const RELOAD_SALT_KEY = "sp00ky:salt";
|
|
|
14696
14758
|
* query onto the `_00_query` row it already had, which the SSP answers from
|
|
14697
14759
|
* the live view instead of building a new one.
|
|
14698
14760
|
*
|
|
14761
|
+
* The page's owner secret travels with it: an anonymous row answers only to
|
|
14762
|
+
* the secret it was registered with, so the next page needs the same one to
|
|
14763
|
+
* rejoin those views. `null` from a page that predates it; the next page then
|
|
14764
|
+
* mints one, and the rows it left (which hold no secret) take the new one.
|
|
14765
|
+
*
|
|
14699
14766
|
* Taken, not read: the key is absent while a page lives, so a duplicated tab
|
|
14700
14767
|
* (which copies sessionStorage) mints its own salt rather than sharing this
|
|
14701
14768
|
* tab's remote views.
|
|
@@ -14707,17 +14774,33 @@ function takeReloadSalt(authId) {
|
|
|
14707
14774
|
if (raw === null) return null;
|
|
14708
14775
|
sessionStorage.removeItem(RELOAD_SALT_KEY);
|
|
14709
14776
|
const left = JSON.parse(raw);
|
|
14710
|
-
|
|
14777
|
+
if (typeof left.salt !== "string" || left.salt === "" || (left.authId ?? null) !== authId) return null;
|
|
14778
|
+
const sessionIds = Array.isArray(left.sessionIds) ? left.sessionIds.filter((sid) => typeof sid === "string") : [];
|
|
14779
|
+
const owner = typeof left.owner === "string" && left.owner !== "" ? left.owner : null;
|
|
14780
|
+
return {
|
|
14781
|
+
salt: left.salt,
|
|
14782
|
+
sessionIds,
|
|
14783
|
+
owner
|
|
14784
|
+
};
|
|
14711
14785
|
} catch {
|
|
14712
14786
|
return null;
|
|
14713
14787
|
}
|
|
14714
14788
|
}
|
|
14715
|
-
/**
|
|
14716
|
-
|
|
14789
|
+
/**
|
|
14790
|
+
* Leave this page's salt for the next page in this tab; see {@link takeReloadSalt}.
|
|
14791
|
+
*
|
|
14792
|
+
* The session ids of this page's sockets go with it. The views the next page
|
|
14793
|
+
* re-registers keep the edges this page's sockets stamped, and the anonymous
|
|
14794
|
+
* LIVE only hears edges stamped by a session it knows (see `listRefLive`).
|
|
14795
|
+
* So does the page's owner secret, which those views answer to.
|
|
14796
|
+
*/
|
|
14797
|
+
function leaveReloadSalt(salt, authId, sessionIds = [], owner = null) {
|
|
14717
14798
|
try {
|
|
14718
14799
|
if (typeof sessionStorage !== "undefined") sessionStorage.setItem(RELOAD_SALT_KEY, JSON.stringify({
|
|
14719
14800
|
salt,
|
|
14720
|
-
authId
|
|
14801
|
+
authId,
|
|
14802
|
+
sessionIds,
|
|
14803
|
+
owner
|
|
14721
14804
|
}));
|
|
14722
14805
|
} catch {}
|
|
14723
14806
|
}
|
|
@@ -14782,7 +14865,7 @@ function createTabsCoordinator(config, s, host) {
|
|
|
14782
14865
|
return new TabsCoordinator({
|
|
14783
14866
|
tabId: s.tabId,
|
|
14784
14867
|
fingerprint: computeTabsFingerprint({
|
|
14785
|
-
coreVersion: "0.0.1-canary.
|
|
14868
|
+
coreVersion: "0.0.1-canary.322",
|
|
14786
14869
|
schemaHash: hash53(config.schemaSurql),
|
|
14787
14870
|
endpoint: config.database.endpoint ?? "",
|
|
14788
14871
|
namespace: config.database.namespace,
|
|
@@ -14886,8 +14969,16 @@ function createAdapters(config, s, host, lateModules) {
|
|
|
14886
14969
|
const services = {
|
|
14887
14970
|
"hint.read": readBootBucketHint,
|
|
14888
14971
|
"hint.write": writeBootBucketHint,
|
|
14889
|
-
"salt.take":
|
|
14890
|
-
|
|
14972
|
+
"salt.take": (authId) => {
|
|
14973
|
+
const left = takeReloadSalt(authId);
|
|
14974
|
+
if (!left) return null;
|
|
14975
|
+
s.remote.adoptSessionIds(left.sessionIds);
|
|
14976
|
+
return {
|
|
14977
|
+
salt: left.salt,
|
|
14978
|
+
owner: left.owner
|
|
14979
|
+
};
|
|
14980
|
+
},
|
|
14981
|
+
"salt.leave": (salt, authId, owner) => leaveReloadSalt(salt, authId, s.remote.sessionIds(), owner),
|
|
14891
14982
|
"local.connect": (bucketId) => s.local.connect(bucketId),
|
|
14892
14983
|
"local.switchStore": (bucketId) => s.local.switchStore(bucketId),
|
|
14893
14984
|
"local.beginSwitch": () => s.local.beginSwitch(),
|
|
@@ -14983,8 +15074,11 @@ function createAdapters(config, s, host, lateModules) {
|
|
|
14983
15074
|
remote: {
|
|
14984
15075
|
queryResponses: (sql, vars) => s.remote.queryResponses(sql, vars),
|
|
14985
15076
|
live: async (table, onChange) => {
|
|
14986
|
-
const
|
|
14987
|
-
|
|
15077
|
+
const stmt = listRefLive(table, {
|
|
15078
|
+
inlineBodies: config.liveInlineBodies === true,
|
|
15079
|
+
sessionIds: s.remote.sessionIds()
|
|
15080
|
+
});
|
|
15081
|
+
const [uuid] = await s.remote.query(stmt.sql, stmt.vars);
|
|
14988
15082
|
(await s.remote.getClient().liveOf(uuid)).subscribe((message) => {
|
|
14989
15083
|
if (message.action === "KILLED") return;
|
|
14990
15084
|
const hash = hashOfEdge(message.value);
|
|
@@ -15021,7 +15115,8 @@ function createAdapters(config, s, host, lateModules) {
|
|
|
15021
15115
|
clock: { now: () => Date.now() },
|
|
15022
15116
|
ids: {
|
|
15023
15117
|
mutation: () => mintMutationId(s.tabId),
|
|
15024
|
-
salt: mintSalt
|
|
15118
|
+
salt: mintSalt,
|
|
15119
|
+
owner: mintOwnerSecret
|
|
15025
15120
|
},
|
|
15026
15121
|
hash: sha256Hex,
|
|
15027
15122
|
services
|
package/dist/live.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as RefMode } from "./worker-ref-tables.js";
|
|
2
2
|
import { SchemaStructure } from "@spooky-sync/query-builder";
|
|
3
3
|
|
|
4
4
|
//#region src/live/index.d.ts
|
|
@@ -43,6 +43,14 @@ interface LiveFeedOptions {
|
|
|
43
43
|
ttl?: string;
|
|
44
44
|
/** Salt of the `_00_query` keys. Default: a fresh random id per feed. */
|
|
45
45
|
sessionId?: string;
|
|
46
|
+
/**
|
|
47
|
+
* The secret the feed's registrations prove they own their views with
|
|
48
|
+
* (`$config.owner`): a signed-out feed's view answers only to the secret it
|
|
49
|
+
* was registered with. Default: a fresh one per feed. Pass the same one
|
|
50
|
+
* wherever you pass the same `sessionId`, or the second registrant of a
|
|
51
|
+
* shared anonymous view is refused.
|
|
52
|
+
*/
|
|
53
|
+
ownerSecret?: string;
|
|
46
54
|
/**
|
|
47
55
|
* The generated schema. Used to type query params (record ids, datetimes)
|
|
48
56
|
* exactly like the full client does before registering. Taken from a query
|
package/dist/live.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as singleSnapshotSelect, H as sha256Hex, I as ANON_USER_ID, L as DEFAULT_REF_MODE, O as registerSelect, S as listRefLive, U as queryHashInput, a as metaFromRow, c as mintOwnerSecret, d as withTimeout, f as parseQueryParams, g as bodySelect, k as registerVars, l as parseDuration, n as rowOfEdge, s as encodeRecordId, t as hashOfEdge, u as parseRecordIdString, v as heartbeatBatch, y as heartbeatRowGone, z as listRefTableFor } from "./worker-edges.js";
|
|
2
2
|
import { n as decodeTokenClaims } from "./worker-impersonation.js";
|
|
3
3
|
import { RecordId, Surreal } from "surrealdb";
|
|
4
4
|
|
|
@@ -15,10 +15,12 @@ import { RecordId, Surreal } from "surrealdb";
|
|
|
15
15
|
* - `fn::query::register($config)` plus the membership read-back in one
|
|
16
16
|
* request (`registerSelect` / `registerVars`, the same builders the full
|
|
17
17
|
* client's `registerRemote` uses; `$config = { id: _00_query:<key>, surql,
|
|
18
|
-
* params, ttl }`, key = sha256(JSON.stringify({ surql, params,
|
|
18
|
+
* params, ttl, owner }`, key = sha256(JSON.stringify({ surql, params,
|
|
19
|
+
* sessionId })), owner = the feed's `ownerSecret`);
|
|
19
20
|
* - bodies with `SELECT * FROM $ids`;
|
|
20
21
|
* - one `LIVE SELECT * FROM <the user's _00_list_ref table> FETCH out` per
|
|
21
|
-
* feed
|
|
22
|
+
* feed (on the shared anon table, filtered to the feed's own edges; see
|
|
23
|
+
* `listRefLive`), so a changed row usually arrives inline with its edge; an edge whose
|
|
22
24
|
* `out` is a bare id (a server that ignores FETCH) costs a body read, one
|
|
23
25
|
* whose `out` is null (unreadable or deleted row) a membership re-read;
|
|
24
26
|
* - `fn::query::heartbeat` at half the ttl, re-registering a reclaimed view;
|
|
@@ -45,6 +47,7 @@ const FLUSH_MS = 10;
|
|
|
45
47
|
const FETCH_BATCH_MS = 15;
|
|
46
48
|
const REREAD_MS = 50;
|
|
47
49
|
const RELEASE_TIMEOUT_MS = 2e3;
|
|
50
|
+
const MAX_SOCKETS = 16;
|
|
48
51
|
function deferred() {
|
|
49
52
|
let resolve;
|
|
50
53
|
let reject;
|
|
@@ -184,6 +187,9 @@ var Feed = class {
|
|
|
184
187
|
liveStarting = null;
|
|
185
188
|
subs = /* @__PURE__ */ new Map();
|
|
186
189
|
sessionId;
|
|
190
|
+
ownerSecret;
|
|
191
|
+
/** `session::id()` of every socket this feed has had, oldest first. */
|
|
192
|
+
sockets = [];
|
|
187
193
|
ttl;
|
|
188
194
|
timeoutMs;
|
|
189
195
|
isClosed = false;
|
|
@@ -198,6 +204,7 @@ var Feed = class {
|
|
|
198
204
|
constructor(options) {
|
|
199
205
|
this.options = options;
|
|
200
206
|
this.sessionId = options.sessionId ?? randomId();
|
|
207
|
+
this.ownerSecret = options.ownerSecret ?? mintOwnerSecret();
|
|
201
208
|
this.ttl = options.ttl ?? "10m";
|
|
202
209
|
this.timeoutMs = options.timeoutMs ?? 15e3;
|
|
203
210
|
}
|
|
@@ -273,9 +280,23 @@ var Feed = class {
|
|
|
273
280
|
}));
|
|
274
281
|
}
|
|
275
282
|
this.db = db;
|
|
283
|
+
this.noteSession();
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Remember the `session::id()` of the socket now open. Views are registered
|
|
287
|
+
* once and not again after a reconnect, so their edges keep naming the socket
|
|
288
|
+
* that registered them; the anonymous LIVE has to keep hearing those.
|
|
289
|
+
*/
|
|
290
|
+
async noteSession() {
|
|
291
|
+
try {
|
|
292
|
+
const [sid] = await this.q("RETURN <string>session::id()");
|
|
293
|
+
if (typeof sid !== "string" || this.sockets.includes(sid)) return;
|
|
294
|
+
this.sockets = [...this.sockets, sid].slice(-MAX_SOCKETS);
|
|
295
|
+
} catch {}
|
|
276
296
|
}
|
|
277
297
|
/** After a reconnect: LIVE again, then re-read every view (events in the gap are lost). */
|
|
278
298
|
async recover() {
|
|
299
|
+
this.noteSession();
|
|
279
300
|
try {
|
|
280
301
|
await this.ensureLive();
|
|
281
302
|
for (const sub of this.subs.values()) if (sub.loaded) this.scheduleReread(sub);
|
|
@@ -310,12 +331,18 @@ var Feed = class {
|
|
|
310
331
|
if (this.live || this.isClosed) return false;
|
|
311
332
|
if (!this.liveStarting) this.liveStarting = (async () => {
|
|
312
333
|
try {
|
|
334
|
+
const live = (inlineBodies) => listRefLive(this.table, {
|
|
335
|
+
inlineBodies,
|
|
336
|
+
sessionIds: this.sockets
|
|
337
|
+
});
|
|
313
338
|
let id;
|
|
314
339
|
try {
|
|
315
|
-
|
|
340
|
+
const { sql, vars } = live(true);
|
|
341
|
+
[id] = await this.q(sql, vars);
|
|
316
342
|
} catch (error) {
|
|
317
343
|
this.report(error);
|
|
318
|
-
|
|
344
|
+
const { sql, vars } = live(false);
|
|
345
|
+
[id] = await this.q(sql, vars);
|
|
319
346
|
}
|
|
320
347
|
const stream = await this.db.liveOf(id);
|
|
321
348
|
const off = stream.subscribe((message) => this.onLive(message));
|
|
@@ -515,7 +542,10 @@ var Feed = class {
|
|
|
515
542
|
}
|
|
516
543
|
/** Register (or re-register) a view and read its membership back, in one request. */
|
|
517
544
|
async register(sub) {
|
|
518
|
-
let snap = snapshotAt(await this.q(registerSelect(this.table), registerVars(
|
|
545
|
+
let snap = snapshotAt(await this.q(registerSelect(this.table), registerVars({
|
|
546
|
+
...sub.payload,
|
|
547
|
+
owner: this.ownerSecret
|
|
548
|
+
})), 1);
|
|
519
549
|
for (let i = 0; snap && snap.state === "materializing" && i < MATERIALIZE_RETRIES; i++) {
|
|
520
550
|
await sleep(MATERIALIZE_RETRY_MS);
|
|
521
551
|
snap = await this.readSnapshot(sub);
|
package/dist/pure.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as bucketIdForUser, i as RefMode, n as ANON_USER_ID, o as listRefTableFor, r as DEFAULT_REF_MODE, s as sanitizeUserId, t as ANON_LIST_REF_TABLE } from "./worker-ref-tables.js";
|
|
2
2
|
import { a as PushNotificationAction, c as PushPayloadKind, d as base64UrlToBytes, f as bytesToBase64Url, i as PushNotification, l as PushUrgency, m as sameBytes, n as BridgeTokenMessage, o as PushOp, p as isPushPayload, r as PUSH_PAYLOAD_VERSION, s as PushPayload, t as BridgeSignOutMessage, u as WebPushClientMessage } from "./worker-types.js";
|
|
3
3
|
import { Duration, RecordId } from "surrealdb";
|
|
4
4
|
import { ColumnSchema, RecordId as RecordId$1, SchemaStructure } from "@spooky-sync/query-builder";
|
|
@@ -201,11 +201,47 @@ declare const queryRowCountBatchSelect: () => string;
|
|
|
201
201
|
declare const singleSnapshotSelect: (table: string) => string;
|
|
202
202
|
/** The many-query read: all edges + all metas in one request. */
|
|
203
203
|
declare const batchSnapshotSelect: (table: string) => string;
|
|
204
|
+
/**
|
|
205
|
+
* The session's one LIVE subscription on its `_00_list_ref` table.
|
|
206
|
+
*
|
|
207
|
+
* `inlineBodies` adds `FETCH out`, which resolves the edge's target into the
|
|
208
|
+
* notification so the body arrives with the doorbell.
|
|
209
|
+
*
|
|
210
|
+
* On the anon table it is filtered to this client's own edges. Every
|
|
211
|
+
* signed-out session shares that table, so unfiltered each one received (and
|
|
212
|
+
* the server fetched a body for) every anonymous edge change of every other
|
|
213
|
+
* session, and the client then dropped all but its own. An edge carries the
|
|
214
|
+
* `session::id()` of whichever socket last registered its view, which is a
|
|
215
|
+
* socket of this client but not necessarily the current one: a view stays
|
|
216
|
+
* registered across a reconnect, and a reload keeps its salt and its views,
|
|
217
|
+
* so their older edges still name an earlier socket. `sessionIds` are those
|
|
218
|
+
* earlier sockets. The current one is matched by `session::id()` itself,
|
|
219
|
+
* which SurrealDB evaluates in the subscriber's own session, so the filter
|
|
220
|
+
* holds before the client has learned its new id.
|
|
221
|
+
*
|
|
222
|
+
* The cast is load-bearing: `session::id()` is a uuid, `clientId` a string,
|
|
223
|
+
* and without it nothing matches. Per-user tables need no filter.
|
|
224
|
+
*/
|
|
225
|
+
declare function listRefLive(table: string, opts: {
|
|
226
|
+
inlineBodies: boolean;
|
|
227
|
+
sessionIds: ReadonlyArray<string>;
|
|
228
|
+
}): {
|
|
229
|
+
sql: string;
|
|
230
|
+
vars?: Vars;
|
|
231
|
+
};
|
|
204
232
|
interface RegisterPayload {
|
|
205
233
|
id: RecordId<string>;
|
|
206
234
|
surql: string;
|
|
207
235
|
params: Record<string, unknown>;
|
|
208
236
|
ttl: string;
|
|
237
|
+
/**
|
|
238
|
+
* The registering page's secret. `fn::query::register` forwards only its
|
|
239
|
+
* hash, and the SSP lets a registration onto an existing anonymous view
|
|
240
|
+
* only with the hash that view was registered with: every signed-out
|
|
241
|
+
* visitor is `anon`, and anyone can read an anonymous view's id. Kept out
|
|
242
|
+
* of `params`, which name the computation the SSP shares between views.
|
|
243
|
+
*/
|
|
244
|
+
owner?: string;
|
|
209
245
|
}
|
|
210
246
|
/** Register + read back edges/meta/children in ONE request. */
|
|
211
247
|
declare const registerSelect: (table: string) => string;
|
|
@@ -334,4 +370,4 @@ declare function sha256Hex(input: string): Promise<string>;
|
|
|
334
370
|
*/
|
|
335
371
|
declare function queryKey(input: QueryKeyInput, sessionId: string | null): Promise<string>;
|
|
336
372
|
//#endregion
|
|
337
|
-
export { ANON_USER_ID, type Answer, BridgeSignOutMessage, BridgeTokenMessage, DEFAULT_REF_MODE, FAILED, IMPERSONATION_ACCESS, LEGACY_VIEW_TABLE, type ListRefSnapshot, PUSH_PAYLOAD_VERSION, PushNotification, PushNotificationAction, PushOp, PushPayload, PushPayloadKind, PushUrgency, type QueryKeyInput, type QueryMetaRow, type RefMode, RegisterPayload, type SealOptions, type SealedQuery, type SurqlHelper, type TokenClaims, type TxQuery, VIEW_TABLE, WebPushClientMessage, base64UrlToBytes, batchSnapshotSelect, bodySelect, bucketIdForUser, bytesToBase64Url, countViewRows, decodeTokenClaims, dedupeRecordVersions, encodeRecordId, hashOfEdge, heartbeatBatch, heartbeatRowGone, isPushPayload, leaveViewsBeacon, listRefBatchSelect, listRefSelect, listRefTableFor, metaFromRow, parseDuration, parseQueryParams, parseRecordIdString, queryHashInput, queryKey, queryRowCountBatchSelect, queryRowCountSelect, readLegacyViewRows, readViewRows, registerSelect, registerVars, rowOfEdge, sameBytes, sanitizeUserId, sha256Hex, singleSnapshotSelect, snapshotFromSingle, subqueryListRefSelect, surql, upsertBodiesTx, viewKeyInput, viewRecordId, viewRow };
|
|
373
|
+
export { ANON_LIST_REF_TABLE, ANON_USER_ID, type Answer, BridgeSignOutMessage, BridgeTokenMessage, DEFAULT_REF_MODE, FAILED, IMPERSONATION_ACCESS, LEGACY_VIEW_TABLE, type ListRefSnapshot, PUSH_PAYLOAD_VERSION, PushNotification, PushNotificationAction, PushOp, PushPayload, PushPayloadKind, PushUrgency, type QueryKeyInput, type QueryMetaRow, type RefMode, RegisterPayload, type SealOptions, type SealedQuery, type SurqlHelper, type TokenClaims, type TxQuery, VIEW_TABLE, WebPushClientMessage, base64UrlToBytes, batchSnapshotSelect, bodySelect, bucketIdForUser, bytesToBase64Url, countViewRows, decodeTokenClaims, dedupeRecordVersions, encodeRecordId, hashOfEdge, heartbeatBatch, heartbeatRowGone, isPushPayload, leaveViewsBeacon, listRefBatchSelect, listRefLive, listRefSelect, listRefTableFor, metaFromRow, parseDuration, parseQueryParams, parseRecordIdString, queryHashInput, queryKey, queryRowCountBatchSelect, queryRowCountSelect, readLegacyViewRows, readViewRows, registerSelect, registerVars, rowOfEdge, sameBytes, sanitizeUserId, sha256Hex, singleSnapshotSelect, snapshotFromSingle, subqueryListRefSelect, surql, upsertBodiesTx, viewKeyInput, viewRecordId, viewRow };
|
package/dist/pure.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as singleSnapshotSelect, B as sanitizeUserId, C as listRefSelect, D as readViewRows, E as readLegacyViewRows, F as ANON_LIST_REF_TABLE, H as sha256Hex, I as ANON_USER_ID, L as DEFAULT_REF_MODE, M as upsertBodiesTx, N as viewRecordId, O as registerSelect, P as viewRow, R as bucketIdForUser, S as listRefLive, T as queryRowCountSelect, U as queryHashInput, V as surql, W as viewKeyInput, _ as countViewRows, a as metaFromRow, b as leaveViewsBeacon, f as parseQueryParams, g as bodySelect, h as batchSnapshotSelect, i as dedupeRecordVersions, j as subqueryListRefSelect, k as registerVars, l as parseDuration, m as VIEW_TABLE, n as rowOfEdge, o as snapshotFromSingle, p as LEGACY_VIEW_TABLE, r as FAILED, s as encodeRecordId, t as hashOfEdge, u as parseRecordIdString, v as heartbeatBatch, w as queryRowCountBatchSelect, x as listRefBatchSelect, y as heartbeatRowGone, z as listRefTableFor } from "./worker-edges.js";
|
|
2
2
|
import { n as decodeTokenClaims, t as IMPERSONATION_ACCESS } from "./worker-impersonation.js";
|
|
3
3
|
import { a as sameBytes, i as isPushPayload, n as base64UrlToBytes, r as bytesToBase64Url, t as PUSH_PAYLOAD_VERSION } from "./worker-types.js";
|
|
4
4
|
|
|
@@ -21,4 +21,4 @@ function queryKey(input, sessionId) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
//#endregion
|
|
24
|
-
export { ANON_USER_ID, DEFAULT_REF_MODE, FAILED, IMPERSONATION_ACCESS, LEGACY_VIEW_TABLE, PUSH_PAYLOAD_VERSION, VIEW_TABLE, base64UrlToBytes, batchSnapshotSelect, bodySelect, bucketIdForUser, bytesToBase64Url, countViewRows, decodeTokenClaims, dedupeRecordVersions, encodeRecordId, hashOfEdge, heartbeatBatch, heartbeatRowGone, isPushPayload, leaveViewsBeacon, listRefBatchSelect, listRefSelect, listRefTableFor, metaFromRow, parseDuration, parseQueryParams, parseRecordIdString, queryHashInput, queryKey, queryRowCountBatchSelect, queryRowCountSelect, readLegacyViewRows, readViewRows, registerSelect, registerVars, rowOfEdge, sameBytes, sanitizeUserId, sha256Hex, singleSnapshotSelect, snapshotFromSingle, subqueryListRefSelect, surql, upsertBodiesTx, viewKeyInput, viewRecordId, viewRow };
|
|
24
|
+
export { ANON_LIST_REF_TABLE, ANON_USER_ID, DEFAULT_REF_MODE, FAILED, IMPERSONATION_ACCESS, LEGACY_VIEW_TABLE, PUSH_PAYLOAD_VERSION, VIEW_TABLE, base64UrlToBytes, batchSnapshotSelect, bodySelect, bucketIdForUser, bytesToBase64Url, countViewRows, decodeTokenClaims, dedupeRecordVersions, encodeRecordId, hashOfEdge, heartbeatBatch, heartbeatRowGone, isPushPayload, leaveViewsBeacon, listRefBatchSelect, listRefLive, listRefSelect, listRefTableFor, metaFromRow, parseDuration, parseQueryParams, parseRecordIdString, queryHashInput, queryKey, queryRowCountBatchSelect, queryRowCountSelect, readLegacyViewRows, readViewRows, registerSelect, registerVars, rowOfEdge, sameBytes, sanitizeUserId, sha256Hex, singleSnapshotSelect, snapshotFromSingle, subqueryListRefSelect, surql, upsertBodiesTx, viewKeyInput, viewRecordId, viewRow };
|
package/dist/types.d.ts
CHANGED
|
@@ -668,11 +668,21 @@ declare class RemoteDatabaseService extends AbstractDatabaseService {
|
|
|
668
668
|
* whose own session id matches none of them, so it names these instead. Kept
|
|
669
669
|
* for the page, not the socket: a view registered before a reconnect still
|
|
670
670
|
* carries the old id until its next heartbeat.
|
|
671
|
+
*
|
|
672
|
+
* The anonymous LIVE filters on the same ids (see `listRefLive`): its edges
|
|
673
|
+
* name the socket that registered their view, which may be an earlier one.
|
|
674
|
+
* A reload hands its ids to the next page with the salt.
|
|
671
675
|
*/
|
|
672
676
|
private sessionIdsSeen;
|
|
673
677
|
constructor(config: Sp00kyConfig<any>['database'], logger: Logger$1);
|
|
674
678
|
/** See {@link sessionIdsSeen}. */
|
|
675
679
|
sessionIds(): readonly string[];
|
|
680
|
+
/**
|
|
681
|
+
* Take on the session ids of the page this one reloaded from. It kept that
|
|
682
|
+
* page's salt, so the views it re-registers still carry edges stamped by
|
|
683
|
+
* those sockets. Older than anything this page has seen, so they go first.
|
|
684
|
+
*/
|
|
685
|
+
adoptSessionIds(ids: ReadonlyArray<string>): void;
|
|
676
686
|
/**
|
|
677
687
|
* Ask the socket that just connected for its session id. Straight to the
|
|
678
688
|
* SDK: it needs no namespace or identity, so it need not wait behind an
|