@spooky-sync/core 0.0.1-canary.286 → 0.0.1-canary.288
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 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +272 -46
- package/dist/live.js +1 -1
- package/dist/pure.d.ts +9 -2
- package/dist/pure.js +2 -2
- package/dist/worker-edges.js +10 -2
- package/package.json +3 -3
- package/src/boot/boot.saga.test.ts +9 -2
- package/src/boot/boot.saga.ts +4 -1
- package/src/boot/bucket-switch.saga.ts +2 -0
- package/src/client/runtime.test.ts +4 -1
- package/src/client/sp00ky-client.test.ts +8 -1
- package/src/kernel/constants.ts +15 -2
- package/src/query/lifecycle.saga.test.ts +169 -17
- package/src/query/lifecycle.saga.ts +86 -28
- package/src/query/materialize.saga.test.ts +36 -3
- package/src/query/materialize.saga.ts +15 -6
- package/src/query/materialize.test.ts +9 -1
- package/src/query/materialize.ts +14 -1
- package/src/query/membership.saga.test.ts +30 -2
- package/src/query/membership.saga.ts +18 -2
- package/src/query/membership.ts +30 -0
- package/src/query/render-set.test.ts +12 -1
- package/src/query/render-set.ts +11 -0
- package/src/query/sql.test.ts +2 -1
- package/src/query/sql.ts +15 -1
- package/src/services/tabs/coordinator.test.ts +21 -0
- package/src/services/tabs/coordinator.ts +5 -0
- package/src/state/client-state.ts +7 -0
- package/src/state/reducers.test.ts +19 -0
- package/src/state/reducers.ts +41 -3
- package/src/state/selectors.test.ts +18 -0
- package/src/state/selectors.ts +32 -0
- package/src/sync/tabs.saga.test.ts +4 -0
- package/src/sync/tabs.saga.ts +3 -0
package/AGENTS.md
CHANGED
|
@@ -55,6 +55,7 @@ re-materialized from state, never patched in place.
|
|
|
55
55
|
- **Raw remote calls are gated by the query allowlist.** When the generated schema carries `policy.queryAllowlist: 'warn' | 'enforce'` (from `sync.queryAllowlist` in `sp00ky.yml`), `remoteQuery`, `useRemote` and `queryRaw` throw unless the client config sets `allowRawRemote: true`; builder queries are unaffected. A registration the SSP refuses as not allowlisted (403 `not_allowlisted`) is marked `remote: 'failed'` after one attempt with an error-level log carrying the surql and a `spky generate` hint; the view stays cold (`membershipKnown` false), it is never read as confirmed empty.
|
|
56
56
|
- **`@parent` columns are auto-populated.** Defined in the `.surql` schema as `record<user>` with the `-- @parent` annotation; never write them yourself — they're set from the auth context server-side.
|
|
57
57
|
- **`db.preload` is a registered query.** Resolved before on this device (a `_00_view` row exists) it returns at once; never resolved it waits until the server's membership and every body are local. It holds no subscriber, so it is evicted a ttl after registration unless a view mounts the same query. `refresh`/`staleTime` are ignored; pass `signal` to abort a cold wait.
|
|
58
|
+
- **A never-resolved query paints only vouched rows.** Before the server answers (`cold`), the local predicate scan shows a cached body only if some `_00_view` row names it (as a member or subquery child) or a write for it is in flight. A body no view names is an orphan (deleted upstream while nothing watched it): it stays hidden, and the GC deletes it 30 s after boot and then hourly. A `_00_view` row no query has resolved for 14 days is retired by the same sweep, so that query paints cold again.
|
|
58
59
|
- **A rejected write is not lost.** Application errors roll the local row back (create/update/delete) and move the mutation to `_00_failed_mutations`; the tray API above lists, retries (as a fresh write) or discards it. Network errors keep the row queued.
|
|
59
60
|
|
|
60
61
|
## Module map (under `src/`)
|
package/dist/index.d.ts
CHANGED
|
@@ -1311,6 +1311,12 @@ interface ClientState {
|
|
|
1311
1311
|
readonly membershipReread: ReadonlyMap<QueryHash, number>;
|
|
1312
1312
|
/** Local body versions (`_00_rv`) by encoded record id. */
|
|
1313
1313
|
readonly versions: ReadonlyMap<string, number>;
|
|
1314
|
+
/**
|
|
1315
|
+
* What each durable `_00_view` row vouches for, by view key: its members
|
|
1316
|
+
* and subquery children. A body no row names is an orphan: it never paints
|
|
1317
|
+
* before the server answers and the GC deletes it.
|
|
1318
|
+
*/
|
|
1319
|
+
readonly views: ReadonlyMap<string, ReadonlyArray<string>>;
|
|
1314
1320
|
readonly outbox: ReadonlyArray<OutboxItem>;
|
|
1315
1321
|
readonly pendingWrites: ReadonlyMap<string, PendingWrite>;
|
|
1316
1322
|
readonly failedCount: number;
|
package/dist/index.js
CHANGED
|
@@ -1752,8 +1752,8 @@ function selfAllowlistedVariant(flag, userId) {
|
|
|
1752
1752
|
|
|
1753
1753
|
//#endregion
|
|
1754
1754
|
//#region src/modules/devtools/index.ts
|
|
1755
|
-
const CORE_VERSION = "0.0.1-canary.
|
|
1756
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
1755
|
+
const CORE_VERSION = "0.0.1-canary.288";
|
|
1756
|
+
const WASM_VERSION = "0.0.1-canary.288";
|
|
1757
1757
|
const SURREAL_VERSION = "3.0.3";
|
|
1758
1758
|
var DevToolsService = class DevToolsService {
|
|
1759
1759
|
eventsHistory = [];
|
|
@@ -2438,8 +2438,21 @@ const AUTH_READY_RETRY_MS = 500;
|
|
|
2438
2438
|
const AUTH_READY_MAX_ATTEMPTS = 10;
|
|
2439
2439
|
/** Heartbeat at this fraction of the shortest ttl in state. */
|
|
2440
2440
|
const TTL_HEARTBEAT_FRACTION = .5;
|
|
2441
|
-
/**
|
|
2442
|
-
|
|
2441
|
+
/**
|
|
2442
|
+
* Orphan body garbage collection: the first sweep waits this long after boot
|
|
2443
|
+
* (off the first paint and the first sync round), then repeats on the interval.
|
|
2444
|
+
*/
|
|
2445
|
+
const GC_BOOT_DELAY_MS = 3e4;
|
|
2446
|
+
const GC_INTERVAL_MS = 3600 * 1e3;
|
|
2447
|
+
/** Ids deleted per GC step. */
|
|
2448
|
+
const GC_CHUNK = 200;
|
|
2449
|
+
/**
|
|
2450
|
+
* A `_00_view` row no query has resolved for this long stops vouching for its
|
|
2451
|
+
* ids (the sweep deletes it, so its bodies can be collected). Each session
|
|
2452
|
+
* that resolves a query rewrites its row, so this only retires query shapes
|
|
2453
|
+
* the app no longer runs.
|
|
2454
|
+
*/
|
|
2455
|
+
const VIEW_RETENTION_MS = 336 * 60 * 60 * 1e3;
|
|
2443
2456
|
/** Rolling telemetry sample window per query. */
|
|
2444
2457
|
const TELEMETRY_SAMPLE_WINDOW = 100;
|
|
2445
2458
|
function backoffMs(attempt, base = RETRY_BASE_MS, max = RETRY_MAX_MS) {
|
|
@@ -2512,6 +2525,32 @@ const setServerState = (hash, serverState) => (s) => withEntry(s, hash, (e) => e
|
|
|
2512
2525
|
* set names: membership has caught up with the write, the overlay's job is
|
|
2513
2526
|
* done (invariant I7).
|
|
2514
2527
|
*/
|
|
2528
|
+
/** What a `_00_view` row holding these arrays vouches for. */
|
|
2529
|
+
const vouchedIds = (members, children) => [...new Set([...members.map(([id]) => id), ...children.map(([id]) => id)])];
|
|
2530
|
+
const withView = (s, viewKey, ids) => {
|
|
2531
|
+
const views = new Map(s.views);
|
|
2532
|
+
views.set(viewKey, ids);
|
|
2533
|
+
return {
|
|
2534
|
+
...s,
|
|
2535
|
+
views
|
|
2536
|
+
};
|
|
2537
|
+
};
|
|
2538
|
+
/**
|
|
2539
|
+
* Replace the durable view index with a fresh `_00_view` read (boot, bucket
|
|
2540
|
+
* switch, GC). An entry of a query in state is kept: its commits update the
|
|
2541
|
+
* index as they write the row, so it is never older than the read.
|
|
2542
|
+
*/
|
|
2543
|
+
const reloadViews = (fromStore) => (s) => {
|
|
2544
|
+
const views = new Map(fromStore);
|
|
2545
|
+
for (const e of s.queries.values()) {
|
|
2546
|
+
const held = s.views.get(e.def.viewKey);
|
|
2547
|
+
if (held) views.set(e.def.viewKey, held);
|
|
2548
|
+
}
|
|
2549
|
+
return {
|
|
2550
|
+
...s,
|
|
2551
|
+
views
|
|
2552
|
+
};
|
|
2553
|
+
};
|
|
2515
2554
|
const commitMembership = (hash, remoteArray, present) => (s) => {
|
|
2516
2555
|
const withArray = withEntry(s, hash, (e) => ({
|
|
2517
2556
|
...e,
|
|
@@ -2522,10 +2561,11 @@ const commitMembership = (hash, remoteArray, present) => (s) => {
|
|
|
2522
2561
|
})
|
|
2523
2562
|
}));
|
|
2524
2563
|
if (withArray === s) return s;
|
|
2564
|
+
const entry = withArray.queries.get(hash);
|
|
2525
2565
|
const named = new Set(remoteArray.map(([id]) => id));
|
|
2526
2566
|
const outbox = withArray.outbox.filter((item) => !(item.status === "acked" && named.has(item.recordId)));
|
|
2527
2567
|
return {
|
|
2528
|
-
...withArray,
|
|
2568
|
+
...withView(withArray, entry.def.viewKey, vouchedIds(remoteArray, entry.subqueryRemoteArray)),
|
|
2529
2569
|
outbox,
|
|
2530
2570
|
dirty: addAll(withArray.dirty, [hash])
|
|
2531
2571
|
};
|
|
@@ -2540,10 +2580,19 @@ const setLocalArray = (hash, localArray) => (s) => {
|
|
|
2540
2580
|
dirty: addAll(next.dirty, [hash])
|
|
2541
2581
|
};
|
|
2542
2582
|
};
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2583
|
+
/**
|
|
2584
|
+
* Replace a query's subquery child set. A query past `cold` has a `_00_view`
|
|
2585
|
+
* row, which records the children too (see `applySubqueryChildren`).
|
|
2586
|
+
*/
|
|
2587
|
+
const setSubqueryRemoteArray = (hash, subqueryRemoteArray) => (s) => {
|
|
2588
|
+
const next = withEntry(s, hash, (e) => ({
|
|
2589
|
+
...e,
|
|
2590
|
+
subqueryRemoteArray
|
|
2591
|
+
}));
|
|
2592
|
+
const entry = next.queries.get(hash);
|
|
2593
|
+
if (!entry || entry.lifecycle.phase === "cold") return next;
|
|
2594
|
+
return withView(next, entry.def.viewKey, vouchedIds(entry.remoteArray, subqueryRemoteArray));
|
|
2595
|
+
};
|
|
2547
2596
|
const setRecords = (hash, records, changed, materializeMs) => (s) => {
|
|
2548
2597
|
const next = withEntry(s, hash, (e) => {
|
|
2549
2598
|
const prev = e.telemetry.phaseSamples.localFetch ?? [];
|
|
@@ -2844,6 +2893,7 @@ const rebindQuery = (hash, next) => (s) => {
|
|
|
2844
2893
|
const clearBucketState = () => (s) => ({
|
|
2845
2894
|
...s,
|
|
2846
2895
|
versions: /* @__PURE__ */ new Map(),
|
|
2896
|
+
views: /* @__PURE__ */ new Map(),
|
|
2847
2897
|
outbox: [],
|
|
2848
2898
|
pendingWrites: /* @__PURE__ */ new Map(),
|
|
2849
2899
|
dirty: /* @__PURE__ */ new Set(),
|
|
@@ -2903,6 +2953,35 @@ function overlay(s) {
|
|
|
2903
2953
|
deletes
|
|
2904
2954
|
};
|
|
2905
2955
|
}
|
|
2956
|
+
const vouchedCache = /* @__PURE__ */ new WeakMap();
|
|
2957
|
+
/**
|
|
2958
|
+
* Every id some durable `_00_view` row vouches for. Memoized on the index,
|
|
2959
|
+
* which only changes when a view row does.
|
|
2960
|
+
*/
|
|
2961
|
+
function vouched(s) {
|
|
2962
|
+
const hit = vouchedCache.get(s.views);
|
|
2963
|
+
if (hit) return hit;
|
|
2964
|
+
const out = /* @__PURE__ */ new Set();
|
|
2965
|
+
for (const ids of s.views.values()) for (const id of ids) out.add(id);
|
|
2966
|
+
vouchedCache.set(s.views, out);
|
|
2967
|
+
return out;
|
|
2968
|
+
}
|
|
2969
|
+
/**
|
|
2970
|
+
* Bodies the GC must keep: `_00_` bookkeeping, anything a durable view
|
|
2971
|
+
* vouches for, anything a query in memory names (members or children), and
|
|
2972
|
+
* anything a write is still in flight for.
|
|
2973
|
+
*/
|
|
2974
|
+
function retained(s) {
|
|
2975
|
+
const held = /* @__PURE__ */ new Set();
|
|
2976
|
+
for (const e of s.queries.values()) {
|
|
2977
|
+
for (const [id] of e.remoteArray) held.add(id);
|
|
2978
|
+
for (const [id] of e.subqueryRemoteArray) held.add(id);
|
|
2979
|
+
}
|
|
2980
|
+
for (const item of s.outbox) held.add(item.recordId);
|
|
2981
|
+
for (const write of s.pendingWrites.values()) held.add(write.recordId);
|
|
2982
|
+
const known = vouched(s);
|
|
2983
|
+
return (id) => id.startsWith("_00_") || known.has(id) || held.has(id);
|
|
2984
|
+
}
|
|
2906
2985
|
const pendingDeleteIds = (s) => new Set(s.outbox.filter((i) => i.type === "delete").map((i) => i.recordId));
|
|
2907
2986
|
const hasAckedWrites = (s) => s.outbox.some((i) => i.status === "acked");
|
|
2908
2987
|
const pendingMutationCount = (s) => s.outbox.filter((i) => i.status === "pending").length;
|
|
@@ -3316,6 +3395,7 @@ function emptyState(init) {
|
|
|
3316
3395
|
registering: /* @__PURE__ */ new Set(),
|
|
3317
3396
|
membershipReread: /* @__PURE__ */ new Map(),
|
|
3318
3397
|
versions: /* @__PURE__ */ new Map(),
|
|
3398
|
+
views: /* @__PURE__ */ new Map(),
|
|
3319
3399
|
outbox: [],
|
|
3320
3400
|
pendingWrites: /* @__PURE__ */ new Map(),
|
|
3321
3401
|
failedCount: 0,
|
|
@@ -3362,6 +3442,24 @@ function parseViewRow(row) {
|
|
|
3362
3442
|
confirmed: row.confirmed === true
|
|
3363
3443
|
};
|
|
3364
3444
|
}
|
|
3445
|
+
const pairIds = (pairs) => Array.isArray(pairs) ? pairs.filter((p) => Array.isArray(p) && typeof p[0] === "string").map((p) => p[0]) : [];
|
|
3446
|
+
/** `SELECT * FROM _00_view` rows, keyed by view key; junk rows are skipped. */
|
|
3447
|
+
function parseViewIndex(rows) {
|
|
3448
|
+
if (!Array.isArray(rows)) return [];
|
|
3449
|
+
const out = [];
|
|
3450
|
+
for (const row of rows) {
|
|
3451
|
+
if (!row || typeof row !== "object" || !Array.isArray(row.ids)) continue;
|
|
3452
|
+
const id = row.id;
|
|
3453
|
+
const key = typeof id === "string" ? id.slice(id.indexOf(":") + 1) : String(id?.id ?? "");
|
|
3454
|
+
if (!key) continue;
|
|
3455
|
+
out.push({
|
|
3456
|
+
key,
|
|
3457
|
+
ids: [...new Set([...pairIds(row.ids), ...pairIds(row.children)])],
|
|
3458
|
+
updatedAt: typeof row.updatedAt === "number" ? row.updatedAt : 0
|
|
3459
|
+
});
|
|
3460
|
+
}
|
|
3461
|
+
return out;
|
|
3462
|
+
}
|
|
3365
3463
|
/**
|
|
3366
3464
|
* "Resolved before": the server answered this query on this device at some
|
|
3367
3465
|
* point. An unconfirmed empty row cannot be told apart from one written before
|
|
@@ -3634,11 +3732,19 @@ const selfHealDelayMs = (attempt) => Math.min(SELF_HEAL_MAX_MS, SELF_HEAL_BASE_M
|
|
|
3634
3732
|
const VIEW_TABLE = "_00_view";
|
|
3635
3733
|
const LEGACY_VIEW_TABLE = "_00_window";
|
|
3636
3734
|
const viewRecordId = (viewKey) => new RecordId$1(VIEW_TABLE, viewKey);
|
|
3637
|
-
|
|
3735
|
+
/**
|
|
3736
|
+
* The durable `_00_view` row: the confirmed membership plus the subquery
|
|
3737
|
+
* children, which the GC must keep and a cold scan may paint as much as the
|
|
3738
|
+
* members.
|
|
3739
|
+
*/
|
|
3740
|
+
const viewRow = (ids, children, confirmed, now) => ({
|
|
3638
3741
|
ids,
|
|
3742
|
+
children,
|
|
3639
3743
|
confirmed,
|
|
3640
3744
|
updatedAt: now
|
|
3641
3745
|
});
|
|
3746
|
+
/** Every `_00_view` row: the durable view index and the GC read it. */
|
|
3747
|
+
const readViewRows = () => `SELECT * FROM ${VIEW_TABLE}`;
|
|
3642
3748
|
/** Copy every legacy `_00_window` row into `_00_view` once. */
|
|
3643
3749
|
const readLegacyViewRows = () => `SELECT * FROM ${LEGACY_VIEW_TABLE}`;
|
|
3644
3750
|
const countViewRows = () => `SELECT count() FROM ${VIEW_TABLE} GROUP ALL`;
|
|
@@ -3747,7 +3853,7 @@ function* applyMembership(hash, remoteArray, meta, verifiedRemoval) {
|
|
|
3747
3853
|
});
|
|
3748
3854
|
const now = yield fx.now();
|
|
3749
3855
|
try {
|
|
3750
|
-
yield fx.local.upsert(VIEW_TABLE, viewRecordId(entry.def.viewKey), viewRow(remoteArray, true, now), "replace");
|
|
3856
|
+
yield fx.local.upsert(VIEW_TABLE, viewRecordId(entry.def.viewKey), viewRow(remoteArray, entry.subqueryRemoteArray, true, now), "replace");
|
|
3751
3857
|
} catch (err) {
|
|
3752
3858
|
yield fx.emit({
|
|
3753
3859
|
type: "log",
|
|
@@ -3777,11 +3883,28 @@ function* recoverLostView(hash) {
|
|
|
3777
3883
|
hash
|
|
3778
3884
|
});
|
|
3779
3885
|
}
|
|
3780
|
-
/**
|
|
3886
|
+
/**
|
|
3887
|
+
* Replace the subquery child set; bodies follow through the fetch plan. A
|
|
3888
|
+
* query with a `_00_view` row (past `cold`) records the children there too,
|
|
3889
|
+
* so the GC keeps their bodies and a cold scan may paint them.
|
|
3890
|
+
*/
|
|
3781
3891
|
function* applySubqueryChildren(hash, children) {
|
|
3782
3892
|
const entry = yield fx.state.read((s) => s.queries.get(hash));
|
|
3783
3893
|
if (!entry || recordVersionArraysEqual(entry.subqueryRemoteArray, children)) return;
|
|
3784
3894
|
yield fx.state.update(setSubqueryRemoteArray(hash, children));
|
|
3895
|
+
if (entry.lifecycle.phase !== "cold") try {
|
|
3896
|
+
yield fx.local.upsert(VIEW_TABLE, viewRecordId(entry.def.viewKey), { children }, "merge");
|
|
3897
|
+
} catch (err) {
|
|
3898
|
+
yield fx.emit({
|
|
3899
|
+
type: "log",
|
|
3900
|
+
level: "debug",
|
|
3901
|
+
message: "view row write failed",
|
|
3902
|
+
data: {
|
|
3903
|
+
hash,
|
|
3904
|
+
err
|
|
3905
|
+
}
|
|
3906
|
+
});
|
|
3907
|
+
}
|
|
3785
3908
|
yield fx.dispatch({ type: "FetchRows" });
|
|
3786
3909
|
}
|
|
3787
3910
|
const ok = (r) => r.ok;
|
|
@@ -4211,42 +4334,65 @@ function* ackPrune() {
|
|
|
4211
4334
|
yield fx.state.update(outboxPruneAcked(now, ACK_GRACE_MS));
|
|
4212
4335
|
if (yield fx.state.read((s) => s.outbox.some((i) => i.status === "acked"))) yield fx.timer.set("ack-prune", ACK_GRACE_MS, { type: "AckPrune" });
|
|
4213
4336
|
}
|
|
4337
|
+
/** Every `_00_view` row, parsed. Throws when the read fails. */
|
|
4338
|
+
function* readViews() {
|
|
4339
|
+
const res = yield fx.local.query(readViewRows());
|
|
4340
|
+
return parseViewIndex(Array.isArray(res) ? res[0] : void 0);
|
|
4341
|
+
}
|
|
4342
|
+
const indexOf = (rows) => new Map(rows.map((r) => [r.key, r.ids]));
|
|
4343
|
+
/**
|
|
4344
|
+
* Load the durable view index (boot, bucket switch). A failed read leaves it
|
|
4345
|
+
* empty, which fails closed: cold scans paint nothing until the server answers.
|
|
4346
|
+
*/
|
|
4347
|
+
function* loadViews() {
|
|
4348
|
+
try {
|
|
4349
|
+
yield fx.state.update(reloadViews(indexOf(yield* readViews())));
|
|
4350
|
+
} catch (error) {
|
|
4351
|
+
yield fx.emit({
|
|
4352
|
+
type: "log",
|
|
4353
|
+
level: "warn",
|
|
4354
|
+
message: "view index read failed",
|
|
4355
|
+
data: { error }
|
|
4356
|
+
});
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
const tableOf$1 = (id) => id.slice(0, id.indexOf(":"));
|
|
4214
4360
|
/**
|
|
4215
|
-
*
|
|
4216
|
-
*
|
|
4361
|
+
* Orphan collection, `GC_BOOT_DELAY_MS` after boot and then every
|
|
4362
|
+
* `GC_INTERVAL_MS`. Re-reads every `_00_view` row into the index, retires the
|
|
4363
|
+
* rows no query has resolved for `VIEW_RETENTION_MS`, then deletes the bodies
|
|
4364
|
+
* nothing retains (see `retained`) from the store and the circuit, a chunk at
|
|
4365
|
+
* a time. Only a leader or solo tab deletes, and a leader relays the deletes
|
|
4366
|
+
* so followers drop the rows too; a follower only refreshes its index.
|
|
4217
4367
|
*/
|
|
4218
4368
|
function* gcTick() {
|
|
4369
|
+
yield fx.state.wait((s) => s.primed);
|
|
4219
4370
|
try {
|
|
4220
|
-
const
|
|
4221
|
-
const
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
const
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
const
|
|
4232
|
-
|
|
4233
|
-
}
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
yield fx.state.update(deleteVersions(done));
|
|
4238
|
-
if (done.length > 0) yield fx.ssp.ingest(done.map((id) => ({
|
|
4239
|
-
table: id.slice(0, id.indexOf(":")),
|
|
4240
|
-
op: "DELETE",
|
|
4241
|
-
id,
|
|
4242
|
-
record: {}
|
|
4243
|
-
})));
|
|
4244
|
-
}
|
|
4371
|
+
const rows = yield* readViews();
|
|
4372
|
+
const [role, bucket, held] = yield fx.state.read((s) => [
|
|
4373
|
+
s.tabRole,
|
|
4374
|
+
s.bucketId,
|
|
4375
|
+
new Set([...s.queries.values()].map((e) => e.def.viewKey))
|
|
4376
|
+
]);
|
|
4377
|
+
const now = yield fx.now();
|
|
4378
|
+
const expired = role === "follower" ? [] : rows.filter((r) => !held.has(r.key) && now - r.updatedAt >= VIEW_RETENTION_MS);
|
|
4379
|
+
const retired = /* @__PURE__ */ new Set();
|
|
4380
|
+
for (let i = 0; i < expired.length; i += GC_CHUNK) {
|
|
4381
|
+
const chunk = expired.slice(i, i + GC_CHUNK);
|
|
4382
|
+
const settled = yield fx.all(chunk.map((r) => fx.local.delete(VIEW_TABLE, viewRecordId(r.key))));
|
|
4383
|
+
chunk.forEach((r, j) => settled[j].ok && retired.add(r.key));
|
|
4384
|
+
}
|
|
4385
|
+
yield fx.state.update(reloadViews(indexOf(rows.filter((r) => !retired.has(r.key)))));
|
|
4386
|
+
if (role === "follower") return;
|
|
4387
|
+
const removed = yield* collectOrphans(bucket, role === "leader");
|
|
4245
4388
|
yield fx.emit({
|
|
4246
4389
|
type: "log",
|
|
4247
4390
|
level: "info",
|
|
4248
4391
|
message: "orphan gc done",
|
|
4249
|
-
data: {
|
|
4392
|
+
data: {
|
|
4393
|
+
removed,
|
|
4394
|
+
retiredViews: retired.size
|
|
4395
|
+
}
|
|
4250
4396
|
});
|
|
4251
4397
|
} catch (error) {
|
|
4252
4398
|
yield fx.emit({
|
|
@@ -4255,8 +4401,50 @@ function* gcTick() {
|
|
|
4255
4401
|
message: "orphan gc failed",
|
|
4256
4402
|
data: { error }
|
|
4257
4403
|
});
|
|
4404
|
+
} finally {
|
|
4405
|
+
yield fx.timer.set("gc", GC_INTERVAL_MS, { type: "GcTick" });
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4408
|
+
/**
|
|
4409
|
+
* Delete every body nothing retains, `GC_CHUNK` at a time. Each chunk is
|
|
4410
|
+
* re-checked against fresh state (a membership committed meanwhile may name
|
|
4411
|
+
* an id again) and the sweep stops if the bucket moved under it.
|
|
4412
|
+
*/
|
|
4413
|
+
function* collectOrphans(bucket, relay) {
|
|
4414
|
+
const candidates = yield fx.state.read((s) => {
|
|
4415
|
+
const keep = retained(s);
|
|
4416
|
+
return [...s.versions.keys()].filter((id) => !keep(id));
|
|
4417
|
+
});
|
|
4418
|
+
let removed = 0;
|
|
4419
|
+
for (let i = 0; i < candidates.length; i += GC_CHUNK) {
|
|
4420
|
+
const chunk = yield fx.state.read((s) => {
|
|
4421
|
+
if (!s.primed || s.bucketId !== bucket) return null;
|
|
4422
|
+
const keep = retained(s);
|
|
4423
|
+
return candidates.slice(i, i + GC_CHUNK).filter((id) => s.versions.has(id) && !keep(id));
|
|
4424
|
+
});
|
|
4425
|
+
if (chunk === null) break;
|
|
4426
|
+
const settled = yield fx.all(chunk.map((id) => fx.local.delete(tableOf$1(id), id)));
|
|
4427
|
+
const done = chunk.filter((_, j) => settled[j].ok);
|
|
4428
|
+
if (done.length === 0) continue;
|
|
4429
|
+
const records = done.map((id) => ({
|
|
4430
|
+
table: tableOf$1(id),
|
|
4431
|
+
op: "DELETE",
|
|
4432
|
+
id,
|
|
4433
|
+
record: {}
|
|
4434
|
+
}));
|
|
4435
|
+
yield fx.state.update(deleteVersions(done));
|
|
4436
|
+
yield fx.ssp.ingest(records);
|
|
4437
|
+
if (relay) yield fx.emit({
|
|
4438
|
+
type: "tabs:broadcast",
|
|
4439
|
+
message: {
|
|
4440
|
+
type: "ingest",
|
|
4441
|
+
records
|
|
4442
|
+
}
|
|
4443
|
+
});
|
|
4444
|
+
removed += done.length;
|
|
4258
4445
|
}
|
|
4259
|
-
yield fx.
|
|
4446
|
+
if (removed > 0) yield fx.dispatch({ type: "FetchRows" });
|
|
4447
|
+
return removed;
|
|
4260
4448
|
}
|
|
4261
4449
|
|
|
4262
4450
|
//#endregion
|
|
@@ -5215,6 +5403,7 @@ function* boot(env, opts) {
|
|
|
5215
5403
|
yield fx.state.update(setIdentity({ bucketId: bucket }));
|
|
5216
5404
|
if (yield fx.service("local.usesSurqlSchema")) yield fx.service("migrator.provision");
|
|
5217
5405
|
yield* migrateWindowToView();
|
|
5406
|
+
yield* loadViews();
|
|
5218
5407
|
yield fx.dispatch({ type: "WarmBlobs" });
|
|
5219
5408
|
yield fx.service("ssp.init");
|
|
5220
5409
|
yield fx.service("ssp.setPermissions");
|
|
@@ -5234,7 +5423,7 @@ function* boot(env, opts) {
|
|
|
5234
5423
|
yield fx.service("features.init");
|
|
5235
5424
|
yield fx.service("releases.init");
|
|
5236
5425
|
yield fx.dispatch({ type: "LifecycleTick" });
|
|
5237
|
-
yield fx.
|
|
5426
|
+
yield fx.timer.set("gc", GC_BOOT_DELAY_MS, { type: "GcTick" });
|
|
5238
5427
|
yield fx.state.update(setIdentity({ localReady: true }));
|
|
5239
5428
|
yield fx.dispatch({ type: "StartRemote" });
|
|
5240
5429
|
}
|
|
@@ -5736,6 +5925,17 @@ function rowsFromResult(effect, result) {
|
|
|
5736
5925
|
const first = Array.isArray(result) ? result[0] : void 0;
|
|
5737
5926
|
return Array.isArray(first) ? first : [];
|
|
5738
5927
|
}
|
|
5928
|
+
/**
|
|
5929
|
+
* A row's encoded record id: a string on the SQLite engine, a `RecordId` on
|
|
5930
|
+
* the SurrealDB one. `null` for a row that carries none (a projection
|
|
5931
|
+
* without `id`, a `SELECT VALUE`).
|
|
5932
|
+
*/
|
|
5933
|
+
function rowKey(row) {
|
|
5934
|
+
const id = row !== null && typeof row === "object" ? row.id : void 0;
|
|
5935
|
+
if (typeof id === "string") return id;
|
|
5936
|
+
if (id !== null && typeof id === "object" && "table" in id && "id" in id) return encodeRecordId(id);
|
|
5937
|
+
return null;
|
|
5938
|
+
}
|
|
5739
5939
|
/** Cheap structural equality for the "did the rows change" check. */
|
|
5740
5940
|
function rowsEqual(a, b) {
|
|
5741
5941
|
if (a === b) return true;
|
|
@@ -5757,6 +5957,16 @@ function resolveMembership(input) {
|
|
|
5757
5957
|
return null;
|
|
5758
5958
|
}
|
|
5759
5959
|
/**
|
|
5960
|
+
* Whether a body may paint before the server has answered (`cold`). One no
|
|
5961
|
+
* durable view vouches for and no optimistic write touches is an orphan:
|
|
5962
|
+
* deleted upstream while no view watched it, or dropped from every view since.
|
|
5963
|
+
* The predicate scan would otherwise resurrect it until the server answers,
|
|
5964
|
+
* and indefinitely offline.
|
|
5965
|
+
*/
|
|
5966
|
+
function paintsCold(id, vouched, overlay) {
|
|
5967
|
+
return id.startsWith("_00_") || vouched.has(id) || overlay.writes.has(id);
|
|
5968
|
+
}
|
|
5969
|
+
/**
|
|
5760
5970
|
* render = (membership ∪ (writes ∩ localView)) − deletes
|
|
5761
5971
|
*
|
|
5762
5972
|
* `writes`/`deletes` are the outbox overlay (pending and acked items). The
|
|
@@ -5784,21 +5994,32 @@ function buildRenderIds(membership, localView, overlay, opts) {
|
|
|
5784
5994
|
|
|
5785
5995
|
//#endregion
|
|
5786
5996
|
//#region src/query/materialize.saga.ts
|
|
5997
|
+
const NONE = /* @__PURE__ */ new Set();
|
|
5787
5998
|
/**
|
|
5788
5999
|
* Render one query from state: membership (or a cold scan), the outbox
|
|
5789
6000
|
* overlay, one local read, then notify subscribers if the rows changed. The
|
|
5790
6001
|
* only writer of `records`. Runs on the `mat:<hash>` dedupe lane whenever the
|
|
5791
|
-
* query is dirty.
|
|
6002
|
+
* query is dirty. Before the server has answered (`cold`) only bodies some
|
|
6003
|
+
* durable view vouches for paint (see `paintsCold`).
|
|
5792
6004
|
*/
|
|
5793
6005
|
function* materialize(hash) {
|
|
5794
|
-
const [entry, ov] = yield fx.state.read((s) =>
|
|
6006
|
+
const [entry, ov, known] = yield fx.state.read((s) => {
|
|
6007
|
+
const e = s.queries.get(hash);
|
|
6008
|
+
return [
|
|
6009
|
+
e,
|
|
6010
|
+
overlay(s),
|
|
6011
|
+
e?.lifecycle.phase === "cold" ? vouched(s) : NONE
|
|
6012
|
+
];
|
|
6013
|
+
});
|
|
5795
6014
|
if (!entry) return;
|
|
5796
6015
|
const t0 = yield fx.now();
|
|
5797
6016
|
const isWindow = isWindowed(entry.def.surql);
|
|
6017
|
+
const paints = (id) => id === null || paintsCold(id, known, ov);
|
|
6018
|
+
const coldWindow = entry.lifecycle.phase === "cold" && isWindow;
|
|
5798
6019
|
const membership = resolveMembership({
|
|
5799
6020
|
phase: entry.lifecycle.phase,
|
|
5800
6021
|
remoteArray: entry.remoteArray,
|
|
5801
|
-
localArray: entry.localArray,
|
|
6022
|
+
localArray: coldWindow ? entry.localArray.filter(([id]) => paints(id)) : entry.localArray,
|
|
5802
6023
|
isWindow
|
|
5803
6024
|
});
|
|
5804
6025
|
const ids = membership ? buildRenderIds(membership, entry.localArray, ov, {
|
|
@@ -5809,6 +6030,7 @@ function* materialize(hash) {
|
|
|
5809
6030
|
let rows;
|
|
5810
6031
|
try {
|
|
5811
6032
|
rows = rowsFromResult(effect, yield effect);
|
|
6033
|
+
if (ids === null) rows = rows.filter((row) => paints(rowKey(row)));
|
|
5812
6034
|
} catch (error) {
|
|
5813
6035
|
yield fx.state.update(compose(recordError(hash), clearDirty(hash)));
|
|
5814
6036
|
yield fx.emit({
|
|
@@ -6118,6 +6340,7 @@ function* tabMessage(env, raw) {
|
|
|
6118
6340
|
});
|
|
6119
6341
|
}
|
|
6120
6342
|
yield fx.state.update(compose(setVersions(msg.records.filter((r) => r.op !== "DELETE").map((r) => [r.id, typeof r.record?._00_rv === "number" ? r.record._00_rv : 0])), deleteVersions(msg.records.filter((r) => r.op === "DELETE").map((r) => r.id)), ...[...new Set(msg.records.map((r) => r.table))].map((t) => markTableDirty(t))));
|
|
6343
|
+
if (msg.records.some((r) => r.op === "DELETE")) yield fx.dispatch({ type: "FetchRows" });
|
|
6121
6344
|
return;
|
|
6122
6345
|
case "membership-dirty":
|
|
6123
6346
|
if (Array.isArray(msg.hashes)) yield* liveChange(env, msg.hashes);
|
|
@@ -6236,6 +6459,7 @@ function* bucketSwitch(env, target, release = null, opts = { clearBlobsOnSignOut
|
|
|
6236
6459
|
data: { error }
|
|
6237
6460
|
});
|
|
6238
6461
|
}
|
|
6462
|
+
yield* loadViews();
|
|
6239
6463
|
yield* rebindQueries();
|
|
6240
6464
|
yield* loadOutbox(env);
|
|
6241
6465
|
yield fx.dispatch({ type: "EnsureRegistered" });
|
|
@@ -12283,6 +12507,8 @@ var TabsCoordinator = class {
|
|
|
12283
12507
|
this.enqueue(async () => {
|
|
12284
12508
|
if (this.role === "leader") await this.teardownLeader();
|
|
12285
12509
|
else if (this.role === "follower") this.teardownFollower("bucket switch");
|
|
12510
|
+
this.setRole("solo");
|
|
12511
|
+
this.leaderTabId = null;
|
|
12286
12512
|
this.bucketId = bucketId;
|
|
12287
12513
|
const timeout = setTimeout(() => {
|
|
12288
12514
|
this.startResolve = null;
|
|
@@ -13787,7 +14013,7 @@ function createTabsCoordinator(config, s, host) {
|
|
|
13787
14013
|
return new TabsCoordinator({
|
|
13788
14014
|
tabId: s.tabId,
|
|
13789
14015
|
fingerprint: computeTabsFingerprint({
|
|
13790
|
-
coreVersion: "0.0.1-canary.
|
|
14016
|
+
coreVersion: "0.0.1-canary.288",
|
|
13791
14017
|
schemaHash: hash53(config.schemaSurql),
|
|
13792
14018
|
endpoint: config.database.endpoint ?? "",
|
|
13793
14019
|
namespace: config.database.namespace,
|
package/dist/live.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as registerSelect, C as heartbeatRowGone, M as singleSnapshotSelect, R as sha256Hex, S as heartbeatBatch, a as metaFromRow, b as bodySelect, c as parseDuration, d as parseQueryParams, f as ANON_USER_ID, h as listRefTableFor, j as registerVars, l as parseRecordIdString, n as rowOfEdge, p as DEFAULT_REF_MODE, s as encodeRecordId, t as hashOfEdge, u as withTimeout, z as queryHashInput } from "./worker-edges.js";
|
|
2
2
|
import { n as decodeTokenClaims } from "./worker-impersonation.js";
|
|
3
3
|
import { RecordId, Surreal } from "surrealdb";
|
|
4
4
|
|
package/dist/pure.d.ts
CHANGED
|
@@ -181,7 +181,14 @@ type Vars = Record<string, unknown>;
|
|
|
181
181
|
declare const VIEW_TABLE = "_00_view";
|
|
182
182
|
declare const LEGACY_VIEW_TABLE = "_00_window";
|
|
183
183
|
declare const viewRecordId: (viewKey: string) => RecordId<string>;
|
|
184
|
-
|
|
184
|
+
/**
|
|
185
|
+
* The durable `_00_view` row: the confirmed membership plus the subquery
|
|
186
|
+
* children, which the GC must keep and a cold scan may paint as much as the
|
|
187
|
+
* members.
|
|
188
|
+
*/
|
|
189
|
+
declare const viewRow: (ids: RecordVersionArray, children: RecordVersionArray, confirmed: boolean, now: number) => Record<string, unknown>;
|
|
190
|
+
/** Every `_00_view` row: the durable view index and the GC read it. */
|
|
191
|
+
declare const readViewRows: () => string;
|
|
185
192
|
/** Copy every legacy `_00_window` row into `_00_view` once. */
|
|
186
193
|
declare const readLegacyViewRows: () => string;
|
|
187
194
|
declare const countViewRows: () => string;
|
|
@@ -319,4 +326,4 @@ declare function sha256Hex(input: string): Promise<string>;
|
|
|
319
326
|
*/
|
|
320
327
|
declare function queryKey(input: QueryKeyInput, sessionId: string | null): Promise<string>;
|
|
321
328
|
//#endregion
|
|
322
|
-
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, listRefBatchSelect, listRefSelect, listRefTableFor, metaFromRow, parseDuration, parseQueryParams, parseRecordIdString, queryHashInput, queryKey, queryRowCountBatchSelect, queryRowCountSelect, readLegacyViewRows, registerSelect, registerVars, rowOfEdge, sameBytes, sanitizeUserId, sha256Hex, singleSnapshotSelect, snapshotFromSingle, subqueryListRefSelect, surql, upsertBodiesTx, viewKeyInput, viewRecordId, viewRow };
|
|
329
|
+
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, 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 };
|
package/dist/pure.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as registerSelect, B as viewKeyInput, C as heartbeatRowGone, D as queryRowCountSelect, E as queryRowCountBatchSelect, F as viewRecordId, I as viewRow, L as surql, M as singleSnapshotSelect, N as subqueryListRefSelect, O as readLegacyViewRows, P as upsertBodiesTx, R as sha256Hex, S as heartbeatBatch, T as listRefSelect, _ as LEGACY_VIEW_TABLE, a as metaFromRow, b as bodySelect, c as parseDuration, d as parseQueryParams, f as ANON_USER_ID, g as sanitizeUserId, h as listRefTableFor, i as dedupeRecordVersions, j as registerVars, k as readViewRows, l as parseRecordIdString, m as bucketIdForUser, n as rowOfEdge, o as snapshotFromSingle, p as DEFAULT_REF_MODE, r as FAILED, s as encodeRecordId, t as hashOfEdge, v as VIEW_TABLE, w as listRefBatchSelect, x as countViewRows, y as batchSnapshotSelect, z as queryHashInput } 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, listRefBatchSelect, listRefSelect, listRefTableFor, metaFromRow, parseDuration, parseQueryParams, parseRecordIdString, queryHashInput, queryKey, queryRowCountBatchSelect, queryRowCountSelect, readLegacyViewRows, registerSelect, registerVars, rowOfEdge, sameBytes, sanitizeUserId, sha256Hex, singleSnapshotSelect, snapshotFromSingle, subqueryListRefSelect, surql, upsertBodiesTx, viewKeyInput, viewRecordId, viewRow };
|
|
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, 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 };
|
package/dist/worker-edges.js
CHANGED
|
@@ -103,11 +103,19 @@ const surql = {
|
|
|
103
103
|
const VIEW_TABLE = "_00_view";
|
|
104
104
|
const LEGACY_VIEW_TABLE = "_00_window";
|
|
105
105
|
const viewRecordId = (viewKey) => new RecordId(VIEW_TABLE, viewKey);
|
|
106
|
-
|
|
106
|
+
/**
|
|
107
|
+
* The durable `_00_view` row: the confirmed membership plus the subquery
|
|
108
|
+
* children, which the GC must keep and a cold scan may paint as much as the
|
|
109
|
+
* members.
|
|
110
|
+
*/
|
|
111
|
+
const viewRow = (ids, children, confirmed, now) => ({
|
|
107
112
|
ids,
|
|
113
|
+
children,
|
|
108
114
|
confirmed,
|
|
109
115
|
updatedAt: now
|
|
110
116
|
});
|
|
117
|
+
/** Every `_00_view` row: the durable view index and the GC read it. */
|
|
118
|
+
const readViewRows = () => `SELECT * FROM ${VIEW_TABLE}`;
|
|
111
119
|
/** Copy every legacy `_00_window` row into `_00_view` once. */
|
|
112
120
|
const readLegacyViewRows = () => `SELECT * FROM ${LEGACY_VIEW_TABLE}`;
|
|
113
121
|
const countViewRows = () => `SELECT count() FROM ${VIEW_TABLE} GROUP ALL`;
|
|
@@ -412,4 +420,4 @@ function rowOfEdge(value) {
|
|
|
412
420
|
}
|
|
413
421
|
|
|
414
422
|
//#endregion
|
|
415
|
-
export {
|
|
423
|
+
export { registerSelect as A, viewKeyInput as B, heartbeatRowGone as C, queryRowCountSelect as D, queryRowCountBatchSelect as E, viewRecordId as F, viewRow as I, surql as L, singleSnapshotSelect as M, subqueryListRefSelect as N, readLegacyViewRows as O, upsertBodiesTx as P, sha256Hex as R, heartbeatBatch as S, listRefSelect as T, LEGACY_VIEW_TABLE as _, metaFromRow as a, bodySelect as b, parseDuration as c, parseQueryParams as d, ANON_USER_ID as f, sanitizeUserId as g, listRefTableFor as h, dedupeRecordVersions as i, registerVars as j, readViewRows as k, parseRecordIdString as l, bucketIdForUser as m, rowOfEdge as n, snapshotFromSingle as o, DEFAULT_REF_MODE as p, FAILED as r, encodeRecordId as s, hashOfEdge as t, withTimeout as u, VIEW_TABLE as v, listRefBatchSelect as w, countViewRows as x, batchSnapshotSelect as y, queryHashInput as z };
|
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.288",
|
|
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.288",
|
|
79
|
+
"@spooky-sync/ssp-wasm": "0.0.1-canary.288",
|
|
80
80
|
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
|
|
81
81
|
"@surrealdb/wasm": "^3.0.3",
|
|
82
82
|
"blurhash": "^2.0.5",
|
|
@@ -19,7 +19,11 @@ describe('boot', () => {
|
|
|
19
19
|
'auth.access': () => 'account',
|
|
20
20
|
});
|
|
21
21
|
const out = await runPure(boot(env, { sharedTabs: false }), {
|
|
22
|
-
handlers: {
|
|
22
|
+
handlers: {
|
|
23
|
+
service: svc.handler,
|
|
24
|
+
'local.query': (e: any) =>
|
|
25
|
+
e.sql.startsWith('SELECT count()') ? [[{ count: 1 }]] : e.sql === 'SELECT * FROM _00_view' ? [[{ id: '_00_view:k', ids: [['t:1', 1]] }]] : [],
|
|
26
|
+
},
|
|
23
27
|
});
|
|
24
28
|
expect(svc.names()).toEqual([
|
|
25
29
|
'hint.read',
|
|
@@ -39,7 +43,10 @@ describe('boot', () => {
|
|
|
39
43
|
]);
|
|
40
44
|
expect(svc.calls[1]).toEqual(['local.connect', ['u1']]);
|
|
41
45
|
expect(out.state).toMatchObject({ bucketId: 'u1', userId: 'user:u1', saltUserId: 'user:u1', sessionId: 'salt-1', localReady: true });
|
|
42
|
-
expect(out.dispatched.map((d) => d.type)).toEqual(['WarmBlobs', 'PrimeCircuit', 'LifecycleTick', '
|
|
46
|
+
expect(out.dispatched.map((d) => d.type)).toEqual(['WarmBlobs', 'PrimeCircuit', 'LifecycleTick', 'StartRemote']);
|
|
47
|
+
// The view index is loaded before anything can paint; the first GC waits out the boot.
|
|
48
|
+
expect(out.state.views).toEqual(new Map([['k', ['t:1']]]));
|
|
49
|
+
expect(out.timers.get('gc')).toEqual({ ms: 30_000, event: { type: 'GcTick' } });
|
|
43
50
|
expect(svc.names()).not.toContain('remote.connect');
|
|
44
51
|
});
|
|
45
52
|
it('shared tabs: takes the role from the coordinator, or falls back to solo; schemaless engines skip provisioning; anonymous skips session auth', async () => {
|