js-bao 0.7.0-alpha.8 → 0.7.0-alpha.9
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/dist/browser.cjs +2252 -497
- package/dist/browser.d.cts +908 -137
- package/dist/browser.d.ts +908 -137
- package/dist/browser.js +2250 -496
- package/dist/client.cjs +138 -2
- package/dist/client.d.cts +715 -108
- package/dist/client.d.ts +715 -108
- package/dist/client.js +136 -1
- package/dist/cloudflare-do.cjs +243 -13
- package/dist/cloudflare-do.d.cts +750 -93
- package/dist/cloudflare-do.d.ts +750 -93
- package/dist/cloudflare-do.js +241 -12
- package/dist/cloudflare.cjs +146 -10
- package/dist/cloudflare.d.cts +715 -108
- package/dist/cloudflare.d.ts +715 -108
- package/dist/cloudflare.js +144 -9
- package/dist/codegen-v2.cjs +138 -2
- package/dist/format2-worker.js +838 -89
- package/dist/index.cjs +2264 -502
- package/dist/index.d.cts +908 -137
- package/dist/index.d.ts +908 -137
- package/dist/index.js +2254 -500
- package/dist/node.cjs +2259 -502
- package/dist/node.d.cts +849 -137
- package/dist/node.d.ts +849 -137
- package/dist/node.js +2251 -500
- package/package.json +1 -1
package/dist/browser.cjs
CHANGED
|
@@ -1314,6 +1314,14 @@ var init_DocumentQueryTranslator = __esm({
|
|
|
1314
1314
|
}
|
|
1315
1315
|
return Array.from(new Set(normalized));
|
|
1316
1316
|
}
|
|
1317
|
+
/**
|
|
1318
|
+
* The `_meta_doc_id` condition for a document scope, alone: `IN (...)`, or
|
|
1319
|
+
* `1 = 0` for an empty scope, or `null` when there is no scope. For a
|
|
1320
|
+
* statement the translator does not assemble itself (the aggregations).
|
|
1321
|
+
*/
|
|
1322
|
+
documentCondition(documents) {
|
|
1323
|
+
return this.buildDocumentClause(documents);
|
|
1324
|
+
}
|
|
1317
1325
|
buildDocumentClause(documents) {
|
|
1318
1326
|
const normalized = this.normalizeDocumentIds(documents);
|
|
1319
1327
|
if (normalized === null) {
|
|
@@ -2374,6 +2382,69 @@ var init_overlaySql = __esm({
|
|
|
2374
2382
|
}
|
|
2375
2383
|
});
|
|
2376
2384
|
|
|
2385
|
+
// src/models/format2FoldedState.ts
|
|
2386
|
+
function orderedClients(vector) {
|
|
2387
|
+
return Object.keys(vector).sort((a, b) => {
|
|
2388
|
+
const left = Number(a);
|
|
2389
|
+
const right = Number(b);
|
|
2390
|
+
if (left !== right) return left < right ? -1 : 1;
|
|
2391
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
function canonicalFoldedState(state) {
|
|
2395
|
+
const vector = {};
|
|
2396
|
+
for (const client of orderedClients(state.vector)) {
|
|
2397
|
+
const clock = Number(state.vector[client]);
|
|
2398
|
+
if (Number.isFinite(clock) && clock >= 0) vector[client] = Math.floor(clock);
|
|
2399
|
+
}
|
|
2400
|
+
const models = Array.from(new Set(state.models.map(String))).sort();
|
|
2401
|
+
return { vector, models };
|
|
2402
|
+
}
|
|
2403
|
+
function encodeFoldedState(state) {
|
|
2404
|
+
const canonical = canonicalFoldedState(state);
|
|
2405
|
+
const clients = orderedClients(canonical.vector).map(
|
|
2406
|
+
(client) => `${JSON.stringify(client)}:${canonical.vector[client]}`
|
|
2407
|
+
);
|
|
2408
|
+
return `{"vector":{${clients.join(",")}},"models":${JSON.stringify(canonical.models)}}`;
|
|
2409
|
+
}
|
|
2410
|
+
function decodeFoldedState(raw) {
|
|
2411
|
+
if (raw === null || raw === void 0) return null;
|
|
2412
|
+
try {
|
|
2413
|
+
const parsed = JSON.parse(String(raw));
|
|
2414
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.vector !== "object" || parsed.vector === null || !Array.isArray(parsed.models)) {
|
|
2415
|
+
return null;
|
|
2416
|
+
}
|
|
2417
|
+
return canonicalFoldedState({
|
|
2418
|
+
vector: parsed.vector,
|
|
2419
|
+
models: parsed.models
|
|
2420
|
+
});
|
|
2421
|
+
} catch {
|
|
2422
|
+
return null;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
function mergeFoldedStates(held, offered) {
|
|
2426
|
+
if (held === null) return canonicalFoldedState(offered);
|
|
2427
|
+
const vector = { ...held.vector };
|
|
2428
|
+
for (const [client, clock] of Object.entries(offered.vector)) {
|
|
2429
|
+
vector[client] = Math.max(vector[client] ?? 0, Number(clock));
|
|
2430
|
+
}
|
|
2431
|
+
return canonicalFoldedState({
|
|
2432
|
+
vector,
|
|
2433
|
+
models: [...held.models, ...offered.models]
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
function vectorIsAtOrAhead(docState, stored) {
|
|
2437
|
+
for (const [client, clock] of Object.entries(stored)) {
|
|
2438
|
+
if ((docState[client] ?? 0) < clock) return false;
|
|
2439
|
+
}
|
|
2440
|
+
return true;
|
|
2441
|
+
}
|
|
2442
|
+
var init_format2FoldedState = __esm({
|
|
2443
|
+
"src/models/format2FoldedState.ts"() {
|
|
2444
|
+
"use strict";
|
|
2445
|
+
}
|
|
2446
|
+
});
|
|
2447
|
+
|
|
2377
2448
|
// src/models/format2RecordStore.ts
|
|
2378
2449
|
function format2TableNames(docId) {
|
|
2379
2450
|
let hex = "";
|
|
@@ -2386,6 +2457,9 @@ function format2TableNames(docId) {
|
|
|
2386
2457
|
};
|
|
2387
2458
|
}
|
|
2388
2459
|
function format2ClientDDL(docId) {
|
|
2460
|
+
return [...format2DocumentDDL(docId), ...FORMAT2_SHARED_DDL];
|
|
2461
|
+
}
|
|
2462
|
+
function format2DocumentDDL(docId) {
|
|
2389
2463
|
const { records, stringSetIndex } = format2TableNames(docId);
|
|
2390
2464
|
return [
|
|
2391
2465
|
`CREATE TABLE IF NOT EXISTS ${records} (
|
|
@@ -2409,68 +2483,7 @@ function format2ClientDDL(docId) {
|
|
|
2409
2483
|
// `(_type, field, _record_id)` shape left the planner narrowing on `_type`
|
|
2410
2484
|
// alone — a walk of the model's whole member table per record. The old
|
|
2411
2485
|
// name is dropped in `init()`, not here, so this list stays creation-only.
|
|
2412
|
-
`CREATE INDEX IF NOT EXISTS idx_${stringSetIndex}_trf ON ${stringSetIndex}(_type, _record_id, field)
|
|
2413
|
-
`CREATE TABLE IF NOT EXISTS _epoch (
|
|
2414
|
-
doc_id TEXT PRIMARY KEY,
|
|
2415
|
-
epoch INTEGER NOT NULL DEFAULT 0,
|
|
2416
|
-
acked_seq INTEGER NOT NULL DEFAULT 0,
|
|
2417
|
-
last_sync_at INTEGER,
|
|
2418
|
-
window_days INTEGER,
|
|
2419
|
-
clock_offset INTEGER,
|
|
2420
|
-
hydrated_models TEXT
|
|
2421
|
-
)`,
|
|
2422
|
-
PENDING_OPS_DDL("_pending_ops"),
|
|
2423
|
-
`CREATE TABLE IF NOT EXISTS _client_acks (
|
|
2424
|
-
doc_id TEXT NOT NULL,
|
|
2425
|
-
client_id TEXT NOT NULL,
|
|
2426
|
-
acked_seq INTEGER NOT NULL DEFAULT 0,
|
|
2427
|
-
PRIMARY KEY (doc_id, client_id)
|
|
2428
|
-
)`,
|
|
2429
|
-
// #3432 — keyed on the manifest ORDINAL. `{model}/{n}` stopped being a
|
|
2430
|
-
// chunk's identity within its manifest the moment a manifest could reuse
|
|
2431
|
-
// a chunk from an earlier build: two entries can then share that pair, and
|
|
2432
|
-
// a resume keyed on it would treat the second as already loaded.
|
|
2433
|
-
`CREATE TABLE IF NOT EXISTS _snapshot_load (
|
|
2434
|
-
doc_id TEXT NOT NULL,
|
|
2435
|
-
build_id TEXT NOT NULL,
|
|
2436
|
-
ordinal INTEGER NOT NULL,
|
|
2437
|
-
PRIMARY KEY (doc_id, build_id, ordinal)
|
|
2438
|
-
)`,
|
|
2439
|
-
// #3435 — which base this merged view is on, and whether its load ever
|
|
2440
|
-
// FINISHED. The second half is what makes a partial replacement safe: a
|
|
2441
|
-
// convergence may keep a chunk by marking it rather than fetching it only
|
|
2442
|
-
// when the rows it is keeping are a complete baseline. A client that is
|
|
2443
|
-
// cold, or whose last load was interrupted, has no such guarantee and
|
|
2444
|
-
// takes the full plan (finding 3435-R04).
|
|
2445
|
-
`CREATE TABLE IF NOT EXISTS _snapshot_base (
|
|
2446
|
-
doc_id TEXT PRIMARY KEY,
|
|
2447
|
-
build_id TEXT NOT NULL,
|
|
2448
|
-
epoch INTEGER NOT NULL,
|
|
2449
|
-
complete INTEGER NOT NULL DEFAULT 0
|
|
2450
|
-
)`,
|
|
2451
|
-
// #3435 — every bulk-load epoch this document crossed and has not yet
|
|
2452
|
-
// converged past. Durable, because a connected client that followed a
|
|
2453
|
-
// flagged seal onto E+1 has an epoch mark that reads perfectly current:
|
|
2454
|
-
// if the debt lived only in memory, a restart would clear it and the
|
|
2455
|
-
// client would serve rows the server replaced, for good.
|
|
2456
|
-
`CREATE TABLE IF NOT EXISTS _discontinuities (
|
|
2457
|
-
doc_id TEXT NOT NULL,
|
|
2458
|
-
epoch INTEGER NOT NULL,
|
|
2459
|
-
PRIMARY KEY (doc_id, epoch)
|
|
2460
|
-
)`,
|
|
2461
|
-
// The judgement an epoch move DEFERRED and has not made yet. Durable,
|
|
2462
|
-
// written in the same commit as the epoch mark, because the mark moves
|
|
2463
|
-
// first: without this a restart in between reads as a client that is
|
|
2464
|
-
// perfectly current and whose unacknowledged writes are simply missing
|
|
2465
|
-
// from the overlay, so the bind puts them back by key presence alone and
|
|
2466
|
-
// publishes an offline write recency would have dropped.
|
|
2467
|
-
//
|
|
2468
|
-
// Swift's columns and meanings (`Format2RecordStore.swift`), keyed by
|
|
2469
|
-
// CLIENT as well as document: tabs share this database, sequence spaces
|
|
2470
|
-
// are per client, and a note keyed by document alone would be overwritten
|
|
2471
|
-
// by the second tab and read by a restarted one against sequences that
|
|
2472
|
-
// were never its own.
|
|
2473
|
-
DEFERRED_REPLAY_DDL
|
|
2486
|
+
`CREATE INDEX IF NOT EXISTS idx_${stringSetIndex}_trf ON ${stringSetIndex}(_type, _record_id, field)`
|
|
2474
2487
|
];
|
|
2475
2488
|
}
|
|
2476
2489
|
function toDeferredReplayNote(rows) {
|
|
@@ -2496,6 +2509,24 @@ function format2DocumentIdFromTableName(table) {
|
|
|
2496
2509
|
}
|
|
2497
2510
|
return docId;
|
|
2498
2511
|
}
|
|
2512
|
+
function legacyAdoptedPools(sql, tables) {
|
|
2513
|
+
if (!tables.includes("_legacy_adopted")) return [];
|
|
2514
|
+
return Array.from(
|
|
2515
|
+
sql.exec("SELECT DISTINCT pool FROM _legacy_adopted")
|
|
2516
|
+
).map((row) => String(row.pool));
|
|
2517
|
+
}
|
|
2518
|
+
function legacyDeliveredAck(sql, docId, clientId, marked) {
|
|
2519
|
+
if (!listTables(sql).includes("_legacy_handover")) return marked;
|
|
2520
|
+
const handover = Array.from(
|
|
2521
|
+
sql.exec(
|
|
2522
|
+
"SELECT handed_seq, acked_seq FROM _legacy_handover WHERE doc_id = ? AND client_id = ?",
|
|
2523
|
+
docId,
|
|
2524
|
+
clientId
|
|
2525
|
+
)
|
|
2526
|
+
)[0];
|
|
2527
|
+
if (!handover) return marked;
|
|
2528
|
+
return marked <= Number(handover.handed_seq) ? Number(handover.acked_seq) : marked;
|
|
2529
|
+
}
|
|
2499
2530
|
function listTables(sql) {
|
|
2500
2531
|
return Array.from(
|
|
2501
2532
|
sql.exec("SELECT name FROM sqlite_master WHERE type = 'table'")
|
|
@@ -2559,13 +2590,14 @@ function normalizeBinding(value) {
|
|
|
2559
2590
|
if (typeof value === "number") return value;
|
|
2560
2591
|
return String(value);
|
|
2561
2592
|
}
|
|
2562
|
-
var FORMAT2_LEGACY_CLIENT_ID, PENDING_OPS_DDL, DEFERRED_REPLAY_DDL, Format2ModelNotHydratedError, Format2RecordStore;
|
|
2593
|
+
var FORMAT2_LEGACY_CLIENT_ID, PENDING_OPS_DDL, DEFERRED_REPLAY_DDL, FORMAT2_SHARED_DDL, LEGACY_HANDOVER_DDL, Format2ModelNotHydratedError, Format2RecordStore;
|
|
2563
2594
|
var init_format2RecordStore = __esm({
|
|
2564
2595
|
"src/models/format2RecordStore.ts"() {
|
|
2565
2596
|
"use strict";
|
|
2566
2597
|
init_overlaySql();
|
|
2567
2598
|
init_largeDocuments();
|
|
2568
2599
|
init_format2OfflineWindow();
|
|
2600
|
+
init_format2FoldedState();
|
|
2569
2601
|
FORMAT2_LEGACY_CLIENT_ID = "legacy";
|
|
2570
2602
|
PENDING_OPS_DDL = (table) => `CREATE TABLE IF NOT EXISTS ${table} (
|
|
2571
2603
|
doc_id TEXT NOT NULL,
|
|
@@ -2591,6 +2623,94 @@ var init_format2RecordStore = __esm({
|
|
|
2591
2623
|
to_epoch INTEGER NOT NULL,
|
|
2592
2624
|
kind TEXT NOT NULL,
|
|
2593
2625
|
PRIMARY KEY (doc_id, client_id)
|
|
2626
|
+
)`;
|
|
2627
|
+
FORMAT2_SHARED_DDL = [
|
|
2628
|
+
`CREATE TABLE IF NOT EXISTS _epoch (
|
|
2629
|
+
doc_id TEXT PRIMARY KEY,
|
|
2630
|
+
epoch INTEGER NOT NULL DEFAULT 0,
|
|
2631
|
+
acked_seq INTEGER NOT NULL DEFAULT 0,
|
|
2632
|
+
last_sync_at INTEGER,
|
|
2633
|
+
window_days INTEGER,
|
|
2634
|
+
clock_offset INTEGER,
|
|
2635
|
+
hydrated_models TEXT,
|
|
2636
|
+
folded_state TEXT
|
|
2637
|
+
)`,
|
|
2638
|
+
PENDING_OPS_DDL("_pending_ops"),
|
|
2639
|
+
`CREATE TABLE IF NOT EXISTS _client_acks (
|
|
2640
|
+
doc_id TEXT NOT NULL,
|
|
2641
|
+
client_id TEXT NOT NULL,
|
|
2642
|
+
acked_seq INTEGER NOT NULL DEFAULT 0,
|
|
2643
|
+
PRIMARY KEY (doc_id, client_id)
|
|
2644
|
+
)`,
|
|
2645
|
+
// #3432 — keyed on the manifest ORDINAL. `{model}/{n}` stopped being a
|
|
2646
|
+
// chunk's identity within its manifest the moment a manifest could reuse
|
|
2647
|
+
// a chunk from an earlier build: two entries can then share that pair, and
|
|
2648
|
+
// a resume keyed on it would treat the second as already loaded.
|
|
2649
|
+
`CREATE TABLE IF NOT EXISTS _snapshot_load (
|
|
2650
|
+
doc_id TEXT NOT NULL,
|
|
2651
|
+
build_id TEXT NOT NULL,
|
|
2652
|
+
ordinal INTEGER NOT NULL,
|
|
2653
|
+
PRIMARY KEY (doc_id, build_id, ordinal)
|
|
2654
|
+
)`,
|
|
2655
|
+
// #3435 — which base this merged view is on, and whether its load ever
|
|
2656
|
+
// FINISHED. The second half is what makes a partial replacement safe: a
|
|
2657
|
+
// convergence may keep a chunk by marking it rather than fetching it only
|
|
2658
|
+
// when the rows it is keeping are a complete baseline. A client that is
|
|
2659
|
+
// cold, or whose last load was interrupted, has no such guarantee and
|
|
2660
|
+
// takes the full plan (finding 3435-R04).
|
|
2661
|
+
`CREATE TABLE IF NOT EXISTS _snapshot_base (
|
|
2662
|
+
doc_id TEXT PRIMARY KEY,
|
|
2663
|
+
build_id TEXT NOT NULL,
|
|
2664
|
+
epoch INTEGER NOT NULL,
|
|
2665
|
+
complete INTEGER NOT NULL DEFAULT 0
|
|
2666
|
+
)`,
|
|
2667
|
+
// #3435 — every bulk-load epoch this document crossed and has not yet
|
|
2668
|
+
// converged past. Durable, because a connected client that followed a
|
|
2669
|
+
// flagged seal onto E+1 has an epoch mark that reads perfectly current:
|
|
2670
|
+
// if the debt lived only in memory, a restart would clear it and the
|
|
2671
|
+
// client would serve rows the server replaced, for good.
|
|
2672
|
+
`CREATE TABLE IF NOT EXISTS _discontinuities (
|
|
2673
|
+
doc_id TEXT NOT NULL,
|
|
2674
|
+
epoch INTEGER NOT NULL,
|
|
2675
|
+
PRIMARY KEY (doc_id, epoch)
|
|
2676
|
+
)`,
|
|
2677
|
+
// The judgement an epoch move DEFERRED and has not made yet. Durable,
|
|
2678
|
+
// written in the same commit as the epoch mark, because the mark moves
|
|
2679
|
+
// first: without this a restart in between reads as a client that is
|
|
2680
|
+
// perfectly current and whose unacknowledged writes are simply missing
|
|
2681
|
+
// from the overlay, so the bind puts them back by key presence alone and
|
|
2682
|
+
// publishes an offline write recency would have dropped.
|
|
2683
|
+
//
|
|
2684
|
+
// Swift's columns and meanings (`Format2RecordStore.swift`), keyed by
|
|
2685
|
+
// CLIENT as well as document: tabs share this database, sequence spaces
|
|
2686
|
+
// are per client, and a note keyed by document alone would be overwritten
|
|
2687
|
+
// by the second tab and read by a restarted one against sequences that
|
|
2688
|
+
// were never its own.
|
|
2689
|
+
DEFERRED_REPLAY_DDL,
|
|
2690
|
+
// #3760 — which of a PRE-AMENDMENT pool's unacknowledged writes this store
|
|
2691
|
+
// has already taken over, so none is ever taken twice.
|
|
2692
|
+
//
|
|
2693
|
+
// Keyed by POOL and not by document (finding 3760-R3). A `doc_id`-keyed
|
|
2694
|
+
// ledger would be swept by that document's purge, and the sequence this
|
|
2695
|
+
// run is about is exactly: import a write, crash before the legacy copy is
|
|
2696
|
+
// dropped, the user discards the document, reopen — at which point a
|
|
2697
|
+
// document-keyed ledger is gone, the legacy copy is still there, and a
|
|
2698
|
+
// write the user explicitly discarded comes back. The pool name outlives
|
|
2699
|
+
// every document of it, so this does too, and only the scope store's own
|
|
2700
|
+
// wipe takes it.
|
|
2701
|
+
`CREATE TABLE IF NOT EXISTS _legacy_adopted (
|
|
2702
|
+
pool TEXT NOT NULL,
|
|
2703
|
+
client_id TEXT NOT NULL,
|
|
2704
|
+
seq INTEGER NOT NULL,
|
|
2705
|
+
PRIMARY KEY (pool, client_id, seq)
|
|
2706
|
+
)`
|
|
2707
|
+
];
|
|
2708
|
+
LEGACY_HANDOVER_DDL = `CREATE TABLE IF NOT EXISTS _legacy_handover (
|
|
2709
|
+
doc_id TEXT NOT NULL,
|
|
2710
|
+
client_id TEXT NOT NULL,
|
|
2711
|
+
handed_seq INTEGER NOT NULL,
|
|
2712
|
+
acked_seq INTEGER NOT NULL,
|
|
2713
|
+
PRIMARY KEY (doc_id, client_id)
|
|
2594
2714
|
)`;
|
|
2595
2715
|
Format2ModelNotHydratedError = class extends Error {
|
|
2596
2716
|
constructor(model, hydrated) {
|
|
@@ -2671,6 +2791,9 @@ var init_format2RecordStore = __esm({
|
|
|
2671
2791
|
if (columns.length > 0 && !columns.includes("hydrated_models")) {
|
|
2672
2792
|
this.sql.exec("ALTER TABLE _epoch ADD COLUMN hydrated_models TEXT");
|
|
2673
2793
|
}
|
|
2794
|
+
if (columns.length > 0 && !columns.includes("folded_state")) {
|
|
2795
|
+
this.sql.exec("ALTER TABLE _epoch ADD COLUMN folded_state TEXT");
|
|
2796
|
+
}
|
|
2674
2797
|
const pendingColumns = Array.from(
|
|
2675
2798
|
this.sql.exec("PRAGMA table_info(_pending_ops)")
|
|
2676
2799
|
).map((row) => String(row.name));
|
|
@@ -2746,11 +2869,18 @@ var init_format2RecordStore = __esm({
|
|
|
2746
2869
|
*/
|
|
2747
2870
|
setHydrationScope(models) {
|
|
2748
2871
|
const scope = models === null ? null : [...models];
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2872
|
+
const encoded = scope === null ? null : JSON.stringify(scope);
|
|
2873
|
+
const current = this.hydrationScope();
|
|
2874
|
+
this.sql.transaction(() => {
|
|
2875
|
+
if (encoded !== (current === null ? null : JSON.stringify(current))) {
|
|
2876
|
+
this.clearFoldedState();
|
|
2877
|
+
}
|
|
2878
|
+
this.sql.exec(
|
|
2879
|
+
"UPDATE _epoch SET hydrated_models = ? WHERE doc_id = ?",
|
|
2880
|
+
encoded,
|
|
2881
|
+
this.docId
|
|
2882
|
+
);
|
|
2883
|
+
});
|
|
2754
2884
|
this.scope = scope;
|
|
2755
2885
|
this.scopeLoaded = true;
|
|
2756
2886
|
}
|
|
@@ -3225,6 +3355,7 @@ var init_format2RecordStore = __esm({
|
|
|
3225
3355
|
*/
|
|
3226
3356
|
setEpoch(epoch, options = {}) {
|
|
3227
3357
|
this.sql.transaction(() => {
|
|
3358
|
+
if (epoch !== this.epoch()) this.clearFoldedState();
|
|
3228
3359
|
this.sql.exec(
|
|
3229
3360
|
"UPDATE _epoch SET epoch = ? WHERE doc_id = ?",
|
|
3230
3361
|
epoch,
|
|
@@ -3295,6 +3426,59 @@ var init_format2RecordStore = __esm({
|
|
|
3295
3426
|
this.client()
|
|
3296
3427
|
);
|
|
3297
3428
|
}
|
|
3429
|
+
// ── What the store last folded (#3782) ───────────────────────────
|
|
3430
|
+
/**
|
|
3431
|
+
* The overlay state the merged view was last folded from, or `null` when
|
|
3432
|
+
* nothing is known — a store written before this, or one whose rows were
|
|
3433
|
+
* just replaced from another source.
|
|
3434
|
+
*
|
|
3435
|
+
* What it vouches for: for every model in `models`, every overlay item of
|
|
3436
|
+
* that model's map below `vector` has been folded into `records` AND either
|
|
3437
|
+
* projected into the query tables or had its model's projection mark
|
|
3438
|
+
* withdrawn.
|
|
3439
|
+
*/
|
|
3440
|
+
foldedState() {
|
|
3441
|
+
const rows = this.rows(
|
|
3442
|
+
"SELECT folded_state FROM _epoch WHERE doc_id = ?",
|
|
3443
|
+
this.docId
|
|
3444
|
+
);
|
|
3445
|
+
return rows.length > 0 ? decodeFoldedState(rows[0].folded_state) : null;
|
|
3446
|
+
}
|
|
3447
|
+
/**
|
|
3448
|
+
* Record that a fold certified `state`. Called INSIDE the transaction of the
|
|
3449
|
+
* fold it vouches for, never beside it.
|
|
3450
|
+
*
|
|
3451
|
+
* Merged per client by MAX, with the models unioned — but only while the
|
|
3452
|
+
* document that folded, at `docState`, is at or ahead of the stored vector
|
|
3453
|
+
* on every client. Otherwise it answers `behind` and writes nothing: a
|
|
3454
|
+
* lagging document folded older values under keys the stored vector
|
|
3455
|
+
* vouches for newer ones of (finding 3782-R02), and a MAX would go on
|
|
3456
|
+
* certifying the newer state over the older rows.
|
|
3457
|
+
*
|
|
3458
|
+
* A store that knows nothing (`NULL`) can only be stamped by a fold of the
|
|
3459
|
+
* WHOLE overlay: an incremental batch folded its own keys and says nothing
|
|
3460
|
+
* about the rest, so it answers `skipped` there.
|
|
3461
|
+
*/
|
|
3462
|
+
noteFoldedState(state, options) {
|
|
3463
|
+
const held = this.foldedState();
|
|
3464
|
+
if (held === null && options.whole !== true) return "skipped";
|
|
3465
|
+
if (held !== null && !vectorIsAtOrAhead(options.docState, held.vector)) {
|
|
3466
|
+
return "behind";
|
|
3467
|
+
}
|
|
3468
|
+
this.sql.exec(
|
|
3469
|
+
"UPDATE _epoch SET folded_state = ? WHERE doc_id = ?",
|
|
3470
|
+
encodeFoldedState(mergeFoldedStates(held, state)),
|
|
3471
|
+
this.docId
|
|
3472
|
+
);
|
|
3473
|
+
return "stamped";
|
|
3474
|
+
}
|
|
3475
|
+
/** Nothing is known about what the merged view was folded from. */
|
|
3476
|
+
clearFoldedState() {
|
|
3477
|
+
this.sql.exec(
|
|
3478
|
+
"UPDATE _epoch SET folded_state = NULL WHERE doc_id = ?",
|
|
3479
|
+
this.docId
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
3298
3482
|
lastSyncAt() {
|
|
3299
3483
|
const rows = this.rows(
|
|
3300
3484
|
"SELECT last_sync_at FROM _epoch WHERE doc_id = ?",
|
|
@@ -3477,6 +3661,7 @@ var init_format2RecordStore = __esm({
|
|
|
3477
3661
|
let applied = 0;
|
|
3478
3662
|
this.sql.transaction(() => {
|
|
3479
3663
|
if (this.chunkIsComplete(buildId, ordinal)) return;
|
|
3664
|
+
this.clearFoldedState();
|
|
3480
3665
|
for (const entry of entries) {
|
|
3481
3666
|
if (!this.isHydrated(modelName)) break;
|
|
3482
3667
|
projectOverlayEntry(this.sql, modelName, entry, this.tables);
|
|
@@ -3621,6 +3806,7 @@ var init_format2RecordStore = __esm({
|
|
|
3621
3806
|
if (!this.isHydrated(modelName)) {
|
|
3622
3807
|
return;
|
|
3623
3808
|
}
|
|
3809
|
+
this.clearFoldedState();
|
|
3624
3810
|
removed = this.deleteCoverage(modelName, coverage);
|
|
3625
3811
|
for (const entry of entries) {
|
|
3626
3812
|
projectOverlayEntry(this.sql, modelName, entry, this.tables);
|
|
@@ -3646,6 +3832,7 @@ var init_format2RecordStore = __esm({
|
|
|
3646
3832
|
let removed = 0;
|
|
3647
3833
|
this.sql.transaction(() => {
|
|
3648
3834
|
removed = this.deleteCoverage(modelName, { after, through }, limit);
|
|
3835
|
+
if (removed > 0) this.clearFoldedState();
|
|
3649
3836
|
});
|
|
3650
3837
|
return { removed, done: removed < limit };
|
|
3651
3838
|
}
|
|
@@ -3774,7 +3961,8 @@ var init_format2RecordStore = __esm({
|
|
|
3774
3961
|
sql.exec(`DROP TABLE IF EXISTS ${quote(own.stringSetIndex)}`);
|
|
3775
3962
|
return {
|
|
3776
3963
|
pendingOps,
|
|
3777
|
-
remainingDocuments: countStoredDocuments(sql).length
|
|
3964
|
+
remainingDocuments: countStoredDocuments(sql).length,
|
|
3965
|
+
legacyPools: legacyAdoptedPools(sql, tables)
|
|
3778
3966
|
};
|
|
3779
3967
|
});
|
|
3780
3968
|
}
|
|
@@ -3790,6 +3978,29 @@ var init_format2RecordStore = __esm({
|
|
|
3790
3978
|
static storedDocuments(sql) {
|
|
3791
3979
|
return countStoredDocuments(sql);
|
|
3792
3980
|
}
|
|
3981
|
+
/**
|
|
3982
|
+
* The pre-amendment pools this database's `_legacy_adopted` ledger names
|
|
3983
|
+
* (#3760): while one of them is still on disk, this database is the only
|
|
3984
|
+
* record of which of its writes have already been taken over, and must not
|
|
3985
|
+
* be removed (findings 3760-REVIEW-02 and 3760-REVIEW-09).
|
|
3986
|
+
*/
|
|
3987
|
+
static legacyAdoptedPools(sql) {
|
|
3988
|
+
return legacyAdoptedPools(sql, listTables(sql));
|
|
3989
|
+
}
|
|
3990
|
+
/**
|
|
3991
|
+
* Create the bookkeeping every document of this database shares, without
|
|
3992
|
+
* naming one (#3760).
|
|
3993
|
+
*
|
|
3994
|
+
* What a store hosting SEVERAL documents needs before it knows any of them:
|
|
3995
|
+
* the browser's worker opens one database per user scope and must answer
|
|
3996
|
+
* `storedEpoch` and `listDocuments` for it straight away, and a document's
|
|
3997
|
+
* own tables arrive later, on the first `serve` or probe that names it.
|
|
3998
|
+
* Exactly the statements {@link init} runs that are not per document, so
|
|
3999
|
+
* `init` on any document afterwards is a no-op over them.
|
|
4000
|
+
*/
|
|
4001
|
+
static ensureSharedSchema(sql) {
|
|
4002
|
+
for (const ddl of FORMAT2_SHARED_DDL) sql.exec(ddl);
|
|
4003
|
+
}
|
|
3793
4004
|
/**
|
|
3794
4005
|
* Purge every document this database holds except `keep` (#3754).
|
|
3795
4006
|
*
|
|
@@ -3841,9 +4052,314 @@ var init_format2RecordStore = __esm({
|
|
|
3841
4052
|
this.sql.exec(`DELETE FROM ${this.tables.records}`);
|
|
3842
4053
|
this.sql.exec(`DELETE FROM ${this.tables.stringSetIndex}`);
|
|
3843
4054
|
this.sql.exec("DELETE FROM _snapshot_load WHERE doc_id = ?", this.docId);
|
|
4055
|
+
this.clearFoldedState();
|
|
3844
4056
|
this.sql.exec("DELETE FROM _snapshot_base WHERE doc_id = ?", this.docId);
|
|
3845
4057
|
});
|
|
3846
4058
|
}
|
|
4059
|
+
// ── a pre-amendment pool's debt (#3760) ───────────────────────────
|
|
4060
|
+
/**
|
|
4061
|
+
* What a pre-amendment per-document pool still owes the server for `docId`.
|
|
4062
|
+
*
|
|
4063
|
+
* Read from the LEGACY database, by a transient worker on the legacy
|
|
4064
|
+
* namespace. Its schema is MIGRATED first, through the one path that knows
|
|
4065
|
+
* how (finding 3760-REVIEW-04): the transient worker's open runs only
|
|
4066
|
+
* {@link ensureSharedSchema}, which creates a missing table and leaves an
|
|
4067
|
+
* existing one exactly as it found it — so against a genuine 0.7.0 pool,
|
|
4068
|
+
* whose `_pending_ops` has no `client_id`, no `mutation` and no
|
|
4069
|
+
* `prior_overlay`, the read below failed on the column and the export
|
|
4070
|
+
* answered "this pool owes nothing". That is the one outcome the intent's
|
|
4071
|
+
* 0.7.0 rule does not permit, and it was silent. {@link init} is what turns
|
|
4072
|
+
* a pool of any 0.7.x or 0.9.x shape into one these columns exist on, and it
|
|
4073
|
+
* maps the 0.7.0 log to {@link FORMAT2_LEGACY_CLIENT_ID} with the acked mark
|
|
4074
|
+
* that measures it (edge E5), which is also what lets edge E14's rule reach
|
|
4075
|
+
* those rows. Writing to a pool this call is about to delete rows from
|
|
4076
|
+
* anyway costs nothing, and is what an 0.8.0 client would have done to it.
|
|
4077
|
+
*
|
|
4078
|
+
* A table that is genuinely ABSENT afterwards is a release that never wrote
|
|
4079
|
+
* it and owes nothing through it (edge E4). Any other read failure is raised:
|
|
4080
|
+
* the adoption logs it and retries at the next open, which is honest, where
|
|
4081
|
+
* reading it as "owes nothing" would silently strand the writes and then
|
|
4082
|
+
* drop nothing — the caller only deletes what it was told moved.
|
|
4083
|
+
*/
|
|
4084
|
+
static exportLegacyDocument(sql, docId) {
|
|
4085
|
+
new _Format2RecordStore(sql, docId).init();
|
|
4086
|
+
const tables = new Set(listTables(sql));
|
|
4087
|
+
const rowsOf = (table, query, ...bindings) => {
|
|
4088
|
+
if (!tables.has(table)) return [];
|
|
4089
|
+
return Array.from(sql.exec(query, ...bindings));
|
|
4090
|
+
};
|
|
4091
|
+
const epochRow = rowsOf(
|
|
4092
|
+
"_epoch",
|
|
4093
|
+
"SELECT epoch, acked_seq FROM _epoch WHERE doc_id = ?",
|
|
4094
|
+
docId
|
|
4095
|
+
)[0];
|
|
4096
|
+
return {
|
|
4097
|
+
// A pool with no `_epoch` row for the document was never bound; its
|
|
4098
|
+
// writes, if any, were made on no epoch this can name, and 0 is the
|
|
4099
|
+
// honest answer — the judgement then spans the whole chain.
|
|
4100
|
+
epoch: epochRow ? Number(epochRow.epoch) : 0,
|
|
4101
|
+
pendingOps: rowsOf(
|
|
4102
|
+
"_pending_ops",
|
|
4103
|
+
`SELECT client_id, seq, model, record_id, op, fields, base_epoch, ts,
|
|
4104
|
+
mutation, prior_overlay
|
|
4105
|
+
FROM _pending_ops WHERE doc_id = ? ORDER BY client_id, seq`,
|
|
4106
|
+
docId
|
|
4107
|
+
),
|
|
4108
|
+
// The mark a server DELIVERY set, not the floor an earlier drop raised
|
|
4109
|
+
// so the pre-amendment allocator could not reuse a handed-over sequence
|
|
4110
|
+
// (finding 3760-REVIEW-08). The importer reads these as "already
|
|
4111
|
+
// acknowledged", and an adopted row at or below that is pruned as sent.
|
|
4112
|
+
clientAcks: rowsOf(
|
|
4113
|
+
"_client_acks",
|
|
4114
|
+
"SELECT client_id, acked_seq FROM _client_acks WHERE doc_id = ?",
|
|
4115
|
+
docId
|
|
4116
|
+
).map((row) => ({
|
|
4117
|
+
client_id: String(row.client_id),
|
|
4118
|
+
acked_seq: legacyDeliveredAck(
|
|
4119
|
+
sql,
|
|
4120
|
+
docId,
|
|
4121
|
+
String(row.client_id),
|
|
4122
|
+
Number(row.acked_seq)
|
|
4123
|
+
)
|
|
4124
|
+
})),
|
|
4125
|
+
deferredReplay: rowsOf(
|
|
4126
|
+
"_deferred_replay",
|
|
4127
|
+
`SELECT client_id, through_seq, from_epoch, to_epoch, kind
|
|
4128
|
+
FROM _deferred_replay WHERE doc_id = ?`,
|
|
4129
|
+
docId
|
|
4130
|
+
),
|
|
4131
|
+
discontinuities: rowsOf(
|
|
4132
|
+
"_discontinuities",
|
|
4133
|
+
"SELECT epoch FROM _discontinuities WHERE doc_id = ? ORDER BY epoch",
|
|
4134
|
+
docId
|
|
4135
|
+
).map((row) => Number(row.epoch))
|
|
4136
|
+
};
|
|
4137
|
+
}
|
|
4138
|
+
/**
|
|
4139
|
+
* Take over `legacy`'s debt for `docId`, once, in ONE transaction (#3760).
|
|
4140
|
+
*
|
|
4141
|
+
* Every row goes in under its ORIGINAL client id. That is what makes the
|
|
4142
|
+
* bind's own adoption do the rest of the work: those clients are dead by
|
|
4143
|
+
* construction — their lock names are keyed on the SCOPE namespace and the
|
|
4144
|
+
* document, which no pre-amendment tab ever held — so
|
|
4145
|
+
* `adoptOrphanedPendingOps` re-sequences them into this tab's space and
|
|
4146
|
+
* classifies each against the merged row, exactly as it does for a sibling
|
|
4147
|
+
* tab that closed (#3431).
|
|
4148
|
+
*
|
|
4149
|
+
* `_legacy_adopted` is consulted first and written in the same transaction,
|
|
4150
|
+
* so a row already taken over is never taken again — including after a crash
|
|
4151
|
+
* that left the legacy copy in place, and including after the document has
|
|
4152
|
+
* been purged in between.
|
|
4153
|
+
*
|
|
4154
|
+
* Answers both halves the caller needs: the rows it took over just now, and
|
|
4155
|
+
* every row of the batch the ledger records — which is what may be deleted
|
|
4156
|
+
* from the legacy pool. They differ after a crash between an import and its
|
|
4157
|
+
* drop: the row is already adopted, so it is not imported again, and its
|
|
4158
|
+
* legacy copy still has to go. Dropping the whole document's log instead of
|
|
4159
|
+
* the named batch would take a row a legacy tab wrote since the export.
|
|
4160
|
+
*/
|
|
4161
|
+
static importLegacyDocument(sql, options) {
|
|
4162
|
+
const { docId, pool, legacyEpoch, rows } = options;
|
|
4163
|
+
const imported = [];
|
|
4164
|
+
const settled = [];
|
|
4165
|
+
const discontinuities = [];
|
|
4166
|
+
let note = false;
|
|
4167
|
+
sql.transaction(() => {
|
|
4168
|
+
const adopted = new Set(
|
|
4169
|
+
Array.from(
|
|
4170
|
+
sql.exec(
|
|
4171
|
+
"SELECT client_id, seq FROM _legacy_adopted WHERE pool = ?",
|
|
4172
|
+
pool
|
|
4173
|
+
)
|
|
4174
|
+
// NUL as the separator, and written as an escape: a client id is
|
|
4175
|
+
// whatever the app's build minted and nothing forbids a space in
|
|
4176
|
+
// one, so a printable separator could make two rows one key.
|
|
4177
|
+
).map((row) => `${row.client_id}\0${row.seq}`)
|
|
4178
|
+
);
|
|
4179
|
+
const throughSeq = /* @__PURE__ */ new Map();
|
|
4180
|
+
const fromEpoch = /* @__PURE__ */ new Map();
|
|
4181
|
+
const lowerTo = (clientId, epoch) => {
|
|
4182
|
+
if (!Number.isFinite(epoch) || epoch <= 0) return;
|
|
4183
|
+
const held = fromEpoch.get(clientId);
|
|
4184
|
+
fromEpoch.set(clientId, held === void 0 ? epoch : Math.min(held, epoch));
|
|
4185
|
+
};
|
|
4186
|
+
const legacyNotes = /* @__PURE__ */ new Map();
|
|
4187
|
+
for (const row of rows.deferredReplay) {
|
|
4188
|
+
const from = Number(row.from_epoch);
|
|
4189
|
+
if (Number.isFinite(from) && from > 0) {
|
|
4190
|
+
legacyNotes.set(String(row.client_id), from);
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
for (const row of rows.pendingOps) {
|
|
4194
|
+
const clientId = String(row.client_id);
|
|
4195
|
+
const seq = Number(row.seq);
|
|
4196
|
+
if (adopted.has(`${clientId}\0${seq}`)) {
|
|
4197
|
+
settled.push({ clientId, seq });
|
|
4198
|
+
continue;
|
|
4199
|
+
}
|
|
4200
|
+
sql.exec(
|
|
4201
|
+
`INSERT OR IGNORE INTO _pending_ops
|
|
4202
|
+
(doc_id, client_id, seq, model, record_id, op, fields, base_epoch, ts,
|
|
4203
|
+
mutation, prior_overlay)
|
|
4204
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
4205
|
+
docId,
|
|
4206
|
+
clientId,
|
|
4207
|
+
seq,
|
|
4208
|
+
String(row.model),
|
|
4209
|
+
String(row.record_id),
|
|
4210
|
+
String(row.op),
|
|
4211
|
+
String(row.fields),
|
|
4212
|
+
Number(row.base_epoch),
|
|
4213
|
+
Number(row.ts),
|
|
4214
|
+
row.mutation ?? null,
|
|
4215
|
+
row.prior_overlay ?? null
|
|
4216
|
+
);
|
|
4217
|
+
sql.exec(
|
|
4218
|
+
"INSERT OR IGNORE INTO _legacy_adopted (pool, client_id, seq) VALUES (?, ?, ?)",
|
|
4219
|
+
pool,
|
|
4220
|
+
clientId,
|
|
4221
|
+
seq
|
|
4222
|
+
);
|
|
4223
|
+
imported.push({ clientId, seq });
|
|
4224
|
+
settled.push({ clientId, seq });
|
|
4225
|
+
throughSeq.set(clientId, Math.max(throughSeq.get(clientId) ?? 0, seq));
|
|
4226
|
+
lowerTo(clientId, Number(row.base_epoch));
|
|
4227
|
+
const deferred = legacyNotes.get(clientId);
|
|
4228
|
+
if (deferred !== void 0) lowerTo(clientId, deferred);
|
|
4229
|
+
}
|
|
4230
|
+
for (const ack of rows.clientAcks) {
|
|
4231
|
+
sql.exec(
|
|
4232
|
+
`INSERT INTO _client_acks (doc_id, client_id, acked_seq)
|
|
4233
|
+
VALUES (?, ?, ?)
|
|
4234
|
+
ON CONFLICT(doc_id, client_id) DO UPDATE SET
|
|
4235
|
+
acked_seq = MAX(acked_seq, ?)`,
|
|
4236
|
+
docId,
|
|
4237
|
+
String(ack.client_id),
|
|
4238
|
+
Number(ack.acked_seq),
|
|
4239
|
+
Number(ack.acked_seq)
|
|
4240
|
+
);
|
|
4241
|
+
}
|
|
4242
|
+
for (const epoch of rows.discontinuities) {
|
|
4243
|
+
sql.exec(
|
|
4244
|
+
"INSERT OR IGNORE INTO _discontinuities (doc_id, epoch) VALUES (?, ?)",
|
|
4245
|
+
docId,
|
|
4246
|
+
epoch
|
|
4247
|
+
);
|
|
4248
|
+
discontinuities.push(epoch);
|
|
4249
|
+
}
|
|
4250
|
+
for (const [clientId, seq] of throughSeq) {
|
|
4251
|
+
const from = Math.min(fromEpoch.get(clientId) ?? legacyEpoch, legacyEpoch);
|
|
4252
|
+
sql.exec(
|
|
4253
|
+
`INSERT INTO _deferred_replay
|
|
4254
|
+
(doc_id, client_id, through_seq, from_epoch, to_epoch, kind)
|
|
4255
|
+
VALUES (?, ?, ?, ?, ?, 'legacy')
|
|
4256
|
+
ON CONFLICT(doc_id, client_id) DO UPDATE SET
|
|
4257
|
+
through_seq = MAX(through_seq, ?),
|
|
4258
|
+
from_epoch = MIN(from_epoch, ?),
|
|
4259
|
+
to_epoch = MAX(to_epoch, ?),
|
|
4260
|
+
kind = 'legacy'`,
|
|
4261
|
+
docId,
|
|
4262
|
+
clientId,
|
|
4263
|
+
seq,
|
|
4264
|
+
from,
|
|
4265
|
+
legacyEpoch,
|
|
4266
|
+
seq,
|
|
4267
|
+
from,
|
|
4268
|
+
legacyEpoch
|
|
4269
|
+
);
|
|
4270
|
+
note = true;
|
|
4271
|
+
}
|
|
4272
|
+
});
|
|
4273
|
+
return { imported, settled, discontinuities, note };
|
|
4274
|
+
}
|
|
4275
|
+
/**
|
|
4276
|
+
* Delete exactly the rows that were imported, from the LEGACY pool (#3760).
|
|
4277
|
+
*
|
|
4278
|
+
* By primary key, and only the named batch: a legacy tab still running may
|
|
4279
|
+
* have written more since the export, and a blanket delete of the document's
|
|
4280
|
+
* log would take a write nothing has taken over.
|
|
4281
|
+
*
|
|
4282
|
+
* `discontinuities` are the bulk-load debts the scope store has recorded
|
|
4283
|
+
* (finding 3760-REVIEW-06). They are deleted for the same reason the pending
|
|
4284
|
+
* rows are: the debt now lives in the scope store, where a convergence
|
|
4285
|
+
* settles it, and a copy left in a pool that is never removed would keep
|
|
4286
|
+
* putting it back.
|
|
4287
|
+
*
|
|
4288
|
+
* The sequences go with the rows, never back into circulation (finding
|
|
4289
|
+
* 3760-REVIEW-08). A pre-amendment bundle allocates `max(pending, acked) + 1`
|
|
4290
|
+
* per client, so with its rows deleted and nothing acknowledged a legacy tab
|
|
4291
|
+
* that reopens the document under the same client id would hand out
|
|
4292
|
+
* sequence 1 again — and the next import would find `(pool, client, 1)` in
|
|
4293
|
+
* the ledger, read the NEW write as the one already taken over, and delete
|
|
4294
|
+
* it. So the client's `_client_acks` mark is raised to the highest sequence
|
|
4295
|
+
* handed over, the one floor that allocator reads. That mark is a HAND-OVER,
|
|
4296
|
+
* not a delivery, and the scope store must never read it as one (an adopted
|
|
4297
|
+
* row at or below an acknowledged mark is pruned as delivered): the real
|
|
4298
|
+
* acknowledgement is kept beside it in `_legacy_handover`, and
|
|
4299
|
+
* {@link exportLegacyDocument} reports that one.
|
|
4300
|
+
*/
|
|
4301
|
+
static dropLegacyDocument(sql, docId, rows, discontinuities = []) {
|
|
4302
|
+
if (rows.length === 0 && discontinuities.length === 0) return 0;
|
|
4303
|
+
let dropped = 0;
|
|
4304
|
+
const handedThrough = /* @__PURE__ */ new Map();
|
|
4305
|
+
for (const row of rows) {
|
|
4306
|
+
handedThrough.set(
|
|
4307
|
+
row.clientId,
|
|
4308
|
+
Math.max(handedThrough.get(row.clientId) ?? 0, row.seq)
|
|
4309
|
+
);
|
|
4310
|
+
}
|
|
4311
|
+
sql.transaction(() => {
|
|
4312
|
+
if (handedThrough.size > 0) sql.exec(LEGACY_HANDOVER_DDL);
|
|
4313
|
+
for (const [clientId, through] of handedThrough) {
|
|
4314
|
+
const marked = Number(
|
|
4315
|
+
Array.from(
|
|
4316
|
+
sql.exec(
|
|
4317
|
+
"SELECT acked_seq FROM _client_acks WHERE doc_id = ? AND client_id = ?",
|
|
4318
|
+
docId,
|
|
4319
|
+
clientId
|
|
4320
|
+
)
|
|
4321
|
+
)[0]?.acked_seq ?? 0
|
|
4322
|
+
);
|
|
4323
|
+
const delivered = legacyDeliveredAck(sql, docId, clientId, marked);
|
|
4324
|
+
const floor = Math.max(marked, through);
|
|
4325
|
+
sql.exec(
|
|
4326
|
+
`INSERT INTO _client_acks (doc_id, client_id, acked_seq) VALUES (?, ?, ?)
|
|
4327
|
+
ON CONFLICT (doc_id, client_id) DO UPDATE SET acked_seq = excluded.acked_seq`,
|
|
4328
|
+
docId,
|
|
4329
|
+
clientId,
|
|
4330
|
+
floor
|
|
4331
|
+
);
|
|
4332
|
+
sql.exec(
|
|
4333
|
+
`INSERT INTO _legacy_handover (doc_id, client_id, handed_seq, acked_seq)
|
|
4334
|
+
VALUES (?, ?, ?, ?)
|
|
4335
|
+
ON CONFLICT (doc_id, client_id) DO UPDATE SET
|
|
4336
|
+
handed_seq = excluded.handed_seq,
|
|
4337
|
+
acked_seq = excluded.acked_seq`,
|
|
4338
|
+
docId,
|
|
4339
|
+
clientId,
|
|
4340
|
+
floor,
|
|
4341
|
+
delivered
|
|
4342
|
+
);
|
|
4343
|
+
}
|
|
4344
|
+
for (const epoch of discontinuities) {
|
|
4345
|
+
sql.exec(
|
|
4346
|
+
"DELETE FROM _discontinuities WHERE doc_id = ? AND epoch = ?",
|
|
4347
|
+
docId,
|
|
4348
|
+
epoch
|
|
4349
|
+
);
|
|
4350
|
+
}
|
|
4351
|
+
for (const row of rows) {
|
|
4352
|
+
sql.exec(
|
|
4353
|
+
"DELETE FROM _pending_ops WHERE doc_id = ? AND client_id = ? AND seq = ?",
|
|
4354
|
+
docId,
|
|
4355
|
+
row.clientId,
|
|
4356
|
+
row.seq
|
|
4357
|
+
);
|
|
4358
|
+
dropped++;
|
|
4359
|
+
}
|
|
4360
|
+
});
|
|
4361
|
+
return dropped;
|
|
4362
|
+
}
|
|
3847
4363
|
// ── helpers ───────────────────────────────────────────────────────
|
|
3848
4364
|
rows(query, ...bindings) {
|
|
3849
4365
|
return Array.from(this.sql.exec(query, ...bindings));
|
|
@@ -4039,10 +4555,10 @@ function observeFormat2Overlay(doc, store, onApplied) {
|
|
|
4039
4555
|
doc.on("afterTransaction", handler);
|
|
4040
4556
|
return () => doc.off("afterTransaction", handler);
|
|
4041
4557
|
}
|
|
4042
|
-
function foldFormat2Overlay(doc, store, onApplied) {
|
|
4558
|
+
function foldFormat2Overlay(doc, store, onApplied, options = {}) {
|
|
4043
4559
|
const sink = store;
|
|
4044
4560
|
if (typeof sink.foldWholeOverlay === "function") {
|
|
4045
|
-
sink.foldWholeOverlay(doc, onApplied);
|
|
4561
|
+
sink.foldWholeOverlay(doc, onApplied, options);
|
|
4046
4562
|
return;
|
|
4047
4563
|
}
|
|
4048
4564
|
const sync = store;
|
|
@@ -4066,6 +4582,68 @@ var init_format2Observer = __esm({
|
|
|
4066
4582
|
}
|
|
4067
4583
|
});
|
|
4068
4584
|
|
|
4585
|
+
// src/models/format2OverlayKeysSince.ts
|
|
4586
|
+
function readStateVector(doc) {
|
|
4587
|
+
const vector = {};
|
|
4588
|
+
for (const client of doc.store.clients.keys()) {
|
|
4589
|
+
vector[String(client)] = Y7.getState(doc.store, client);
|
|
4590
|
+
}
|
|
4591
|
+
return vector;
|
|
4592
|
+
}
|
|
4593
|
+
function foldableModels(doc, isHydrated = () => true) {
|
|
4594
|
+
const models = [];
|
|
4595
|
+
for (const name of doc.share.keys()) {
|
|
4596
|
+
if (!isFormat2ModelMap(name)) continue;
|
|
4597
|
+
if (!isHydrated(name)) continue;
|
|
4598
|
+
models.push(name);
|
|
4599
|
+
}
|
|
4600
|
+
return models.sort();
|
|
4601
|
+
}
|
|
4602
|
+
function overlayKeysSince(doc, stored, isHydrated = () => true) {
|
|
4603
|
+
const rootNames = /* @__PURE__ */ new Map();
|
|
4604
|
+
for (const [name, type] of doc.share) rootNames.set(type, name);
|
|
4605
|
+
const covered = new Set(stored.models);
|
|
4606
|
+
const keys = /* @__PURE__ */ new Map();
|
|
4607
|
+
let moved = false;
|
|
4608
|
+
let visited = 0;
|
|
4609
|
+
for (const [client, structs] of doc.store.clients) {
|
|
4610
|
+
const from = stored.vector[String(client)] ?? 0;
|
|
4611
|
+
if (Y7.getState(doc.store, client) <= from) continue;
|
|
4612
|
+
moved = true;
|
|
4613
|
+
let index = from === 0 ? 0 : Y7.findIndexSS(structs, from);
|
|
4614
|
+
for (; index < structs.length; index++) {
|
|
4615
|
+
const struct = structs[index];
|
|
4616
|
+
visited++;
|
|
4617
|
+
if (!(struct instanceof Y7.Item)) continue;
|
|
4618
|
+
const key = struct.parentSub;
|
|
4619
|
+
if (typeof key !== "string") continue;
|
|
4620
|
+
const model = rootNames.get(struct.parent);
|
|
4621
|
+
if (model === void 0 || !isFormat2ModelMap(model)) continue;
|
|
4622
|
+
if (!isHydrated(model) || !covered.has(model)) continue;
|
|
4623
|
+
let set = keys.get(model);
|
|
4624
|
+
if (!set) keys.set(model, set = /* @__PURE__ */ new Set());
|
|
4625
|
+
set.add(key);
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
const uncovered = [];
|
|
4629
|
+
for (const model of foldableModels(doc, isHydrated)) {
|
|
4630
|
+
if (covered.has(model)) continue;
|
|
4631
|
+
const map = doc.getMap(model);
|
|
4632
|
+
if (map.size === 0) continue;
|
|
4633
|
+
uncovered.push(model);
|
|
4634
|
+
}
|
|
4635
|
+
if (uncovered.length > 0) moved = true;
|
|
4636
|
+
return { moved, keys, uncovered, visited };
|
|
4637
|
+
}
|
|
4638
|
+
var Y7;
|
|
4639
|
+
var init_format2OverlayKeysSince = __esm({
|
|
4640
|
+
"src/models/format2OverlayKeysSince.ts"() {
|
|
4641
|
+
"use strict";
|
|
4642
|
+
Y7 = __toESM(require("yjs"), 1);
|
|
4643
|
+
init_format2Observer();
|
|
4644
|
+
}
|
|
4645
|
+
});
|
|
4646
|
+
|
|
4069
4647
|
// src/models/format2RecordStoreAsync.ts
|
|
4070
4648
|
function widenDeferredReplay(held, note, epoch) {
|
|
4071
4649
|
if (!note) {
|
|
@@ -4082,7 +4660,7 @@ function widenDeferredReplay(held, note, epoch) {
|
|
|
4082
4660
|
function isFormat2RecordStoreAsync(value) {
|
|
4083
4661
|
return value instanceof Format2RecordStoreAsync;
|
|
4084
4662
|
}
|
|
4085
|
-
var ROW_REFRESH_PAGE, Format2FoldBrokenError, Format2RecordStoreAsync;
|
|
4663
|
+
var ROW_REFRESH_PAGE, LOCAL_STAMP_IDLE_MS, FORMAT2_HOLD_ATTEMPTS, FORMAT2_HOLD_RETRY_MS, Format2FoldBrokenError, Format2RecordStoreAsync;
|
|
4086
4664
|
var init_format2RecordStoreAsync = __esm({
|
|
4087
4665
|
"src/models/format2RecordStoreAsync.ts"() {
|
|
4088
4666
|
"use strict";
|
|
@@ -4092,8 +4670,12 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
4092
4670
|
init_snapshotChunks();
|
|
4093
4671
|
init_format2RecordStore();
|
|
4094
4672
|
init_format2RowCache();
|
|
4673
|
+
init_format2OverlayKeysSince();
|
|
4095
4674
|
init_format2Observer();
|
|
4096
4675
|
ROW_REFRESH_PAGE = 500;
|
|
4676
|
+
LOCAL_STAMP_IDLE_MS = 250;
|
|
4677
|
+
FORMAT2_HOLD_ATTEMPTS = 5;
|
|
4678
|
+
FORMAT2_HOLD_RETRY_MS = 1e3;
|
|
4097
4679
|
Format2FoldBrokenError = class extends Error {
|
|
4098
4680
|
constructor(docId, cause) {
|
|
4099
4681
|
super(
|
|
@@ -4110,6 +4692,12 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
4110
4692
|
this.host = host;
|
|
4111
4693
|
this.docId = docId;
|
|
4112
4694
|
}
|
|
4695
|
+
/**
|
|
4696
|
+
* How long a held batch waits for its document to catch up before it is
|
|
4697
|
+
* tried again, overridable so a suite need not spend it.
|
|
4698
|
+
* @internal
|
|
4699
|
+
*/
|
|
4700
|
+
static holdRetryMs = FORMAT2_HOLD_RETRY_MS;
|
|
4113
4701
|
cache = new Format2RowCache();
|
|
4114
4702
|
state = "ready";
|
|
4115
4703
|
foldBrokenCause = null;
|
|
@@ -4119,6 +4707,10 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
4119
4707
|
/** Operations admitted and not yet finished, for `drain()`. */
|
|
4120
4708
|
admitted = 0;
|
|
4121
4709
|
drainWaiters = [];
|
|
4710
|
+
/** Local writes whose commit or projection is still running (#3756). */
|
|
4711
|
+
localWrites = 0;
|
|
4712
|
+
localWriteWaiters = [];
|
|
4713
|
+
localWritesRefused = false;
|
|
4122
4714
|
/**
|
|
4123
4715
|
* Mark commands in flight, outside the scheduler.
|
|
4124
4716
|
*
|
|
@@ -4138,6 +4730,38 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
4138
4730
|
unfoldedBatches = 0;
|
|
4139
4731
|
/** The mirror. Unused on an in-process host, where the store is asked. */
|
|
4140
4732
|
mirror = null;
|
|
4733
|
+
/**
|
|
4734
|
+
* The document that IS this store's current overlay (#3782). Only its
|
|
4735
|
+
* batches are stamped: a sealed archive folded through a throwaway doc
|
|
4736
|
+
* describes an epoch that is over, and its vector means nothing here.
|
|
4737
|
+
*/
|
|
4738
|
+
trackedOverlay = null;
|
|
4739
|
+
/**
|
|
4740
|
+
* The tracked overlay's own clock before the earliest local write still in
|
|
4741
|
+
* flight, or `null` (#3782). A write's items are on the overlay the moment
|
|
4742
|
+
* it publishes, but its query row lands only when its projection settles;
|
|
4743
|
+
* a stamp read in between must not vouch for them, so this client's clock
|
|
4744
|
+
* is capped here in every stamp read while any local write is running.
|
|
4745
|
+
*/
|
|
4746
|
+
localFloor = null;
|
|
4747
|
+
/**
|
|
4748
|
+
* A settled local write is not yet certified by any stamp (finding
|
|
4749
|
+
* 3782-R06). Issued as a stamp-only batch after an idle window, at a
|
|
4750
|
+
* disconnect, or carried along by the next remote batch's stamp.
|
|
4751
|
+
*/
|
|
4752
|
+
localStampOwed = false;
|
|
4753
|
+
localStampTimer = null;
|
|
4754
|
+
/**
|
|
4755
|
+
* A batch the host refused because its document is behind what the store
|
|
4756
|
+
* certifies (#3782, finding 3782-R02). Nothing of it was written. It goes
|
|
4757
|
+
* back out with the next transaction of its document — which is usually the
|
|
4758
|
+
* one that catches it up — or after `holdRetryMs`, and past
|
|
4759
|
+
* {@link FORMAT2_HOLD_ATTEMPTS} refusals it is folded forced.
|
|
4760
|
+
*/
|
|
4761
|
+
heldBatch = null;
|
|
4762
|
+
heldTimer = null;
|
|
4763
|
+
/** Which query tables a batch's records are projected into (#3782). */
|
|
4764
|
+
projectionProvider = null;
|
|
4141
4765
|
/**
|
|
4142
4766
|
* Open the store behind `host`: create the schema and load the marks mirror.
|
|
4143
4767
|
*/
|
|
@@ -4285,6 +4909,120 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
4285
4909
|
await this.batchSettled;
|
|
4286
4910
|
await this.drainMarks();
|
|
4287
4911
|
}
|
|
4912
|
+
// ── Local writes, commit AND projection ───────────────────────────
|
|
4913
|
+
/**
|
|
4914
|
+
* Run one local write — its scheduled commit and the query projection that
|
|
4915
|
+
* follows it — as a unit a release can wait for (#3756).
|
|
4916
|
+
*
|
|
4917
|
+
* `drain()` covers the commit only: the projection runs after the scheduled
|
|
4918
|
+
* operation resolves, and a projection that finds its document unregistered
|
|
4919
|
+
* returns silently. A release keeps the projection marks, so it must not
|
|
4920
|
+
* unregister while a write it admitted still has its row to project.
|
|
4921
|
+
*/
|
|
4922
|
+
async trackLocalWrite(work) {
|
|
4923
|
+
if (this.localWritesRefused || this.state === "closed") {
|
|
4924
|
+
throw new DocumentClosedError(
|
|
4925
|
+
`[format2] Document '${this.docId}' is being released; the write was not run.`
|
|
4926
|
+
);
|
|
4927
|
+
}
|
|
4928
|
+
if (this.localWrites === 0) this.localFloor = this.ownClock();
|
|
4929
|
+
this.localWrites++;
|
|
4930
|
+
try {
|
|
4931
|
+
return await work();
|
|
4932
|
+
} finally {
|
|
4933
|
+
this.localWrites--;
|
|
4934
|
+
if (this.localWrites === 0) {
|
|
4935
|
+
this.localFloor = null;
|
|
4936
|
+
if (this.localStampOwed) this.scheduleLocalStamp();
|
|
4937
|
+
const waiters = this.localWriteWaiters;
|
|
4938
|
+
this.localWriteWaiters = [];
|
|
4939
|
+
for (const waiter of waiters) waiter();
|
|
4940
|
+
}
|
|
4941
|
+
}
|
|
4942
|
+
}
|
|
4943
|
+
/**
|
|
4944
|
+
* A local write just published to the tracked overlay (#3782, finding
|
|
4945
|
+
* 3782-R06). Called inside the scheduled operation that committed it; costs
|
|
4946
|
+
* a flag, not a vector read — the stamp that certifies it reads the vector
|
|
4947
|
+
* when it is issued, once the write's projection has settled.
|
|
4948
|
+
*/
|
|
4949
|
+
noteLocalPublish(doc) {
|
|
4950
|
+
if (doc !== this.trackedOverlay) return;
|
|
4951
|
+
this.localStampOwed = true;
|
|
4952
|
+
}
|
|
4953
|
+
/** The tracked overlay's own client clock right now. */
|
|
4954
|
+
ownClock() {
|
|
4955
|
+
const doc = this.trackedOverlay;
|
|
4956
|
+
if (!doc) return null;
|
|
4957
|
+
return readStateVector(doc)[String(doc.clientID)] ?? 0;
|
|
4958
|
+
}
|
|
4959
|
+
/**
|
|
4960
|
+
* Issue the owed local stamp after an idle window, so a burst of saves
|
|
4961
|
+
* stamps once. Not holding the process open: an idle window that never
|
|
4962
|
+
* fires is a stamp the next open pays for with a fold of those writes.
|
|
4963
|
+
*/
|
|
4964
|
+
scheduleLocalStamp() {
|
|
4965
|
+
if (this.localStampTimer) clearTimeout(this.localStampTimer);
|
|
4966
|
+
const timer = setTimeout(() => {
|
|
4967
|
+
this.localStampTimer = null;
|
|
4968
|
+
this.issueLocalStamp();
|
|
4969
|
+
}, LOCAL_STAMP_IDLE_MS);
|
|
4970
|
+
timer.unref?.();
|
|
4971
|
+
this.localStampTimer = timer;
|
|
4972
|
+
}
|
|
4973
|
+
cancelLocalStamp() {
|
|
4974
|
+
this.localStampOwed = false;
|
|
4975
|
+
if (this.localStampTimer) {
|
|
4976
|
+
clearTimeout(this.localStampTimer);
|
|
4977
|
+
this.localStampTimer = null;
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
/**
|
|
4981
|
+
* Queue the stamp-only batch that certifies the settled local writes,
|
|
4982
|
+
* through the ordinary FIFO and the ordinary guard: every remote batch
|
|
4983
|
+
* queued before it is folded first, and a document behind the store is
|
|
4984
|
+
* refused like any other.
|
|
4985
|
+
*/
|
|
4986
|
+
issueLocalStamp() {
|
|
4987
|
+
if (!this.localStampOwed) return;
|
|
4988
|
+
this.localStampOwed = false;
|
|
4989
|
+
const doc = this.trackedOverlay;
|
|
4990
|
+
if (!doc || this.state === "closed") return;
|
|
4991
|
+
const batch = this.openBatch(doc);
|
|
4992
|
+
batch.stamp = this.captureStamp(doc);
|
|
4993
|
+
this.applyBatchInThread(batch);
|
|
4994
|
+
}
|
|
4995
|
+
/**
|
|
4996
|
+
* Issue the owed local stamp now and wait for it (#3782). A disconnect
|
|
4997
|
+
* calls it once every local write has projected and before the facade
|
|
4998
|
+
* closes, so a clean close leaves the store certified.
|
|
4999
|
+
*/
|
|
5000
|
+
async flushLocalStamp() {
|
|
5001
|
+
if (!this.localStampOwed) return;
|
|
5002
|
+
if (this.localStampTimer) {
|
|
5003
|
+
clearTimeout(this.localStampTimer);
|
|
5004
|
+
this.localStampTimer = null;
|
|
5005
|
+
}
|
|
5006
|
+
this.issueLocalStamp();
|
|
5007
|
+
await this.whenBatchesSettled();
|
|
5008
|
+
}
|
|
5009
|
+
/** Local writes admitted and not yet projected. */
|
|
5010
|
+
localWritesInFlight() {
|
|
5011
|
+
return this.localWrites;
|
|
5012
|
+
}
|
|
5013
|
+
/** Resolves when no local write is committing or projecting. */
|
|
5014
|
+
whenLocalWritesSettled() {
|
|
5015
|
+
if (this.localWrites === 0) return Promise.resolve();
|
|
5016
|
+
return new Promise((resolve) => this.localWriteWaiters.push(resolve));
|
|
5017
|
+
}
|
|
5018
|
+
/**
|
|
5019
|
+
* Refuse every local write from now on. The release calls this once the
|
|
5020
|
+
* writes it waited for have landed, in the same turn as its last check, so
|
|
5021
|
+
* no write can be admitted between that check and the unregistering.
|
|
5022
|
+
*/
|
|
5023
|
+
refuseLocalWrites() {
|
|
5024
|
+
this.localWritesRefused = true;
|
|
5025
|
+
}
|
|
4288
5026
|
ops() {
|
|
4289
5027
|
const dispatch = (command) => this.host.dispatch(command);
|
|
4290
5028
|
return {
|
|
@@ -4397,6 +5135,7 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
4397
5135
|
return;
|
|
4398
5136
|
}
|
|
4399
5137
|
const marks = this.mirrored();
|
|
5138
|
+
if (marks.epoch !== epoch) marks.foldedState = null;
|
|
4400
5139
|
marks.epoch = epoch;
|
|
4401
5140
|
marks.deferredReplay = widenDeferredReplay(
|
|
4402
5141
|
marks.deferredReplay,
|
|
@@ -4492,6 +5231,48 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
4492
5231
|
);
|
|
4493
5232
|
this.refreshMirror({ deferredReplay: null });
|
|
4494
5233
|
}
|
|
5234
|
+
// ── What the merged view was last folded from (#3782) ────────────
|
|
5235
|
+
/** The stored folded state, or `null` when nothing is known. */
|
|
5236
|
+
foldedState() {
|
|
5237
|
+
const store = this.direct();
|
|
5238
|
+
return store ? store.foldedState() : this.mirrored().foldedState ?? null;
|
|
5239
|
+
}
|
|
5240
|
+
/**
|
|
5241
|
+
* Say which document is this store's live overlay. The binding calls it at
|
|
5242
|
+
* every connect, so an epoch move's fresh overlay replaces the sealed one.
|
|
5243
|
+
*/
|
|
5244
|
+
trackOverlay(doc) {
|
|
5245
|
+
if (this.heldBatch && this.heldBatch.doc !== doc) {
|
|
5246
|
+
this.retireHeld("the document was rebound to a new overlay");
|
|
5247
|
+
}
|
|
5248
|
+
this.trackedOverlay = doc;
|
|
5249
|
+
}
|
|
5250
|
+
/**
|
|
5251
|
+
* Say which query tables a fold's records go into. Set by the binding that
|
|
5252
|
+
* owns the model registry; a store nothing set it on — one registered
|
|
5253
|
+
* directly, with no engine — projects nothing, as it never did.
|
|
5254
|
+
*/
|
|
5255
|
+
setProjectionProvider(provider) {
|
|
5256
|
+
this.projectionProvider = provider;
|
|
5257
|
+
}
|
|
5258
|
+
/** What folding the tracked overlay up to now would certify. */
|
|
5259
|
+
captureStamp(doc) {
|
|
5260
|
+
if (doc !== this.trackedOverlay) return null;
|
|
5261
|
+
const vector = readStateVector(doc);
|
|
5262
|
+
const own = String(doc.clientID);
|
|
5263
|
+
if (this.localFloor !== null) {
|
|
5264
|
+
if (this.localFloor === 0) delete vector[own];
|
|
5265
|
+
else if (vector[own] !== void 0) {
|
|
5266
|
+
vector[own] = Math.min(vector[own], this.localFloor);
|
|
5267
|
+
}
|
|
5268
|
+
} else if (this.localStampOwed) {
|
|
5269
|
+
this.cancelLocalStamp();
|
|
5270
|
+
}
|
|
5271
|
+
return {
|
|
5272
|
+
vector,
|
|
5273
|
+
models: foldableModels(doc, (model) => this.isHydrated(model))
|
|
5274
|
+
};
|
|
5275
|
+
}
|
|
4495
5276
|
/** Whether the base this view is on finished loading (#3435). */
|
|
4496
5277
|
baseComplete() {
|
|
4497
5278
|
const store = this.direct();
|
|
@@ -4511,7 +5292,11 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
4511
5292
|
const store = this.direct();
|
|
4512
5293
|
if (store) store.setHydrationScope(scope);
|
|
4513
5294
|
else {
|
|
4514
|
-
this.mirrored()
|
|
5295
|
+
const marks = this.mirrored();
|
|
5296
|
+
if (JSON.stringify(marks.hydrationScope) !== JSON.stringify(scope)) {
|
|
5297
|
+
marks.foldedState = null;
|
|
5298
|
+
}
|
|
5299
|
+
marks.hydrationScope = scope;
|
|
4515
5300
|
this.sendMark({ kind: "setHydrationScope", models: scope });
|
|
4516
5301
|
}
|
|
4517
5302
|
}
|
|
@@ -4707,6 +5492,7 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
4707
5492
|
ordinal
|
|
4708
5493
|
})
|
|
4709
5494
|
);
|
|
5495
|
+
this.refreshMirror({ foldedState: null });
|
|
4710
5496
|
for (const entry of entries) {
|
|
4711
5497
|
if (this.cache.peek(model, entry.id)) {
|
|
4712
5498
|
await this.refreshRows(model, [entry.id]);
|
|
@@ -4745,6 +5531,7 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
4745
5531
|
})
|
|
4746
5532
|
);
|
|
4747
5533
|
if (!reply.skipped) {
|
|
5534
|
+
this.refreshMirror({ foldedState: null });
|
|
4748
5535
|
await this.refreshRange(
|
|
4749
5536
|
model,
|
|
4750
5537
|
coverage,
|
|
@@ -4760,6 +5547,7 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
4760
5547
|
() => this.host.dispatch({ kind: "deleteRange", model, after, through, limit })
|
|
4761
5548
|
);
|
|
4762
5549
|
if (reply.removed > 0) {
|
|
5550
|
+
this.refreshMirror({ foldedState: null });
|
|
4763
5551
|
await this.refreshRange(model, { after, through }, []);
|
|
4764
5552
|
}
|
|
4765
5553
|
return reply;
|
|
@@ -4838,6 +5626,7 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
4838
5626
|
"discardMergedView",
|
|
4839
5627
|
() => this.host.dispatch({ kind: "discardMergedView" })
|
|
4840
5628
|
);
|
|
5629
|
+
this.refreshMirror({ foldedState: null });
|
|
4841
5630
|
this.cache.invalidateAll();
|
|
4842
5631
|
}
|
|
4843
5632
|
// ── Presence and adoption (#3431) ─────────────────────────────────
|
|
@@ -4913,6 +5702,26 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
4913
5702
|
);
|
|
4914
5703
|
return adopted;
|
|
4915
5704
|
}
|
|
5705
|
+
// ── The query projection (#3782) ──────────────────────────────────
|
|
5706
|
+
/**
|
|
5707
|
+
* Project the named records of `model` into its query tables, on the host.
|
|
5708
|
+
*
|
|
5709
|
+
* One command however many ids: the host reads each record from its own
|
|
5710
|
+
* `records` and writes the derived rows in one transaction, so nothing per
|
|
5711
|
+
* record crosses the boundary. A failure withdraws the model's projection
|
|
5712
|
+
* mark on the host and rejects here.
|
|
5713
|
+
*/
|
|
5714
|
+
async projectRows(model, ids, spec) {
|
|
5715
|
+
this.assertReadable();
|
|
5716
|
+
this.assertHydrated(model);
|
|
5717
|
+
if (ids.length === 0) return { projected: 0, deleted: 0 };
|
|
5718
|
+
return this.host.dispatch({
|
|
5719
|
+
kind: "projectRows",
|
|
5720
|
+
model,
|
|
5721
|
+
ids: [...ids],
|
|
5722
|
+
spec
|
|
5723
|
+
});
|
|
5724
|
+
}
|
|
4916
5725
|
// ── Projection marks ──────────────────────────────────────────────
|
|
4917
5726
|
queryProjectionIsCurrent(model) {
|
|
4918
5727
|
return this.host.dispatch({
|
|
@@ -4946,9 +5755,11 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
4946
5755
|
* are COALESCED into one: their key sets are unioned, so one
|
|
4947
5756
|
* `applyRemoteBatch` carries them and each touched record is reported once.
|
|
4948
5757
|
*/
|
|
4949
|
-
enqueueOverlayBatch(doc, touched, onApplied) {
|
|
5758
|
+
enqueueOverlayBatch(doc, touched, onApplied, options = {}) {
|
|
4950
5759
|
if (this.state === "closed") return;
|
|
5760
|
+
if (this.heldBatch && this.heldBatch.doc === doc) this.releaseHeld(false);
|
|
4951
5761
|
const batch = this.openBatch(doc);
|
|
5762
|
+
if (options.atBind) batch.atBind = true;
|
|
4952
5763
|
for (const [model, keys] of touched) {
|
|
4953
5764
|
let set = batch.touched.get(model);
|
|
4954
5765
|
if (!set) batch.touched.set(model, set = /* @__PURE__ */ new Set());
|
|
@@ -4957,16 +5768,23 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
4957
5768
|
if (onApplied && !batch.onApplied.includes(onApplied)) {
|
|
4958
5769
|
batch.onApplied.push(onApplied);
|
|
4959
5770
|
}
|
|
5771
|
+
batch.stamp = this.captureStamp(doc);
|
|
4960
5772
|
this.applyBatchInThread(batch);
|
|
4961
5773
|
}
|
|
4962
5774
|
/** Queue a WHOLE-overlay catch-up fold (connect, refold, sealed overlay). */
|
|
4963
|
-
foldWholeOverlay(doc, onApplied) {
|
|
5775
|
+
foldWholeOverlay(doc, onApplied, options = {}) {
|
|
4964
5776
|
if (this.state === "closed") return;
|
|
4965
5777
|
const batch = this.openBatch(doc);
|
|
4966
5778
|
batch.wholeDocs.push(doc);
|
|
5779
|
+
if (options.atBind) batch.atBind = true;
|
|
4967
5780
|
if (onApplied && !batch.onApplied.includes(onApplied)) {
|
|
4968
5781
|
batch.onApplied.push(onApplied);
|
|
4969
5782
|
}
|
|
5783
|
+
const stamp = this.captureStamp(doc);
|
|
5784
|
+
if (stamp) {
|
|
5785
|
+
batch.stamp = stamp;
|
|
5786
|
+
batch.whole = true;
|
|
5787
|
+
}
|
|
4970
5788
|
this.applyBatchInThread(batch);
|
|
4971
5789
|
}
|
|
4972
5790
|
openBatch(doc) {
|
|
@@ -4979,7 +5797,11 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
4979
5797
|
touched: /* @__PURE__ */ new Map(),
|
|
4980
5798
|
wholeDocs: [],
|
|
4981
5799
|
onApplied: [],
|
|
4982
|
-
applied: false
|
|
5800
|
+
applied: false,
|
|
5801
|
+
stamp: null,
|
|
5802
|
+
whole: false,
|
|
5803
|
+
atBind: false,
|
|
5804
|
+
behind: 0
|
|
4983
5805
|
};
|
|
4984
5806
|
this.pendingBatch = batch;
|
|
4985
5807
|
this.unfoldedBatches++;
|
|
@@ -5014,6 +5836,169 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
5014
5836
|
}
|
|
5015
5837
|
return entries;
|
|
5016
5838
|
}
|
|
5839
|
+
/** Batches held because their document lags the store: 0 or 1. */
|
|
5840
|
+
heldBatches() {
|
|
5841
|
+
return this.heldBatch ? 1 : 0;
|
|
5842
|
+
}
|
|
5843
|
+
/** Everything `source` stands for, added to `target`. */
|
|
5844
|
+
mergeBatch(target, source) {
|
|
5845
|
+
for (const [model, keys] of source.touched) {
|
|
5846
|
+
let set = target.touched.get(model);
|
|
5847
|
+
if (!set) target.touched.set(model, set = /* @__PURE__ */ new Set());
|
|
5848
|
+
for (const key of keys) set.add(key);
|
|
5849
|
+
}
|
|
5850
|
+
for (const doc of source.wholeDocs) {
|
|
5851
|
+
if (!target.wholeDocs.includes(doc)) target.wholeDocs.push(doc);
|
|
5852
|
+
}
|
|
5853
|
+
for (const notify of source.onApplied) {
|
|
5854
|
+
if (!target.onApplied.includes(notify)) target.onApplied.push(notify);
|
|
5855
|
+
}
|
|
5856
|
+
target.stamp = target.stamp ?? source.stamp;
|
|
5857
|
+
target.whole = target.whole || source.whole;
|
|
5858
|
+
target.behind = Math.max(target.behind, source.behind);
|
|
5859
|
+
}
|
|
5860
|
+
/** The host refused `batch`: its document is behind the store. Hold it. */
|
|
5861
|
+
holdBatch(batch) {
|
|
5862
|
+
batch.behind++;
|
|
5863
|
+
if (this.state === "closed") return;
|
|
5864
|
+
if (batch.doc !== this.trackedOverlay) {
|
|
5865
|
+
Logger.warn(
|
|
5866
|
+
`[format2] ${this.docId}: a fold refused for an overlay this document has since moved off was dropped; nothing of it was written`
|
|
5867
|
+
);
|
|
5868
|
+
return;
|
|
5869
|
+
}
|
|
5870
|
+
if (this.heldBatch && this.heldBatch !== batch) {
|
|
5871
|
+
if (this.heldBatch.doc === batch.doc) {
|
|
5872
|
+
this.mergeBatch(this.heldBatch, batch);
|
|
5873
|
+
} else {
|
|
5874
|
+
this.retireHeld("the document was rebound to a new overlay");
|
|
5875
|
+
this.heldBatch = batch;
|
|
5876
|
+
}
|
|
5877
|
+
} else {
|
|
5878
|
+
this.heldBatch = batch;
|
|
5879
|
+
}
|
|
5880
|
+
const held = this.heldBatch;
|
|
5881
|
+
Logger.warn(
|
|
5882
|
+
`[format2] ${this.docId}: the store is ahead of this document; its fold is held until the document catches up (refused ${held.behind} of ${FORMAT2_HOLD_ATTEMPTS} time(s) before it is folded forced).`
|
|
5883
|
+
);
|
|
5884
|
+
if (this.heldTimer) clearTimeout(this.heldTimer);
|
|
5885
|
+
const timer = setTimeout(() => {
|
|
5886
|
+
this.heldTimer = null;
|
|
5887
|
+
this.releaseHeld(true);
|
|
5888
|
+
}, _Format2RecordStoreAsync.holdRetryMs);
|
|
5889
|
+
timer.unref?.();
|
|
5890
|
+
this.heldTimer = timer;
|
|
5891
|
+
}
|
|
5892
|
+
/**
|
|
5893
|
+
* Put the held batch back in the queue. `schedule` false leaves it pending
|
|
5894
|
+
* for the caller, who is adding a transaction's keys to it.
|
|
5895
|
+
*/
|
|
5896
|
+
releaseHeld(schedule) {
|
|
5897
|
+
const held = this.heldBatch;
|
|
5898
|
+
if (!held) return;
|
|
5899
|
+
if (held.doc !== this.trackedOverlay) {
|
|
5900
|
+
this.retireHeld("its overlay is no longer the document's live one");
|
|
5901
|
+
return;
|
|
5902
|
+
}
|
|
5903
|
+
this.heldBatch = null;
|
|
5904
|
+
if (this.heldTimer) {
|
|
5905
|
+
clearTimeout(this.heldTimer);
|
|
5906
|
+
this.heldTimer = null;
|
|
5907
|
+
}
|
|
5908
|
+
if (this.state === "closed") return;
|
|
5909
|
+
const batch = this.openBatch(held.doc);
|
|
5910
|
+
this.mergeBatch(batch, held);
|
|
5911
|
+
if (schedule) this.applyBatchInThread(batch);
|
|
5912
|
+
}
|
|
5913
|
+
/**
|
|
5914
|
+
* Drop the held batch without folding it (finding 3782-C03). It was refused
|
|
5915
|
+
* because the store is ahead of its document, so it wrote nothing and
|
|
5916
|
+
* certified nothing; what it carried is the sealed overlay's, which the
|
|
5917
|
+
* room archived and the tab the store is ahead of folds.
|
|
5918
|
+
*/
|
|
5919
|
+
retireHeld(reason) {
|
|
5920
|
+
const held = this.heldBatch;
|
|
5921
|
+
if (!held) return;
|
|
5922
|
+
this.heldBatch = null;
|
|
5923
|
+
if (this.heldTimer) {
|
|
5924
|
+
clearTimeout(this.heldTimer);
|
|
5925
|
+
this.heldTimer = null;
|
|
5926
|
+
}
|
|
5927
|
+
Logger.warn(
|
|
5928
|
+
`[format2] ${this.docId}: a held fold was dropped because ${reason}; nothing of it was written`
|
|
5929
|
+
);
|
|
5930
|
+
}
|
|
5931
|
+
/**
|
|
5932
|
+
* Fold the held batch with `batch`, which is about to run (finding
|
|
5933
|
+
* 3782-C02). A batch queued while its predecessor's refusal was on the way
|
|
5934
|
+
* opened on its own — nothing was held yet — and its stamp, read after the
|
|
5935
|
+
* refused transaction, covers that transaction's items. Carrying the held
|
|
5936
|
+
* keys is what makes that stamp true; if `batch` is refused in turn, the
|
|
5937
|
+
* two are held together.
|
|
5938
|
+
*/
|
|
5939
|
+
absorbHeld(batch) {
|
|
5940
|
+
const held = this.heldBatch;
|
|
5941
|
+
if (!held || held === batch || held.doc !== batch.doc) return;
|
|
5942
|
+
this.heldBatch = null;
|
|
5943
|
+
if (this.heldTimer) {
|
|
5944
|
+
clearTimeout(this.heldTimer);
|
|
5945
|
+
this.heldTimer = null;
|
|
5946
|
+
}
|
|
5947
|
+
this.mergeBatch(batch, held);
|
|
5948
|
+
}
|
|
5949
|
+
/**
|
|
5950
|
+
* The one command a batch is folded, projected and stamped by (#3782).
|
|
5951
|
+
*
|
|
5952
|
+
* `null` when there is nothing to send: no entries and nothing to certify.
|
|
5953
|
+
* A stamp is offered only for the tracked overlay, and only while the
|
|
5954
|
+
* facade is healthy — a batch folded after a failed one must not vouch for
|
|
5955
|
+
* the items the failure left behind — unless the batch folds the WHOLE
|
|
5956
|
+
* overlay, which is the repair. The folding document's CURRENT vector goes
|
|
5957
|
+
* with every tracked-overlay batch, stamped or not, so the host can refuse
|
|
5958
|
+
* a fold from a document the store is ahead of (finding 3782-R02).
|
|
5959
|
+
*/
|
|
5960
|
+
batchCommand(batch, entries) {
|
|
5961
|
+
const tracked = batch.doc === this.trackedOverlay;
|
|
5962
|
+
const stamp = tracked && batch.stamp && (batch.whole || this.state === "ready") ? batch.stamp : null;
|
|
5963
|
+
if (entries.length === 0 && stamp === null) return null;
|
|
5964
|
+
const command = {
|
|
5965
|
+
kind: "applyRemoteBatch",
|
|
5966
|
+
entries
|
|
5967
|
+
};
|
|
5968
|
+
const models = Array.from(new Set(entries.map((item) => item.model)));
|
|
5969
|
+
const project = models.length > 0 && this.projectionProvider ? this.projectionProvider(models) : [];
|
|
5970
|
+
if (project.length > 0) command.project = project;
|
|
5971
|
+
if (tracked) command.docState = readStateVector(batch.doc);
|
|
5972
|
+
else command.untracked = true;
|
|
5973
|
+
if (stamp) {
|
|
5974
|
+
command.foldedState = stamp;
|
|
5975
|
+
if (batch.whole) command.whole = true;
|
|
5976
|
+
}
|
|
5977
|
+
if (batch.behind >= FORMAT2_HOLD_ATTEMPTS) {
|
|
5978
|
+
return entries.length === 0 ? null : this.forcedCommand(command);
|
|
5979
|
+
}
|
|
5980
|
+
return command;
|
|
5981
|
+
}
|
|
5982
|
+
/**
|
|
5983
|
+
* The same batch, forced (#3782): folded whatever the store holds, with the
|
|
5984
|
+
* stored folded state cleared instead of written. What a bind does when
|
|
5985
|
+
* its document is behind the store — it cannot wait for the room.
|
|
5986
|
+
*/
|
|
5987
|
+
forcedCommand(command) {
|
|
5988
|
+
const { foldedState: _stamp, whole: _whole, ...rest } = command;
|
|
5989
|
+
return { ...rest, force: true };
|
|
5990
|
+
}
|
|
5991
|
+
/** What a folded batch's reply means for this side (#3782). */
|
|
5992
|
+
noteBatchReply(reply) {
|
|
5993
|
+
if (reply.foldedState !== void 0) {
|
|
5994
|
+
this.refreshMirror({ foldedState: reply.foldedState });
|
|
5995
|
+
}
|
|
5996
|
+
for (const failure of reply.projectionFailed ?? []) {
|
|
5997
|
+
Logger.error(
|
|
5998
|
+
`[format2] ${this.docId}: projecting a fold into '${failure.model}' query tables failed; the fold landed, the model's mark is withdrawn and the next bind rebuilds it: ${failure.message}`
|
|
5999
|
+
);
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
5017
6002
|
notifyBatch(batch, touched) {
|
|
5018
6003
|
for (const { model, id } of touched) {
|
|
5019
6004
|
for (const notify of batch.onApplied) {
|
|
@@ -5056,9 +6041,20 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
5056
6041
|
this.unfoldedBatches--;
|
|
5057
6042
|
if (this.pendingBatch === batch) this.pendingBatch = null;
|
|
5058
6043
|
try {
|
|
6044
|
+
this.absorbHeld(batch);
|
|
5059
6045
|
const entries = this.materializeBatch(batch);
|
|
5060
|
-
|
|
5061
|
-
|
|
6046
|
+
const command = this.batchCommand(batch, entries);
|
|
6047
|
+
if (command === null) return true;
|
|
6048
|
+
let reply = host.dispatchSync(command);
|
|
6049
|
+
this.noteBatchReply(reply);
|
|
6050
|
+
if (reply.behind && batch.atBind) {
|
|
6051
|
+
reply = host.dispatchSync(this.forcedCommand(command));
|
|
6052
|
+
this.noteBatchReply(reply);
|
|
6053
|
+
}
|
|
6054
|
+
if (reply.behind) {
|
|
6055
|
+
this.holdBatch(batch);
|
|
6056
|
+
return true;
|
|
6057
|
+
}
|
|
5062
6058
|
if (batch.wholeDocs.length > 0) this.clearFoldBroken();
|
|
5063
6059
|
for (const { model, id } of reply.touched) {
|
|
5064
6060
|
if (!this.cache.peek(model, id)) continue;
|
|
@@ -5078,16 +6074,24 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
5078
6074
|
if (this.state === "closed") return;
|
|
5079
6075
|
try {
|
|
5080
6076
|
await this.run("applyRemoteBatch", async () => {
|
|
6077
|
+
this.absorbHeld(batch);
|
|
5081
6078
|
const entries = this.materializeBatch(batch);
|
|
5082
6079
|
const repairs = batch.wholeDocs.length > 0;
|
|
5083
|
-
|
|
6080
|
+
const command = this.batchCommand(batch, entries);
|
|
6081
|
+
if (command === null) {
|
|
5084
6082
|
if (repairs) this.clearFoldBroken();
|
|
5085
6083
|
return;
|
|
5086
6084
|
}
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
6085
|
+
let reply = await this.host.dispatch(command);
|
|
6086
|
+
this.noteBatchReply(reply);
|
|
6087
|
+
if (reply.behind && batch.atBind) {
|
|
6088
|
+
reply = await this.host.dispatch(this.forcedCommand(command));
|
|
6089
|
+
this.noteBatchReply(reply);
|
|
6090
|
+
}
|
|
6091
|
+
if (reply.behind) {
|
|
6092
|
+
this.holdBatch(batch);
|
|
6093
|
+
return;
|
|
6094
|
+
}
|
|
5091
6095
|
if (repairs) this.clearFoldBroken();
|
|
5092
6096
|
const byModel = /* @__PURE__ */ new Map();
|
|
5093
6097
|
for (const { model, id } of reply.touched) {
|
|
@@ -5128,8 +6132,18 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
5128
6132
|
}
|
|
5129
6133
|
}
|
|
5130
6134
|
// ── Teardown ──────────────────────────────────────────────────────
|
|
5131
|
-
|
|
5132
|
-
|
|
6135
|
+
/**
|
|
6136
|
+
* Release the facade and its host. Removes nothing (#3756): the query
|
|
6137
|
+
* tables and projection marks stay for the next open.
|
|
6138
|
+
*/
|
|
6139
|
+
async close() {
|
|
6140
|
+
if (this.state === "closed") return;
|
|
6141
|
+
this.cancelLocalStamp();
|
|
6142
|
+
this.heldBatch = null;
|
|
6143
|
+
if (this.heldTimer) {
|
|
6144
|
+
clearTimeout(this.heldTimer);
|
|
6145
|
+
this.heldTimer = null;
|
|
6146
|
+
}
|
|
5133
6147
|
try {
|
|
5134
6148
|
await this.drain();
|
|
5135
6149
|
} catch {
|
|
@@ -5139,9 +6153,8 @@ var init_format2RecordStoreAsync = __esm({
|
|
|
5139
6153
|
} catch {
|
|
5140
6154
|
}
|
|
5141
6155
|
this.cache.clear();
|
|
5142
|
-
|
|
6156
|
+
await this.host.close();
|
|
5143
6157
|
this.state = "closed";
|
|
5144
|
-
return result;
|
|
5145
6158
|
}
|
|
5146
6159
|
};
|
|
5147
6160
|
}
|
|
@@ -5285,6 +6298,95 @@ CREATE TABLE IF NOT EXISTS _query_projection (
|
|
|
5285
6298
|
}
|
|
5286
6299
|
});
|
|
5287
6300
|
|
|
6301
|
+
// src/models/format2RowProjector.ts
|
|
6302
|
+
function quote2(identifier) {
|
|
6303
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
6304
|
+
}
|
|
6305
|
+
function binding(value) {
|
|
6306
|
+
if (value === void 0) return null;
|
|
6307
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
6308
|
+
return value;
|
|
6309
|
+
}
|
|
6310
|
+
function projectFormat2Row(sql, spec, docId, recordId, row) {
|
|
6311
|
+
const table = quote2(spec.table);
|
|
6312
|
+
if (row === null) {
|
|
6313
|
+
sql.exec(`DELETE FROM ${table} WHERE ${quote2("id")} = ?`, recordId);
|
|
6314
|
+
return "deleted";
|
|
6315
|
+
}
|
|
6316
|
+
const stringSetFields = new Set(spec.stringSets.map((set) => set.field));
|
|
6317
|
+
const columns = new Set(spec.columns);
|
|
6318
|
+
const data = {};
|
|
6319
|
+
for (const [key, value] of Object.entries(row)) {
|
|
6320
|
+
if (stringSetFields.has(key)) continue;
|
|
6321
|
+
if (value === void 0) continue;
|
|
6322
|
+
if (!columns.has(key) && key !== "id" && key !== "type") continue;
|
|
6323
|
+
data[key] = value;
|
|
6324
|
+
}
|
|
6325
|
+
data.id = recordId;
|
|
6326
|
+
data.type = spec.model;
|
|
6327
|
+
data._meta_doc_id = docId;
|
|
6328
|
+
data._meta_permission_hint = spec.permissionHint;
|
|
6329
|
+
const names = Object.keys(data);
|
|
6330
|
+
const updates = names.filter((name) => name !== "id").map((name) => `${quote2(name)}=excluded.${quote2(name)}`);
|
|
6331
|
+
sql.exec(
|
|
6332
|
+
`INSERT INTO ${table} (${names.map(quote2).join(", ")}) VALUES (${names.map(() => "?").join(", ")}) ON CONFLICT(${quote2("id")}) DO UPDATE SET ${updates.join(", ")}`,
|
|
6333
|
+
...names.map((name) => binding(data[name]))
|
|
6334
|
+
);
|
|
6335
|
+
const foreignKey = quote2(spec.foreignKey);
|
|
6336
|
+
for (const { field, table: junctionName } of spec.stringSets) {
|
|
6337
|
+
const junction = quote2(junctionName);
|
|
6338
|
+
const members = (Array.isArray(row[field]) ? row[field] : []).map(String);
|
|
6339
|
+
let current = [];
|
|
6340
|
+
try {
|
|
6341
|
+
current = Array.from(
|
|
6342
|
+
sql.exec(
|
|
6343
|
+
`SELECT ${quote2("value")} FROM ${junction} WHERE ${foreignKey} = ?`,
|
|
6344
|
+
recordId
|
|
6345
|
+
)
|
|
6346
|
+
).map((entry) => String(entry.value));
|
|
6347
|
+
} catch {
|
|
6348
|
+
current = [];
|
|
6349
|
+
}
|
|
6350
|
+
for (const member of current) {
|
|
6351
|
+
if (members.includes(member)) continue;
|
|
6352
|
+
sql.exec(
|
|
6353
|
+
`DELETE FROM ${junction} WHERE ${foreignKey} = ? AND ${quote2("value")} = ?`,
|
|
6354
|
+
recordId,
|
|
6355
|
+
member
|
|
6356
|
+
);
|
|
6357
|
+
}
|
|
6358
|
+
for (const member of members) {
|
|
6359
|
+
if (current.includes(member)) continue;
|
|
6360
|
+
sql.exec(
|
|
6361
|
+
`INSERT OR IGNORE INTO ${junction} (${quote2("id")}, ${foreignKey}, ${quote2(
|
|
6362
|
+
"value"
|
|
6363
|
+
)}) VALUES (?, ?, ?)`,
|
|
6364
|
+
`${recordId}_${member}`,
|
|
6365
|
+
recordId,
|
|
6366
|
+
member
|
|
6367
|
+
);
|
|
6368
|
+
}
|
|
6369
|
+
}
|
|
6370
|
+
return "projected";
|
|
6371
|
+
}
|
|
6372
|
+
function projectFormat2Rows(sql, spec, docId, ids, readRow) {
|
|
6373
|
+
let projected = 0;
|
|
6374
|
+
let deleted = 0;
|
|
6375
|
+
for (const id of ids) {
|
|
6376
|
+
if (projectFormat2Row(sql, spec, docId, id, readRow(id)) === "projected") {
|
|
6377
|
+
projected++;
|
|
6378
|
+
} else {
|
|
6379
|
+
deleted++;
|
|
6380
|
+
}
|
|
6381
|
+
}
|
|
6382
|
+
return { projected, deleted };
|
|
6383
|
+
}
|
|
6384
|
+
var init_format2RowProjector = __esm({
|
|
6385
|
+
"src/models/format2RowProjector.ts"() {
|
|
6386
|
+
"use strict";
|
|
6387
|
+
}
|
|
6388
|
+
});
|
|
6389
|
+
|
|
5288
6390
|
// src/models/format2StoreDispatcher.ts
|
|
5289
6391
|
function readFormat2StoreMarks(store) {
|
|
5290
6392
|
return {
|
|
@@ -5303,7 +6405,9 @@ function readFormat2StoreMarks(store) {
|
|
|
5303
6405
|
baseComplete: store.baseState().base?.complete ?? false,
|
|
5304
6406
|
// #3755 — read on the same round trip as the rest, because the claim path
|
|
5305
6407
|
// reads it synchronously off the mirror.
|
|
5306
|
-
deferredReplay: store.deferredReplay()
|
|
6408
|
+
deferredReplay: store.deferredReplay(),
|
|
6409
|
+
// #3782 — what the bind compares the loaded overlay with.
|
|
6410
|
+
foldedState: store.foldedState()
|
|
5307
6411
|
};
|
|
5308
6412
|
}
|
|
5309
6413
|
function dispatchFormat2StoreCommand(store, sql, command) {
|
|
@@ -5368,15 +6472,78 @@ function run(store, sql, command) {
|
|
|
5368
6472
|
return { seq, row };
|
|
5369
6473
|
}
|
|
5370
6474
|
case "applyRemoteBatch": {
|
|
6475
|
+
if (command.docState !== void 0 && command.force !== true) {
|
|
6476
|
+
const held = store.foldedState();
|
|
6477
|
+
if (held !== null && !vectorIsAtOrAhead(command.docState, held.vector)) {
|
|
6478
|
+
return {
|
|
6479
|
+
touched: [],
|
|
6480
|
+
projected: [],
|
|
6481
|
+
stamped: false,
|
|
6482
|
+
behind: true,
|
|
6483
|
+
foldedState: held
|
|
6484
|
+
};
|
|
6485
|
+
}
|
|
6486
|
+
}
|
|
5371
6487
|
const touched = [];
|
|
6488
|
+
const projected = [];
|
|
6489
|
+
const projectionFailed = [];
|
|
6490
|
+
let stamped = false;
|
|
5372
6491
|
sql.transaction(() => {
|
|
5373
6492
|
for (const { model, entry } of command.entries) {
|
|
5374
6493
|
if (!store.isHydrated(model)) continue;
|
|
5375
6494
|
store.applyRemote(model, entry);
|
|
5376
6495
|
touched.push({ model, id: entry.id });
|
|
5377
6496
|
}
|
|
6497
|
+
for (const spec of command.project ?? []) {
|
|
6498
|
+
const ids = Array.from(
|
|
6499
|
+
new Set(touched.filter((t) => t.model === spec.model).map((t) => t.id))
|
|
6500
|
+
);
|
|
6501
|
+
if (ids.length === 0) continue;
|
|
6502
|
+
sql.exec("SAVEPOINT f2_project");
|
|
6503
|
+
try {
|
|
6504
|
+
projectFormat2Rows(
|
|
6505
|
+
sql,
|
|
6506
|
+
spec,
|
|
6507
|
+
store.documentId(),
|
|
6508
|
+
ids,
|
|
6509
|
+
(id) => store.read(spec.model, id)
|
|
6510
|
+
);
|
|
6511
|
+
sql.exec("RELEASE f2_project");
|
|
6512
|
+
projected.push({ model: spec.model, count: ids.length });
|
|
6513
|
+
} catch (error) {
|
|
6514
|
+
sql.exec("ROLLBACK TO f2_project");
|
|
6515
|
+
sql.exec("RELEASE f2_project");
|
|
6516
|
+
forgetQueryProjection(sql, store.documentId(), spec.model);
|
|
6517
|
+
projectionFailed.push({
|
|
6518
|
+
model: spec.model,
|
|
6519
|
+
message: error instanceof Error ? error.message : String(error)
|
|
6520
|
+
});
|
|
6521
|
+
}
|
|
6522
|
+
}
|
|
6523
|
+
if (command.force === true) {
|
|
6524
|
+
store.clearFoldedState();
|
|
6525
|
+
Logger.warn(
|
|
6526
|
+
`[format2] ${store.documentId()}: folded from a document behind the store; the next open folds the whole overlay`
|
|
6527
|
+
);
|
|
6528
|
+
} else if (command.foldedState && command.docState) {
|
|
6529
|
+
stamped = store.noteFoldedState(command.foldedState, {
|
|
6530
|
+
docState: command.docState,
|
|
6531
|
+
whole: command.whole === true
|
|
6532
|
+
}) === "stamped";
|
|
6533
|
+
} else if (command.docState === void 0 && command.untracked !== true && touched.length > 0 && store.foldedState() !== null) {
|
|
6534
|
+
store.clearFoldedState();
|
|
6535
|
+
Logger.warn(
|
|
6536
|
+
`[format2] ${store.documentId()}: folded a batch from a tab that predates the folded-state mark; the next open folds the whole overlay`
|
|
6537
|
+
);
|
|
6538
|
+
}
|
|
5378
6539
|
});
|
|
5379
|
-
return {
|
|
6540
|
+
return {
|
|
6541
|
+
touched,
|
|
6542
|
+
projected,
|
|
6543
|
+
...projectionFailed.length > 0 ? { projectionFailed } : {},
|
|
6544
|
+
stamped,
|
|
6545
|
+
foldedState: store.foldedState()
|
|
6546
|
+
};
|
|
5380
6547
|
}
|
|
5381
6548
|
case "applyChunk":
|
|
5382
6549
|
return {
|
|
@@ -5427,6 +6594,23 @@ function run(store, sql, command) {
|
|
|
5427
6594
|
forgetQueryProjection(sql, store.documentId());
|
|
5428
6595
|
return void 0;
|
|
5429
6596
|
}
|
|
6597
|
+
case "projectRows": {
|
|
6598
|
+
const docId = store.documentId();
|
|
6599
|
+
try {
|
|
6600
|
+
return sql.transaction(
|
|
6601
|
+
() => projectFormat2Rows(
|
|
6602
|
+
sql,
|
|
6603
|
+
command.spec,
|
|
6604
|
+
docId,
|
|
6605
|
+
command.ids,
|
|
6606
|
+
(id) => store.read(command.model, id)
|
|
6607
|
+
)
|
|
6608
|
+
);
|
|
6609
|
+
} catch (error) {
|
|
6610
|
+
forgetQueryProjection(sql, docId, command.model);
|
|
6611
|
+
throw error;
|
|
6612
|
+
}
|
|
6613
|
+
}
|
|
5430
6614
|
case "purge": {
|
|
5431
6615
|
const docId = store.documentId();
|
|
5432
6616
|
return sql.transaction(() => {
|
|
@@ -5439,7 +6623,11 @@ function run(store, sql, command) {
|
|
|
5439
6623
|
purged: true,
|
|
5440
6624
|
...report.nothingStored ? { nothingStored: true } : {},
|
|
5441
6625
|
pendingOps: report.pendingOps,
|
|
5442
|
-
remainingDocuments: report.remainingDocuments
|
|
6626
|
+
remainingDocuments: report.remainingDocuments,
|
|
6627
|
+
// Which pre-amendment pools this store still holds adoption evidence
|
|
6628
|
+
// about: what keeps a last-document eviction from removing the
|
|
6629
|
+
// directory that ledger lives in (finding 3760-REVIEW-02).
|
|
6630
|
+
...report.legacyPools?.length ? { legacyPools: report.legacyPools } : {}
|
|
5443
6631
|
};
|
|
5444
6632
|
});
|
|
5445
6633
|
}
|
|
@@ -5466,8 +6654,11 @@ function run(store, sql, command) {
|
|
|
5466
6654
|
var init_format2StoreDispatcher = __esm({
|
|
5467
6655
|
"src/models/format2StoreDispatcher.ts"() {
|
|
5468
6656
|
"use strict";
|
|
6657
|
+
init_logger();
|
|
5469
6658
|
init_format2RecordStore();
|
|
5470
6659
|
init_format2QueryProjection();
|
|
6660
|
+
init_format2RowProjector();
|
|
6661
|
+
init_format2FoldedState();
|
|
5471
6662
|
}
|
|
5472
6663
|
});
|
|
5473
6664
|
|
|
@@ -5644,32 +6835,13 @@ var init_format2StoreHost = __esm({
|
|
|
5644
6835
|
this.log.length = 0;
|
|
5645
6836
|
}
|
|
5646
6837
|
/**
|
|
5647
|
-
* The only holder, always: an in-process host owns its executor.
|
|
5648
|
-
*
|
|
5649
|
-
* the
|
|
5650
|
-
* before the disconnect gate moved (#3431). Node's observable result is
|
|
5651
|
-
* unchanged.
|
|
6838
|
+
* The only holder, always: an in-process host owns its executor. A close
|
|
6839
|
+
* removes nothing (#3756) — the query tables and projection marks stay in
|
|
6840
|
+
* the engine's database for the next open, as on a worker.
|
|
5652
6841
|
*/
|
|
5653
|
-
async close(
|
|
5654
|
-
let wiped = false;
|
|
5655
|
-
if (options.wipeIfLast && this.wipe) {
|
|
5656
|
-
await this.wipe(options.wipeIfLast.models);
|
|
5657
|
-
wiped = true;
|
|
5658
|
-
}
|
|
6842
|
+
async close() {
|
|
5659
6843
|
this.closed = true;
|
|
5660
|
-
return { wiped };
|
|
5661
|
-
}
|
|
5662
|
-
/**
|
|
5663
|
-
* Remove this document from the named models' query tables on close.
|
|
5664
|
-
*
|
|
5665
|
-
* Injected because the query tables belong to the ENGINE, which an
|
|
5666
|
-
* in-process store host has no handle on; over a worker host the same
|
|
5667
|
-
* instruction travels as the worker's own `release` command.
|
|
5668
|
-
*/
|
|
5669
|
-
onWipe(wipe) {
|
|
5670
|
-
this.wipe = wipe;
|
|
5671
6844
|
}
|
|
5672
|
-
wipe = null;
|
|
5673
6845
|
async holders() {
|
|
5674
6846
|
return 1;
|
|
5675
6847
|
}
|
|
@@ -5752,7 +6924,7 @@ var init_format2StoreHost = __esm({
|
|
|
5752
6924
|
return null;
|
|
5753
6925
|
}
|
|
5754
6926
|
async close() {
|
|
5755
|
-
if (this.disposed) return
|
|
6927
|
+
if (this.disposed) return;
|
|
5756
6928
|
this.disposed = true;
|
|
5757
6929
|
this.closed = true;
|
|
5758
6930
|
this.stopListening();
|
|
@@ -5765,7 +6937,6 @@ var init_format2StoreHost = __esm({
|
|
|
5765
6937
|
this.port.close?.();
|
|
5766
6938
|
} catch {
|
|
5767
6939
|
}
|
|
5768
|
-
return { wiped: false };
|
|
5769
6940
|
}
|
|
5770
6941
|
};
|
|
5771
6942
|
}
|
|
@@ -7100,10 +8271,19 @@ var init_IncludeResolver = __esm({
|
|
|
7100
8271
|
* for everything, which is every Node engine.
|
|
7101
8272
|
*/
|
|
7102
8273
|
resolveEngine;
|
|
7103
|
-
|
|
8274
|
+
/**
|
|
8275
|
+
* The documents a related row may come from (#3756): `_meta_doc_id IN
|
|
8276
|
+
* (...)` on every related-model statement, `1 = 0` for an empty list. A
|
|
8277
|
+
* closed large document keeps its query rows for its next open, so without
|
|
8278
|
+
* this an include would answer rows the parent statement already excludes.
|
|
8279
|
+
* Absent, related rows are not restricted by document.
|
|
8280
|
+
*/
|
|
8281
|
+
documents;
|
|
8282
|
+
constructor(db, registry, resolveEngine, documents) {
|
|
7104
8283
|
this.db = db;
|
|
7105
8284
|
this.registry = registry ?? ModelRegistry.getInstance();
|
|
7106
8285
|
this.resolveEngine = resolveEngine ?? (() => this.db);
|
|
8286
|
+
this.documents = documents ? [...documents] : null;
|
|
7107
8287
|
}
|
|
7108
8288
|
/** The engine `modelName`'s rows are read from. */
|
|
7109
8289
|
async engineFor(modelName) {
|
|
@@ -7321,6 +8501,11 @@ var init_IncludeResolver = __esm({
|
|
|
7321
8501
|
filterParams = translated.params;
|
|
7322
8502
|
}
|
|
7323
8503
|
}
|
|
8504
|
+
const documentCondition = this.documents === null ? null : translator.documentCondition([...this.documents]);
|
|
8505
|
+
if (documentCondition) {
|
|
8506
|
+
filterClause += ` AND ${documentCondition.sql}`;
|
|
8507
|
+
filterParams = [...filterParams, ...documentCondition.params];
|
|
8508
|
+
}
|
|
7324
8509
|
const reservedParams = filterParams.length + 1;
|
|
7325
8510
|
const chunkSize = Math.max(1, 100 - reservedParams);
|
|
7326
8511
|
const db = await this.engineFor(spec.model);
|
|
@@ -7367,7 +8552,8 @@ var init_IncludeResolver = __esm({
|
|
|
7367
8552
|
const translated = translator.translateFilter(extraFilter);
|
|
7368
8553
|
filterParamCount = translated.params.length;
|
|
7369
8554
|
}
|
|
7370
|
-
const
|
|
8555
|
+
const documentParamCount = this.documents?.length ?? 0;
|
|
8556
|
+
const chunkSize = Math.max(1, 100 - filterParamCount - documentParamCount);
|
|
7371
8557
|
const db = await this.engineFor(model);
|
|
7372
8558
|
const allResults = [];
|
|
7373
8559
|
for (let i = 0; i < values.length; i += chunkSize) {
|
|
@@ -7379,6 +8565,7 @@ var init_IncludeResolver = __esm({
|
|
|
7379
8565
|
const options = {};
|
|
7380
8566
|
if (extraOptions?.sort) options.sort = extraOptions.sort;
|
|
7381
8567
|
if (extraOptions?.projection) options.projection = extraOptions.projection;
|
|
8568
|
+
if (this.documents !== null) options.documents = [...this.documents];
|
|
7382
8569
|
const { sql, params } = translator.translateFind(filter, options);
|
|
7383
8570
|
const rows = await db.query(sql, params);
|
|
7384
8571
|
allResults.push(...rows);
|
|
@@ -7499,11 +8686,11 @@ function reindexFailure(modelConstructor, modelName, recordId, docId, error) {
|
|
|
7499
8686
|
failure.cause = error;
|
|
7500
8687
|
return failure;
|
|
7501
8688
|
}
|
|
7502
|
-
var
|
|
8689
|
+
var Y8, import_ulid2, FORMAT2_ROW_PAGE, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel2;
|
|
7503
8690
|
var init_BaseModel = __esm({
|
|
7504
8691
|
"src/models/BaseModel.ts"() {
|
|
7505
8692
|
"use strict";
|
|
7506
|
-
|
|
8693
|
+
Y8 = __toESM(require("yjs"), 1);
|
|
7507
8694
|
import_ulid2 = require("ulid");
|
|
7508
8695
|
init_StringSet();
|
|
7509
8696
|
init_documentTypes();
|
|
@@ -8415,7 +9602,7 @@ var init_BaseModel = __esm({
|
|
|
8415
9602
|
* record's key from the map — cannot drift apart (#3252).
|
|
8416
9603
|
*/
|
|
8417
9604
|
static _stringSetMembersFromStored(stored) {
|
|
8418
|
-
if (stored instanceof
|
|
9605
|
+
if (stored instanceof Y8.Map) {
|
|
8419
9606
|
return Array.from(stored.keys());
|
|
8420
9607
|
}
|
|
8421
9608
|
if (Array.isArray(stored)) {
|
|
@@ -8583,7 +9770,7 @@ var init_BaseModel = __esm({
|
|
|
8583
9770
|
}
|
|
8584
9771
|
const extractStringSetValues = (value) => {
|
|
8585
9772
|
if (!value) return [];
|
|
8586
|
-
if (value instanceof
|
|
9773
|
+
if (value instanceof Y8.Map) {
|
|
8587
9774
|
return Array.from(value.entries()).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
|
|
8588
9775
|
}
|
|
8589
9776
|
if (Array.isArray(value)) {
|
|
@@ -8600,7 +9787,7 @@ var init_BaseModel = __esm({
|
|
|
8600
9787
|
if (!recordId) continue;
|
|
8601
9788
|
let itemData;
|
|
8602
9789
|
const stringSetValuesByField = {};
|
|
8603
|
-
if (recordData instanceof
|
|
9790
|
+
if (recordData instanceof Y8.Map) {
|
|
8604
9791
|
itemData = {};
|
|
8605
9792
|
const unknownFields = [];
|
|
8606
9793
|
for (const [fieldKey, value] of recordData.entries()) {
|
|
@@ -8719,7 +9906,7 @@ var init_BaseModel = __esm({
|
|
|
8719
9906
|
const buildUniqueKey2 = (recordData, fields) => buildUniqueKey(recordData, fields);
|
|
8720
9907
|
const extractItemData = (key, recordData) => {
|
|
8721
9908
|
let itemData;
|
|
8722
|
-
if (recordData instanceof
|
|
9909
|
+
if (recordData instanceof Y8.Map) {
|
|
8723
9910
|
itemData = {};
|
|
8724
9911
|
const unknownFields = [];
|
|
8725
9912
|
for (const [fieldKey, value] of recordData.entries()) {
|
|
@@ -8851,7 +10038,7 @@ var init_BaseModel = __esm({
|
|
|
8851
10038
|
if (!recordData || !key) continue;
|
|
8852
10039
|
const itemData = extractItemData(key, recordData);
|
|
8853
10040
|
if (!itemData) continue;
|
|
8854
|
-
if (change.action === "add" && recordData instanceof
|
|
10041
|
+
if (change.action === "add" && recordData instanceof Y8.Map) {
|
|
8855
10042
|
Logger.verbose(
|
|
8856
10043
|
`[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
|
|
8857
10044
|
);
|
|
@@ -9013,7 +10200,7 @@ var init_BaseModel = __esm({
|
|
|
9013
10200
|
`[${this.name}] Setting up observers on existing nested YMaps for ${modelName}/${docId}...`
|
|
9014
10201
|
);
|
|
9015
10202
|
for (const [recordId, recordData] of documentYMap.entries()) {
|
|
9016
|
-
if (recordData instanceof
|
|
10203
|
+
if (recordData instanceof Y8.Map) {
|
|
9017
10204
|
Logger.verbose(
|
|
9018
10205
|
`[${this.name}] Setting up observer on existing nested YMap for record ${recordId} in document ${docId}`
|
|
9019
10206
|
);
|
|
@@ -9077,7 +10264,7 @@ var init_BaseModel = __esm({
|
|
|
9077
10264
|
ctx.notifyListeners(modelName);
|
|
9078
10265
|
return;
|
|
9079
10266
|
}
|
|
9080
|
-
const entries = recordData instanceof
|
|
10267
|
+
const entries = recordData instanceof Y8.Map ? Array.from(recordData.entries()) : Object.entries(recordData);
|
|
9081
10268
|
const rowData = {};
|
|
9082
10269
|
for (const [fieldKey, value] of entries) {
|
|
9083
10270
|
const fieldOptions = schema.fields?.get(fieldKey);
|
|
@@ -9426,7 +10613,7 @@ var init_BaseModel = __esm({
|
|
|
9426
10613
|
const ownerId = constraintMap.get(uniqueKey);
|
|
9427
10614
|
if (!ownerId) return null;
|
|
9428
10615
|
const recordYMap = modelYMap?.get(ownerId);
|
|
9429
|
-
if (!(recordYMap instanceof
|
|
10616
|
+
if (!(recordYMap instanceof Y8.Map)) return null;
|
|
9430
10617
|
const fields = this.getSchema?.()?.fields;
|
|
9431
10618
|
const heldKey = this._buildKeyFromValues(
|
|
9432
10619
|
constraint.fields,
|
|
@@ -9600,16 +10787,18 @@ var init_BaseModel = __esm({
|
|
|
9600
10787
|
yDoc,
|
|
9601
10788
|
targetYMap
|
|
9602
10789
|
);
|
|
9603
|
-
await
|
|
9604
|
-
|
|
9605
|
-
|
|
9606
|
-
|
|
9607
|
-
|
|
9608
|
-
|
|
9609
|
-
|
|
9610
|
-
|
|
9611
|
-
|
|
9612
|
-
|
|
10790
|
+
await target.store.trackLocalWrite(
|
|
10791
|
+
() => this.saveFormat2({
|
|
10792
|
+
store: target.store,
|
|
10793
|
+
schema,
|
|
10794
|
+
modelName,
|
|
10795
|
+
docId: targetDocId,
|
|
10796
|
+
permissionHint,
|
|
10797
|
+
yDoc: target.yDoc,
|
|
10798
|
+
modelYMap: target.modelYMap,
|
|
10799
|
+
upsertOnField: options?.upsertOn ?? null
|
|
10800
|
+
})
|
|
10801
|
+
);
|
|
9613
10802
|
this.clearLocalChanges();
|
|
9614
10803
|
return;
|
|
9615
10804
|
}
|
|
@@ -9708,7 +10897,7 @@ var init_BaseModel = __esm({
|
|
|
9708
10897
|
Logger.debug(`[${modelName}.save] isUpdate: ${isUpdate}`);
|
|
9709
10898
|
Logger.debug(`[${modelName}.save] recordYMap exists: ${!!recordYMap}`);
|
|
9710
10899
|
if (!recordYMap) {
|
|
9711
|
-
recordYMap = new
|
|
10900
|
+
recordYMap = new Y8.Map();
|
|
9712
10901
|
Logger.verbose(
|
|
9713
10902
|
`[${modelName}] Creating new nested YMap for record ${this.id} in document ${targetDocId || "legacy"}`
|
|
9714
10903
|
);
|
|
@@ -10174,6 +11363,7 @@ var init_BaseModel = __esm({
|
|
|
10174
11363
|
stringSetChanges,
|
|
10175
11364
|
stringSetFieldNames
|
|
10176
11365
|
});
|
|
11366
|
+
this._format2Row = store.entryFor(modelName, this.id);
|
|
10177
11367
|
await ops.commitLocalWrite(modelName, mutation, {
|
|
10178
11368
|
model: modelName,
|
|
10179
11369
|
recordId: this.id,
|
|
@@ -10219,11 +11409,34 @@ var init_BaseModel = __esm({
|
|
|
10219
11409
|
applyOverlayMutation(modelYMap, mutation);
|
|
10220
11410
|
}, FORMAT2_LOCAL_WRITE_ORIGIN);
|
|
10221
11411
|
syncModelMeta(yDoc, modelName, schema);
|
|
11412
|
+
store.noteLocalPublish(yDoc);
|
|
10222
11413
|
return true;
|
|
10223
11414
|
});
|
|
10224
11415
|
void planned;
|
|
10225
11416
|
this._format2Row = store.entryFor(modelName, this.id);
|
|
10226
|
-
await modelConstructor.
|
|
11417
|
+
await modelConstructor.projectFormat2LocalWrite(store, docId, modelName, this.id);
|
|
11418
|
+
}
|
|
11419
|
+
/**
|
|
11420
|
+
* Project the row a local write just committed, withdrawing the model's
|
|
11421
|
+
* projection mark if that fails (#3756). The mark outlives a close, so a
|
|
11422
|
+
* projection that failed under it would leave `query()` disagreeing with
|
|
11423
|
+
* `find()` until something rebuilt the model; dropping the mark makes the
|
|
11424
|
+
* next bind that something. The failure still reaches the caller.
|
|
11425
|
+
*/
|
|
11426
|
+
static async projectFormat2LocalWrite(store, docId, modelName, recordId) {
|
|
11427
|
+
try {
|
|
11428
|
+
await this.projectFormat2Record(docId, recordId);
|
|
11429
|
+
} catch (error) {
|
|
11430
|
+
try {
|
|
11431
|
+
await store.forgetQueryProjection(modelName);
|
|
11432
|
+
} catch (markError) {
|
|
11433
|
+
Logger.error(
|
|
11434
|
+
`[format2] ${docId}: failed to withdraw the query-projection mark of ${modelName} after a local write's projection failed:`,
|
|
11435
|
+
markError
|
|
11436
|
+
);
|
|
11437
|
+
}
|
|
11438
|
+
throw error;
|
|
11439
|
+
}
|
|
10227
11440
|
}
|
|
10228
11441
|
/**
|
|
10229
11442
|
* Mirror one merged-view row into the engine's per-model query tables, so
|
|
@@ -10241,67 +11454,60 @@ var init_BaseModel = __esm({
|
|
|
10241
11454
|
* updates that arrive from the sync layer.
|
|
10242
11455
|
*/
|
|
10243
11456
|
static async projectFormat2Record(docId, recordId, options = {}) {
|
|
11457
|
+
const modelConstructor = this;
|
|
11458
|
+
const projected = await modelConstructor.projectFormat2Ids(docId, [recordId]);
|
|
11459
|
+
if (projected && options.notify !== false) modelConstructor.notifyListeners();
|
|
11460
|
+
}
|
|
11461
|
+
/**
|
|
11462
|
+
* What this model's query tables look like, as data a store host can
|
|
11463
|
+
* project with. The one place the table, junction and column names
|
|
11464
|
+
* are decided; `null` when there is nothing to project into — no engine
|
|
11465
|
+
* behind this ORM instance, or no schema.
|
|
11466
|
+
*/
|
|
11467
|
+
static format2ProjectionSpec(docId) {
|
|
10244
11468
|
const modelConstructor = this;
|
|
10245
11469
|
const schema = modelConstructor.getSchema?.();
|
|
10246
11470
|
const modelName = schema?.options?.name;
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
const db = await modelConstructor.queryEngineFor([docId], modelName);
|
|
10251
|
-
const permissionHint = modelConstructor.connectedDocuments.get(docId)?.permissionHint ?? "read-write";
|
|
10252
|
-
const stringSetFieldNames = /* @__PURE__ */ new Set();
|
|
11471
|
+
if (!modelConstructor.dbInstance || !modelName) return null;
|
|
11472
|
+
const columns = [];
|
|
11473
|
+
const stringSets = [];
|
|
10253
11474
|
for (const [fieldKey, fieldOptions] of schema.fields.entries()) {
|
|
10254
11475
|
if (fieldOptions?.type === "stringset") {
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
await db.delete(modelName, recordId);
|
|
10262
|
-
if (notify) modelConstructor.notifyListeners();
|
|
10263
|
-
return;
|
|
10264
|
-
}
|
|
10265
|
-
const scalars = {};
|
|
10266
|
-
for (const [key, value] of Object.entries(row)) {
|
|
10267
|
-
if (stringSetFieldNames.has(key)) continue;
|
|
10268
|
-
if (value === void 0) continue;
|
|
10269
|
-
if (!schema.fields.has(key) && key !== "id" && key !== "type") continue;
|
|
10270
|
-
scalars[key] = value;
|
|
10271
|
-
}
|
|
10272
|
-
await db.insert(modelName, {
|
|
10273
|
-
...scalars,
|
|
10274
|
-
id: recordId,
|
|
10275
|
-
type: modelName,
|
|
10276
|
-
_meta_doc_id: docId,
|
|
10277
|
-
_meta_permission_hint: permissionHint
|
|
10278
|
-
});
|
|
10279
|
-
for (const field of stringSetFieldNames) {
|
|
10280
|
-
const members = (Array.isArray(row[field]) ? row[field] : []).map(String);
|
|
10281
|
-
let current = [];
|
|
10282
|
-
try {
|
|
10283
|
-
const junction = quoteIdentifier(
|
|
10284
|
-
modelConstructor.getDatabaseJunctionTableName(modelName, field)
|
|
10285
|
-
);
|
|
10286
|
-
const foreignKey = quoteIdentifier(`${modelName.toLowerCase()}_id`);
|
|
10287
|
-
const rows = await db.query(
|
|
10288
|
-
`SELECT value FROM ${junction} WHERE ${foreignKey} = ?`,
|
|
10289
|
-
[recordId]
|
|
10290
|
-
);
|
|
10291
|
-
current = rows.map((entry) => String(entry.value));
|
|
10292
|
-
} catch {
|
|
10293
|
-
current = [];
|
|
10294
|
-
}
|
|
10295
|
-
const removals = current.filter((member) => !members.includes(member));
|
|
10296
|
-
if (removals.length > 0) {
|
|
10297
|
-
await db.removeStringSetValues(modelName, field, recordId, removals);
|
|
10298
|
-
}
|
|
10299
|
-
const additions = members.filter((member) => !current.includes(member));
|
|
10300
|
-
if (additions.length > 0) {
|
|
10301
|
-
await db.insertStringSetValues(modelName, field, recordId, additions);
|
|
11476
|
+
stringSets.push({
|
|
11477
|
+
field: fieldKey,
|
|
11478
|
+
table: modelConstructor.getDatabaseJunctionTableName(modelName, fieldKey)
|
|
11479
|
+
});
|
|
11480
|
+
} else {
|
|
11481
|
+
columns.push(fieldKey);
|
|
10302
11482
|
}
|
|
10303
11483
|
}
|
|
10304
|
-
|
|
11484
|
+
return {
|
|
11485
|
+
model: modelName,
|
|
11486
|
+
table: modelConstructor.getDatabaseTableName(modelName),
|
|
11487
|
+
foreignKey: `${modelName.toLowerCase()}_id`,
|
|
11488
|
+
columns,
|
|
11489
|
+
stringSets,
|
|
11490
|
+
permissionHint: modelConstructor.connectedDocuments.get(docId)?.permissionHint ?? "read-write"
|
|
11491
|
+
};
|
|
11492
|
+
}
|
|
11493
|
+
/**
|
|
11494
|
+
* Tell this model's listeners that folded overlay changes reached its rows
|
|
11495
|
+
* and its query tables. Called once per model per fold batch by the
|
|
11496
|
+
* document binding; the projection itself ran on the store's host.
|
|
11497
|
+
* @internal
|
|
11498
|
+
*/
|
|
11499
|
+
static notifyFormat2Folded() {
|
|
11500
|
+
this.notifyListeners();
|
|
11501
|
+
}
|
|
11502
|
+
/** Project `recordIds` through the store's host; whether anything was sent. */
|
|
11503
|
+
static async projectFormat2Ids(docId, recordIds) {
|
|
11504
|
+
const modelConstructor = this;
|
|
11505
|
+
const store = modelConstructor.getFormat2StoreAsync(docId);
|
|
11506
|
+
const spec = modelConstructor.format2ProjectionSpec(docId);
|
|
11507
|
+
if (!store || !spec) return false;
|
|
11508
|
+
if (!store.isHydrated(spec.model)) return false;
|
|
11509
|
+
await store.projectRows(spec.model, recordIds, spec);
|
|
11510
|
+
return true;
|
|
10305
11511
|
}
|
|
10306
11512
|
/**
|
|
10307
11513
|
* Remove a PAGE of records from the query tables, without consulting the
|
|
@@ -10333,12 +11539,9 @@ var init_BaseModel = __esm({
|
|
|
10333
11539
|
static async projectFormat2Records(docId, recordIds) {
|
|
10334
11540
|
if (recordIds.length === 0) return;
|
|
10335
11541
|
const modelConstructor = this;
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
notify: false
|
|
10339
|
-
});
|
|
11542
|
+
if (await modelConstructor.projectFormat2Ids(docId, recordIds)) {
|
|
11543
|
+
modelConstructor.notifyListeners();
|
|
10340
11544
|
}
|
|
10341
|
-
modelConstructor.notifyListeners();
|
|
10342
11545
|
}
|
|
10343
11546
|
async delete() {
|
|
10344
11547
|
const modelConstructor = this.constructor;
|
|
@@ -10379,14 +11582,16 @@ var init_BaseModel = __esm({
|
|
|
10379
11582
|
sourceYDoc,
|
|
10380
11583
|
modelConstructor.documentYMaps.get(`${docId}_${modelName}`) ?? sourceYDoc.getMap(modelName)
|
|
10381
11584
|
);
|
|
10382
|
-
await
|
|
10383
|
-
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
11585
|
+
await target.store.trackLocalWrite(
|
|
11586
|
+
() => this.deleteFormat2({
|
|
11587
|
+
store: target.store,
|
|
11588
|
+
schema,
|
|
11589
|
+
modelName,
|
|
11590
|
+
docId,
|
|
11591
|
+
yDoc: target.yDoc,
|
|
11592
|
+
modelYMap: target.modelYMap
|
|
11593
|
+
})
|
|
11594
|
+
);
|
|
10390
11595
|
return;
|
|
10391
11596
|
}
|
|
10392
11597
|
if (!sourceYMap) {
|
|
@@ -10483,9 +11688,15 @@ var init_BaseModel = __esm({
|
|
|
10483
11688
|
}
|
|
10484
11689
|
applyOverlayMutation(modelYMap, mutation);
|
|
10485
11690
|
}, FORMAT2_LOCAL_WRITE_ORIGIN);
|
|
11691
|
+
store.noteLocalPublish(yDoc);
|
|
10486
11692
|
return existing;
|
|
10487
11693
|
});
|
|
10488
|
-
await modelConstructor.
|
|
11694
|
+
await modelConstructor.projectFormat2LocalWrite(
|
|
11695
|
+
store,
|
|
11696
|
+
ctx.docId,
|
|
11697
|
+
modelName,
|
|
11698
|
+
this.id
|
|
11699
|
+
);
|
|
10489
11700
|
}
|
|
10490
11701
|
// Get current state combining local changes with Yjs data
|
|
10491
11702
|
getCurrentJSState() {
|
|
@@ -10665,13 +11876,16 @@ var init_BaseModel = __esm({
|
|
|
10665
11876
|
limit: requestedLimit + 1
|
|
10666
11877
|
};
|
|
10667
11878
|
}
|
|
11879
|
+
const scope = this.documentsInScope(
|
|
11880
|
+
options?.documents
|
|
11881
|
+
);
|
|
11882
|
+
if (options?.documents == null) {
|
|
11883
|
+
translatorOptions = { ...translatorOptions || {}, documents: scope };
|
|
11884
|
+
}
|
|
10668
11885
|
const translator = new DocumentQueryTranslator(modelName, schema.fields);
|
|
10669
11886
|
const translatedQuery = translator.translateFind(filter, translatorOptions);
|
|
10670
11887
|
Logger.verbose(`[${modelName}] Translated SQL:`, translatedQuery.sql);
|
|
10671
11888
|
Logger.verbose(`[${modelName}] SQL params:`, translatedQuery.params);
|
|
10672
|
-
const scope = this.documentsInScope(
|
|
10673
|
-
options?.documents
|
|
10674
|
-
);
|
|
10675
11889
|
const queryEngine = await this.queryEngineFor(
|
|
10676
11890
|
scope,
|
|
10677
11891
|
modelName
|
|
@@ -10691,7 +11905,8 @@ var init_BaseModel = __esm({
|
|
|
10691
11905
|
const resolver = new IncludeResolver2(
|
|
10692
11906
|
queryEngine,
|
|
10693
11907
|
ormContext.modelRegistry ?? void 0,
|
|
10694
|
-
(relatedModel) => this.queryEngineFor(scope, relatedModel)
|
|
11908
|
+
(relatedModel) => this.queryEngineFor(scope, relatedModel),
|
|
11909
|
+
this.documentsInScope()
|
|
10695
11910
|
);
|
|
10696
11911
|
await resolver.resolve(results, options.include, 0, modelName);
|
|
10697
11912
|
}
|
|
@@ -10842,7 +12057,9 @@ var init_BaseModel = __esm({
|
|
|
10842
12057
|
);
|
|
10843
12058
|
}
|
|
10844
12059
|
this.assertFormat2Readable(
|
|
10845
|
-
this.format2DocumentsInScope(
|
|
12060
|
+
this.format2DocumentsInScope(
|
|
12061
|
+
options.documents
|
|
12062
|
+
)
|
|
10846
12063
|
);
|
|
10847
12064
|
const schema = this.getSchema?.();
|
|
10848
12065
|
if (!schema || !schema.options?.name) {
|
|
@@ -10879,10 +12096,16 @@ var init_BaseModel = __esm({
|
|
|
10879
12096
|
}
|
|
10880
12097
|
}
|
|
10881
12098
|
}
|
|
12099
|
+
const aggregateScope = this.documentsInScope(options.documents);
|
|
12100
|
+
const connected = new Set(
|
|
12101
|
+
this.documentsInScope()
|
|
12102
|
+
);
|
|
12103
|
+
const sqlScope = options.documents == null ? aggregateScope : (typeof options.documents === "string" ? [options.documents] : options.documents).filter((docId) => connected.has(docId));
|
|
10882
12104
|
const aggregationQuery = this.buildAggregationQuery(
|
|
10883
12105
|
options,
|
|
10884
12106
|
schema,
|
|
10885
|
-
modelName
|
|
12107
|
+
modelName,
|
|
12108
|
+
sqlScope
|
|
10886
12109
|
);
|
|
10887
12110
|
Logger.verbose(
|
|
10888
12111
|
`[${modelName}] Generated aggregation SQL:`,
|
|
@@ -10893,7 +12116,7 @@ var init_BaseModel = __esm({
|
|
|
10893
12116
|
aggregationQuery.params
|
|
10894
12117
|
);
|
|
10895
12118
|
const aggregateEngine = await this.queryEngineFor(
|
|
10896
|
-
|
|
12119
|
+
aggregateScope,
|
|
10897
12120
|
modelName
|
|
10898
12121
|
);
|
|
10899
12122
|
const results = await aggregateEngine.query(
|
|
@@ -10929,7 +12152,7 @@ var init_BaseModel = __esm({
|
|
|
10929
12152
|
/**
|
|
10930
12153
|
* Build SQL query for structured aggregation
|
|
10931
12154
|
*/
|
|
10932
|
-
static buildAggregationQuery(options, schema, modelName) {
|
|
12155
|
+
static buildAggregationQuery(options, schema, modelName, documents) {
|
|
10933
12156
|
const translator = new DocumentQueryTranslator(modelName, schema.fields);
|
|
10934
12157
|
const regularGroupBy = [];
|
|
10935
12158
|
const stringSetFacets = [];
|
|
@@ -10951,7 +12174,8 @@ var init_BaseModel = __esm({
|
|
|
10951
12174
|
stringSetFacets,
|
|
10952
12175
|
options,
|
|
10953
12176
|
translator,
|
|
10954
|
-
modelName
|
|
12177
|
+
modelName,
|
|
12178
|
+
documents
|
|
10955
12179
|
);
|
|
10956
12180
|
} else if (stringSetMemberships.length > 0 || regularGroupBy.length > 0) {
|
|
10957
12181
|
return this.buildRegularAggregationQuery(
|
|
@@ -10959,12 +12183,26 @@ var init_BaseModel = __esm({
|
|
|
10959
12183
|
stringSetMemberships,
|
|
10960
12184
|
options,
|
|
10961
12185
|
translator,
|
|
10962
|
-
modelName
|
|
12186
|
+
modelName,
|
|
12187
|
+
documents
|
|
10963
12188
|
);
|
|
10964
12189
|
} else {
|
|
10965
12190
|
throw new Error("Invalid aggregation configuration");
|
|
10966
12191
|
}
|
|
10967
12192
|
}
|
|
12193
|
+
/**
|
|
12194
|
+
* Append the document condition to an aggregation's WHERE (#3756): `AND`
|
|
12195
|
+
* after a filter, `WHERE` without one, nothing when `documents` is absent.
|
|
12196
|
+
*/
|
|
12197
|
+
static withDocumentCondition(sql, params, translator, documents) {
|
|
12198
|
+
const condition = translator.documentCondition(documents);
|
|
12199
|
+
if (!condition) return { sql, params };
|
|
12200
|
+
const joiner = /\sWHERE\s/i.test(sql) ? "AND" : "WHERE";
|
|
12201
|
+
return {
|
|
12202
|
+
sql: `${sql} ${joiner} ${condition.sql}`,
|
|
12203
|
+
params: [...params, ...condition.params]
|
|
12204
|
+
};
|
|
12205
|
+
}
|
|
10968
12206
|
/**
|
|
10969
12207
|
* Get the proper database table name (should match database engine naming)
|
|
10970
12208
|
*/
|
|
@@ -10983,7 +12221,7 @@ var init_BaseModel = __esm({
|
|
|
10983
12221
|
/**
|
|
10984
12222
|
* Build query for StringSet facet counts
|
|
10985
12223
|
*/
|
|
10986
|
-
static buildStringSetFacetQuery(stringSetFields, options, translator, modelName) {
|
|
12224
|
+
static buildStringSetFacetQuery(stringSetFields, options, translator, modelName, documents) {
|
|
10987
12225
|
if (stringSetFields.length > 1) {
|
|
10988
12226
|
throw new Error(
|
|
10989
12227
|
"Multiple StringSet facet fields not supported in single aggregation"
|
|
@@ -11021,6 +12259,12 @@ var init_BaseModel = __esm({
|
|
|
11021
12259
|
params = whereClause.params;
|
|
11022
12260
|
}
|
|
11023
12261
|
}
|
|
12262
|
+
({ sql, params } = this.withDocumentCondition(
|
|
12263
|
+
sql,
|
|
12264
|
+
params,
|
|
12265
|
+
translator,
|
|
12266
|
+
documents
|
|
12267
|
+
));
|
|
11024
12268
|
sql += ` GROUP BY ${quotedJunctionTable}.${quotedValueColumn}`;
|
|
11025
12269
|
if (options.sort) {
|
|
11026
12270
|
const sortField = options.sort.field === "count" ? "COUNT(*)" : quoteIdentifier(options.sort.field);
|
|
@@ -11035,7 +12279,7 @@ var init_BaseModel = __esm({
|
|
|
11035
12279
|
/**
|
|
11036
12280
|
* Build query for regular field aggregation
|
|
11037
12281
|
*/
|
|
11038
|
-
static buildRegularAggregationQuery(regularGroupBy, stringSetMemberships, options, translator, modelName) {
|
|
12282
|
+
static buildRegularAggregationQuery(regularGroupBy, stringSetMemberships, options, translator, modelName, documents) {
|
|
11039
12283
|
const mainTable = this.getDatabaseTableName(modelName);
|
|
11040
12284
|
const quotedMainTable = quoteIdentifier(mainTable);
|
|
11041
12285
|
const recordIdColumn = `${modelName.toLowerCase()}_id`;
|
|
@@ -11141,6 +12385,12 @@ var init_BaseModel = __esm({
|
|
|
11141
12385
|
params = params.concat(whereClause.params);
|
|
11142
12386
|
}
|
|
11143
12387
|
}
|
|
12388
|
+
({ sql, params } = this.withDocumentCondition(
|
|
12389
|
+
sql,
|
|
12390
|
+
params,
|
|
12391
|
+
translator,
|
|
12392
|
+
documents
|
|
12393
|
+
));
|
|
11144
12394
|
const groupByParts = [];
|
|
11145
12395
|
for (const field of regularGroupBy) {
|
|
11146
12396
|
groupByParts.push(`${quotedMainTable}.${quoteIdentifier(field)}`);
|
|
@@ -11287,12 +12537,18 @@ var init_BaseModel = __esm({
|
|
|
11287
12537
|
}
|
|
11288
12538
|
const modelName = schema.options.name;
|
|
11289
12539
|
Logger.verbose(`[${modelName}] Executing count:`, { filter, options });
|
|
12540
|
+
const scope = this.documentsInScope(
|
|
12541
|
+
options?.documents
|
|
12542
|
+
);
|
|
11290
12543
|
const translator = new DocumentQueryTranslator(modelName, schema.fields);
|
|
11291
|
-
const translatedQuery = translator.translateCount(
|
|
12544
|
+
const translatedQuery = translator.translateCount(
|
|
12545
|
+
filter,
|
|
12546
|
+
options?.documents == null ? { documents: scope } : options
|
|
12547
|
+
);
|
|
11292
12548
|
Logger.verbose(`[${modelName}] Translated count SQL:`, translatedQuery.sql);
|
|
11293
12549
|
Logger.verbose(`[${modelName}] Count SQL params:`, translatedQuery.params);
|
|
11294
12550
|
const countEngine = await this.queryEngineFor(
|
|
11295
|
-
|
|
12551
|
+
scope,
|
|
11296
12552
|
modelName
|
|
11297
12553
|
);
|
|
11298
12554
|
const results = await countEngine.query(
|
|
@@ -11366,7 +12622,7 @@ var init_BaseModel = __esm({
|
|
|
11366
12622
|
);
|
|
11367
12623
|
}
|
|
11368
12624
|
for (const [recordId, recordYMap] of documentYMap.entries()) {
|
|
11369
|
-
if (recordYMap instanceof
|
|
12625
|
+
if (recordYMap instanceof Y8.Map) {
|
|
11370
12626
|
const instance = new this({ id: recordId });
|
|
11371
12627
|
instance._metaDocId = docId;
|
|
11372
12628
|
const connectedDoc = modelConstructor.connectedDocuments.get(docId);
|
|
@@ -11552,7 +12808,7 @@ var init_BaseModel = __esm({
|
|
|
11552
12808
|
if (!recordYMap) {
|
|
11553
12809
|
return null;
|
|
11554
12810
|
}
|
|
11555
|
-
if (recordYMap instanceof
|
|
12811
|
+
if (recordYMap instanceof Y8.Map) {
|
|
11556
12812
|
const instance = new modelConstructor({
|
|
11557
12813
|
id: recordId
|
|
11558
12814
|
});
|
|
@@ -11828,7 +13084,7 @@ var init_BaseModel = __esm({
|
|
|
11828
13084
|
for (const [fieldKey, fieldOptions] of schema.fields) {
|
|
11829
13085
|
if (fieldOptions?.type !== "stringset") continue;
|
|
11830
13086
|
const value = recordYMap.get(fieldKey);
|
|
11831
|
-
if (value instanceof
|
|
13087
|
+
if (value instanceof Y8.Map) {
|
|
11832
13088
|
this.observeStringSetMapOnce(value, docId);
|
|
11833
13089
|
}
|
|
11834
13090
|
}
|
|
@@ -12050,6 +13306,7 @@ __export(browser_exports, {
|
|
|
12050
13306
|
FORMAT2_MANIFEST_VERSION: () => FORMAT2_MANIFEST_VERSION,
|
|
12051
13307
|
FORMAT2_MIN_MANIFEST_VERSION: () => FORMAT2_MIN_MANIFEST_VERSION,
|
|
12052
13308
|
FORMAT2_STORE_CHANNEL: () => FORMAT2_STORE_CHANNEL,
|
|
13309
|
+
FORMAT2_STORE_PROTOCOL: () => FORMAT2_STORE_PROTOCOL,
|
|
12053
13310
|
Format2CompositeFieldError: () => Format2CompositeFieldError,
|
|
12054
13311
|
Format2FoldBrokenError: () => Format2FoldBrokenError,
|
|
12055
13312
|
Format2ModelNotHydratedError: () => Format2ModelNotHydratedError,
|
|
@@ -12058,6 +13315,7 @@ __export(browser_exports, {
|
|
|
12058
13315
|
Format2RecordStoreAsync: () => Format2RecordStoreAsync,
|
|
12059
13316
|
Format2RowCache: () => Format2RowCache,
|
|
12060
13317
|
Format2StorageCapabilityError: () => Format2StorageCapabilityError,
|
|
13318
|
+
Format2StoreOutdatedError: () => Format2StoreOutdatedError,
|
|
12061
13319
|
INGEST_CHUNK_LIMITS: () => INGEST_CHUNK_LIMITS,
|
|
12062
13320
|
InProcessFormat2StoreHost: () => InProcessFormat2StoreHost,
|
|
12063
13321
|
LogLevel: () => LogLevel,
|
|
@@ -12093,12 +13351,14 @@ __export(browser_exports, {
|
|
|
12093
13351
|
configuredOfflineWindowDays: () => configuredOfflineWindowDays,
|
|
12094
13352
|
createModelClass: () => createModelClass,
|
|
12095
13353
|
decideDeferredReplay: () => decideDeferredReplay,
|
|
13354
|
+
decodeFoldedState: () => decodeFoldedState,
|
|
12096
13355
|
decodeFormat2StoreError: () => decodeFormat2StoreError,
|
|
12097
13356
|
defineModelSchema: () => defineModelSchema,
|
|
12098
13357
|
detectEnvironment: () => detectEnvironment,
|
|
12099
13358
|
discoverModelNames: () => discoverModelNames,
|
|
12100
13359
|
discoverSchema: () => discoverSchema,
|
|
12101
13360
|
dispatchFormat2StoreCommand: () => dispatchFormat2StoreCommand,
|
|
13361
|
+
encodeFoldedState: () => encodeFoldedState,
|
|
12102
13362
|
encodeFormat2StoreError: () => encodeFormat2StoreError,
|
|
12103
13363
|
encodeOverlayMutation: () => encodeOverlayMutation,
|
|
12104
13364
|
estimateMaterializedBytes: () => estimateMaterializedBytes,
|
|
@@ -12127,10 +13387,12 @@ __export(browser_exports, {
|
|
|
12127
13387
|
loadFormat2Snapshot: () => loadFormat2Snapshot,
|
|
12128
13388
|
loadSchemaFromTomlString: () => loadSchemaFromTomlString,
|
|
12129
13389
|
materializeOverlayRecord: () => materializeOverlayRecord,
|
|
13390
|
+
mergeFoldedStates: () => mergeFoldedStates,
|
|
12130
13391
|
observeFormat2Overlay: () => observeFormat2Overlay,
|
|
12131
13392
|
offlineReplayRecordKey: () => offlineReplayRecordKey,
|
|
12132
13393
|
offlineWindowStatus: () => offlineWindowStatus,
|
|
12133
13394
|
overlayFieldKey: () => overlayFieldKey,
|
|
13395
|
+
overlayKeysSince: () => overlayKeysSince,
|
|
12134
13396
|
overlayMarkerKey: () => overlayMarkerKey,
|
|
12135
13397
|
overlayMemberKey: () => overlayMemberKey,
|
|
12136
13398
|
parseOverlayKey: () => parseOverlayKey,
|
|
@@ -12144,6 +13406,7 @@ __export(browser_exports, {
|
|
|
12144
13406
|
readFormat2StoreMarks: () => readFormat2StoreMarks,
|
|
12145
13407
|
readOverlayRecord: () => readOverlayRecord,
|
|
12146
13408
|
readSnapshotChunk: () => readSnapshotChunk,
|
|
13409
|
+
readStateVector: () => readStateVector,
|
|
12147
13410
|
registerFunctionDefault: () => registerFunctionDefault,
|
|
12148
13411
|
resetJsBao: () => resetJsBao,
|
|
12149
13412
|
resolveChunkOrdinal: () => resolveChunkOrdinal,
|
|
@@ -13015,7 +14278,7 @@ function planSnapshotHydration(input) {
|
|
|
13015
14278
|
var Format2QueryScopeError = class _Format2QueryScopeError extends Error {
|
|
13016
14279
|
constructor(model, documents) {
|
|
13017
14280
|
super(
|
|
13018
|
-
`A query on '${model}' spans documents held in different local
|
|
14281
|
+
`A query on '${model}' spans documents held in different local stores (${documents.join(", ")}): an ordinary document's rows and a large document's cannot be read in one statement. Scope it with \`documents\` to one kind.`
|
|
13019
14282
|
);
|
|
13020
14283
|
this.model = model;
|
|
13021
14284
|
this.documents = documents;
|
|
@@ -13037,6 +14300,7 @@ init_format2StoreHost();
|
|
|
13037
14300
|
// src/engines/opfs/format2WorkerProtocol.ts
|
|
13038
14301
|
init_format2StoreProtocol();
|
|
13039
14302
|
var FORMAT2_WORKER_CHANNEL = "js-bao.format2.worker";
|
|
14303
|
+
var FORMAT2_STORE_PROTOCOL = 2;
|
|
13040
14304
|
var FORMAT2_ENGINE_CHANNEL = "js-bao.format2.engine";
|
|
13041
14305
|
function isFormat2WorkerResponse(data) {
|
|
13042
14306
|
return typeof data === "object" && data !== null && (data.channel === FORMAT2_WORKER_CHANNEL || data.channel === FORMAT2_ENGINE_CHANNEL) && typeof data.id === "number" && typeof data.ok === "boolean";
|
|
@@ -13050,6 +14314,23 @@ var Format2WorkerOpenError = class _Format2WorkerOpenError extends Error {
|
|
|
13050
14314
|
}
|
|
13051
14315
|
code = "FORMAT2_WORKER_OPEN_FAILED";
|
|
13052
14316
|
};
|
|
14317
|
+
function servedStoreProtocol(reply) {
|
|
14318
|
+
const version = reply?.storeProtocol;
|
|
14319
|
+
return typeof version === "number" && Number.isFinite(version) ? version : 1;
|
|
14320
|
+
}
|
|
14321
|
+
var Format2StoreOutdatedError = class _Format2StoreOutdatedError extends Error {
|
|
14322
|
+
constructor(served, required) {
|
|
14323
|
+
super(
|
|
14324
|
+
"the tab leading this store runs an older version of the app; reload it or close it"
|
|
14325
|
+
);
|
|
14326
|
+
this.served = served;
|
|
14327
|
+
this.required = required;
|
|
14328
|
+
this.name = "Format2StoreOutdatedError";
|
|
14329
|
+
Object.setPrototypeOf(this, _Format2StoreOutdatedError.prototype);
|
|
14330
|
+
}
|
|
14331
|
+
/** Stable across builds, so an app can branch on it rather than on text. */
|
|
14332
|
+
code = "FORMAT2_STORE_OUTDATED";
|
|
14333
|
+
};
|
|
13053
14334
|
function decodeFormat2WorkerError(payload) {
|
|
13054
14335
|
if (payload.code === "FORMAT2_WORKER_OPEN_FAILED") {
|
|
13055
14336
|
const rebuilt = new Format2WorkerOpenError(
|
|
@@ -13066,6 +14347,7 @@ function decodeFormat2WorkerError(payload) {
|
|
|
13066
14347
|
var FORMAT2_WORKER_MAX_SPAWNS = 12;
|
|
13067
14348
|
var FORMAT2_WORKER_BACKOFF_MS = 250;
|
|
13068
14349
|
var FORMAT2_WORKER_CLOSE_BOUND_MS = 2e3;
|
|
14350
|
+
var FORMAT2_LEGACY_OPEN_BOUND_MS = 2e3;
|
|
13069
14351
|
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
13070
14352
|
var WorkerLink = class {
|
|
13071
14353
|
constructor(worker, label) {
|
|
@@ -13180,13 +14462,13 @@ async function openFormat2Worker(options) {
|
|
|
13180
14462
|
const failures = [];
|
|
13181
14463
|
const abandoned = () => new Format2WorkerOpenError(
|
|
13182
14464
|
"worker-unavailable",
|
|
13183
|
-
`the format-2 store
|
|
14465
|
+
`the format-2 store '${request.poolName}' was not opened: the engine was destroyed`
|
|
13184
14466
|
);
|
|
13185
14467
|
for (let attempt = 1; attempt <= maxSpawns; attempt++) {
|
|
13186
14468
|
if (signal?.aborted) throw abandoned();
|
|
13187
14469
|
let link;
|
|
13188
14470
|
try {
|
|
13189
|
-
link = new WorkerLink(options.spawn(), request.
|
|
14471
|
+
link = new WorkerLink(options.spawn(), request.poolName);
|
|
13190
14472
|
} catch (error) {
|
|
13191
14473
|
throw new Format2WorkerOpenError(
|
|
13192
14474
|
"worker-unavailable",
|
|
@@ -13215,7 +14497,7 @@ async function openFormat2Worker(options) {
|
|
|
13215
14497
|
}
|
|
13216
14498
|
throw new Format2WorkerOpenError(
|
|
13217
14499
|
"install-failed",
|
|
13218
|
-
`the format-2 store
|
|
14500
|
+
`the format-2 store '${request.poolName}' did not open after ${maxSpawns} worker spawn(s); last failure: ${failures[failures.length - 1] ?? "unknown"}`
|
|
13219
14501
|
);
|
|
13220
14502
|
}
|
|
13221
14503
|
function attachFormat2Worker(port, label) {
|
|
@@ -13242,27 +14524,36 @@ function makeHandle(link, reply, spawns, failures) {
|
|
|
13242
14524
|
get died() {
|
|
13243
14525
|
return link.died;
|
|
13244
14526
|
},
|
|
13245
|
-
storedEpoch: () => control({ kind: "storedEpoch" }),
|
|
14527
|
+
storedEpoch: (docId) => control({ kind: "storedEpoch", docId }),
|
|
13246
14528
|
engine: (command) => link.send(FORMAT2_ENGINE_CHANNEL, command),
|
|
13247
14529
|
send: (channel, command) => link.send(channel, command),
|
|
13248
14530
|
async serveStore(docId, clientId) {
|
|
13249
14531
|
const channel = new MessageChannel();
|
|
13250
|
-
await control(
|
|
14532
|
+
const served = await control(
|
|
13251
14533
|
{ kind: "serve", port: channel.port2, docId, ...clientId ? { clientId } : {} },
|
|
13252
14534
|
[channel.port2]
|
|
13253
14535
|
);
|
|
14536
|
+
const version = servedStoreProtocol(served);
|
|
14537
|
+
if (version < FORMAT2_STORE_PROTOCOL) {
|
|
14538
|
+
try {
|
|
14539
|
+
channel.port1.close();
|
|
14540
|
+
} catch {
|
|
14541
|
+
}
|
|
14542
|
+
throw new Format2StoreOutdatedError(version, FORMAT2_STORE_PROTOCOL);
|
|
14543
|
+
}
|
|
13254
14544
|
const host = new RemoteFormat2StoreHost(channel.port1, docId);
|
|
13255
14545
|
link.trackHost(host);
|
|
13256
14546
|
return host;
|
|
13257
14547
|
},
|
|
13258
|
-
release: (docId, clientId
|
|
14548
|
+
release: (docId, clientId) => control({
|
|
13259
14549
|
kind: "release",
|
|
13260
14550
|
docId,
|
|
13261
|
-
...clientId ? { clientId } : {}
|
|
13262
|
-
...wipeIfLast ? { wipeIfLast } : {}
|
|
14551
|
+
...clientId ? { clientId } : {}
|
|
13263
14552
|
}),
|
|
13264
14553
|
holders: (docId) => control({ kind: "holders", docId }),
|
|
13265
|
-
attach: (attachedPort) =>
|
|
14554
|
+
attach: async (attachedPort) => {
|
|
14555
|
+
await control({ kind: "attach", port: attachedPort }, [attachedPort]);
|
|
14556
|
+
},
|
|
13266
14557
|
async close(boundMs = FORMAT2_WORKER_CLOSE_BOUND_MS) {
|
|
13267
14558
|
if (link.died) {
|
|
13268
14559
|
link.terminate();
|
|
@@ -13503,8 +14794,44 @@ var Format2NamespaceAttachment = class {
|
|
|
13503
14794
|
engine(command) {
|
|
13504
14795
|
return this.send(FORMAT2_ENGINE_CHANNEL, command);
|
|
13505
14796
|
}
|
|
13506
|
-
|
|
13507
|
-
|
|
14797
|
+
/**
|
|
14798
|
+
* The epoch `docId`'s merged view is on, before it is bound (#3760: one
|
|
14799
|
+
* worker holds every document of the scope, so the command names one).
|
|
14800
|
+
*/
|
|
14801
|
+
storedEpoch(docId) {
|
|
14802
|
+
return this.send(FORMAT2_WORKER_CHANNEL, { kind: "storedEpoch", docId });
|
|
14803
|
+
}
|
|
14804
|
+
/** Every large document the pool's store holds (#3760). */
|
|
14805
|
+
listDocuments() {
|
|
14806
|
+
return this.send(FORMAT2_WORKER_CHANNEL, { kind: "listDocuments" });
|
|
14807
|
+
}
|
|
14808
|
+
/**
|
|
14809
|
+
* What a PRE-AMENDMENT pool still owes for `docId` (#3760).
|
|
14810
|
+
*
|
|
14811
|
+
* Sent to a transient attachment over the LEGACY namespace, never to the
|
|
14812
|
+
* scope worker: it is the old database that holds the debt.
|
|
14813
|
+
*/
|
|
14814
|
+
exportLegacy(docId) {
|
|
14815
|
+
return this.send(FORMAT2_WORKER_CHANNEL, { kind: "exportLegacy", docId });
|
|
14816
|
+
}
|
|
14817
|
+
/** Take that debt over, once, in one transaction (#3760). */
|
|
14818
|
+
importLegacy(options) {
|
|
14819
|
+
return this.send(FORMAT2_WORKER_CHANNEL, {
|
|
14820
|
+
kind: "importLegacy",
|
|
14821
|
+
docId: options.docId,
|
|
14822
|
+
pool: options.pool,
|
|
14823
|
+
legacyEpoch: options.legacyEpoch,
|
|
14824
|
+
rows: options.rows
|
|
14825
|
+
});
|
|
14826
|
+
}
|
|
14827
|
+
/** Delete exactly the imported batch from the legacy pool (#3760). */
|
|
14828
|
+
dropLegacy(docId, rows, discontinuities = []) {
|
|
14829
|
+
return this.send(FORMAT2_WORKER_CHANNEL, {
|
|
14830
|
+
kind: "dropLegacy",
|
|
14831
|
+
docId,
|
|
14832
|
+
rows,
|
|
14833
|
+
discontinuities
|
|
14834
|
+
});
|
|
13508
14835
|
}
|
|
13509
14836
|
holders(docId) {
|
|
13510
14837
|
return this.send(FORMAT2_WORKER_CHANNEL, { kind: "holders", docId });
|
|
@@ -13519,6 +14846,21 @@ var Format2NamespaceAttachment = class {
|
|
|
13519
14846
|
purge(docId, force) {
|
|
13520
14847
|
return this.send(FORMAT2_WORKER_CHANNEL, { kind: "purge", docId, force });
|
|
13521
14848
|
}
|
|
14849
|
+
/**
|
|
14850
|
+
* Purge exactly `documents`, in ONE queued command (#3760).
|
|
14851
|
+
*
|
|
14852
|
+
* The scope-wide wipe's single act. Like `purge` it goes out on whatever
|
|
14853
|
+
* this tab is attached to and touches rows rather than the pool; unlike a
|
|
14854
|
+
* loop of `purge`es, no other tab's `serve` can land between two of its
|
|
14855
|
+
* documents.
|
|
14856
|
+
*/
|
|
14857
|
+
purgeAll(documents, force) {
|
|
14858
|
+
return this.send(FORMAT2_WORKER_CHANNEL, {
|
|
14859
|
+
kind: "purgeAll",
|
|
14860
|
+
documents: [...documents],
|
|
14861
|
+
force
|
|
14862
|
+
});
|
|
14863
|
+
}
|
|
13522
14864
|
/**
|
|
13523
14865
|
* Ask the worker to give up the pool, so its directory may be removed
|
|
13524
14866
|
* (#3754).
|
|
@@ -13542,12 +14884,12 @@ var Format2NamespaceAttachment = class {
|
|
|
13542
14884
|
get leads() {
|
|
13543
14885
|
return this.attachment?.role === "leader" && (this.options.locks === null || this.leaderRelease !== null);
|
|
13544
14886
|
}
|
|
13545
|
-
|
|
14887
|
+
/** End one session; answers how many sessions of `docId` remain (#3756). */
|
|
14888
|
+
release(docId, clientId) {
|
|
13546
14889
|
return this.send(FORMAT2_WORKER_CHANNEL, {
|
|
13547
14890
|
kind: "release",
|
|
13548
14891
|
docId,
|
|
13549
|
-
...clientId ? { clientId } : {}
|
|
13550
|
-
...wipeIfLast ? { wipeIfLast } : {}
|
|
14892
|
+
...clientId ? { clientId } : {}
|
|
13551
14893
|
});
|
|
13552
14894
|
}
|
|
13553
14895
|
send(channel, command) {
|
|
@@ -13989,8 +15331,17 @@ var Format2NamespaceAttachment = class {
|
|
|
13989
15331
|
/** @internal — used by {@link AttachedStoreSession}. */
|
|
13990
15332
|
async openStorePort(docId, clientId) {
|
|
13991
15333
|
const attachment = await this.ensureAttached();
|
|
13992
|
-
|
|
13993
|
-
|
|
15334
|
+
try {
|
|
15335
|
+
const host = await attachment.handle.serveStore(docId, clientId);
|
|
15336
|
+
return { host, generation: attachment.generation };
|
|
15337
|
+
} catch (error) {
|
|
15338
|
+
if (error instanceof Format2StoreOutdatedError) {
|
|
15339
|
+
Logger.warn(
|
|
15340
|
+
`[OpfsEngine] ${this.options.namespace.poolName}: the worker serving this store speaks store protocol ${error.served}; this tab needs ${error.required}. ${docId} is not opened: ${error.message}.`
|
|
15341
|
+
);
|
|
15342
|
+
}
|
|
15343
|
+
throw error;
|
|
15344
|
+
}
|
|
13994
15345
|
}
|
|
13995
15346
|
/** @internal */
|
|
13996
15347
|
forgetSession(session) {
|
|
@@ -14074,7 +15425,7 @@ var Format2NamespaceAttachment = class {
|
|
|
14074
15425
|
if (current && !current.handle.died) {
|
|
14075
15426
|
await withinBound(
|
|
14076
15427
|
Promise.all(
|
|
14077
|
-
leaving.map((session) => session.close(
|
|
15428
|
+
leaving.map((session) => session.close().catch(() => void 0))
|
|
14078
15429
|
).then(() => void 0),
|
|
14079
15430
|
this.options.closeBoundMs
|
|
14080
15431
|
);
|
|
@@ -14232,19 +15583,18 @@ var AttachedStoreSession = class {
|
|
|
14232
15583
|
this.opening = opening;
|
|
14233
15584
|
await opening;
|
|
14234
15585
|
}
|
|
14235
|
-
close(
|
|
14236
|
-
return this.closed ??= this.closeOnce(
|
|
15586
|
+
close() {
|
|
15587
|
+
return this.closed ??= this.closeOnce();
|
|
14237
15588
|
}
|
|
14238
|
-
async closeOnce(
|
|
15589
|
+
async closeOnce() {
|
|
14239
15590
|
this.attachment.forgetSession(this);
|
|
14240
|
-
let wiped = false;
|
|
14241
15591
|
try {
|
|
14242
|
-
const reply = await this.attachment.release(
|
|
14243
|
-
|
|
14244
|
-
|
|
14245
|
-
|
|
14246
|
-
|
|
14247
|
-
|
|
15592
|
+
const reply = await this.attachment.release(this.docId, this.clientId);
|
|
15593
|
+
if (typeof reply?.remaining !== "number") {
|
|
15594
|
+
Logger.info(
|
|
15595
|
+
`[OpfsEngine] ${this.docId}: the worker answered release with no holder count (an older worker bundle); the session is ended.`
|
|
15596
|
+
);
|
|
15597
|
+
}
|
|
14248
15598
|
} catch (error) {
|
|
14249
15599
|
Logger.debug(
|
|
14250
15600
|
`[OpfsEngine] ${this.docId}: release was not answered: ${error?.message ?? String(error)}`
|
|
@@ -14254,7 +15604,6 @@ var AttachedStoreSession = class {
|
|
|
14254
15604
|
this.host = null;
|
|
14255
15605
|
await host?.close().catch(() => {
|
|
14256
15606
|
});
|
|
14257
|
-
return { wiped };
|
|
14258
15607
|
}
|
|
14259
15608
|
};
|
|
14260
15609
|
|
|
@@ -14285,8 +15634,8 @@ function observeFormat2Lifecycle(target, handlers) {
|
|
|
14285
15634
|
}
|
|
14286
15635
|
|
|
14287
15636
|
// src/engines/opfs/storeNamespace.ts
|
|
14288
|
-
function format2ClientLockName(namespace, clientId) {
|
|
14289
|
-
return `${namespace.lockName}:client:${encodePart(clientId)}`;
|
|
15637
|
+
function format2ClientLockName(namespace, docId, clientId) {
|
|
15638
|
+
return `${namespace.lockName}:client:${encodePart(docId)}:${encodePart(clientId)}`;
|
|
14290
15639
|
}
|
|
14291
15640
|
function format2DocumentLockName(namespace, docId) {
|
|
14292
15641
|
return `${namespace.lockName}:doc:${encodePart(docId)}`;
|
|
@@ -14294,12 +15643,13 @@ function format2DocumentLockName(namespace, docId) {
|
|
|
14294
15643
|
function format2PoolDirectory(poolName) {
|
|
14295
15644
|
return `.${poolName}`;
|
|
14296
15645
|
}
|
|
14297
|
-
function
|
|
15646
|
+
function format2LegacyPoolPrefix(scope = {}) {
|
|
14298
15647
|
const parts = [scope.server ?? "", scope.app ?? "", scope.user ?? ""];
|
|
14299
|
-
return parts.some((part) => part !== "") ?
|
|
15648
|
+
return parts.some((part) => part !== "") ? LEGACY_PREFIX + parts.map(encodePart).join("-") + "-" : LEGACY_PREFIX;
|
|
14300
15649
|
}
|
|
14301
15650
|
function format2DocumentIdFromPoolName(poolName, scope = {}) {
|
|
14302
|
-
const prefix =
|
|
15651
|
+
const prefix = format2LegacyPoolPrefix(scope);
|
|
15652
|
+
if (poolName.startsWith(SCOPE_PREFIX)) return null;
|
|
14303
15653
|
if (!poolName.startsWith(prefix)) return null;
|
|
14304
15654
|
const encoded = poolName.slice(prefix.length);
|
|
14305
15655
|
if (encoded.length === 0 || encoded.includes("-")) return null;
|
|
@@ -14324,7 +15674,9 @@ function format2DocumentIdFromPoolName(poolName, scope = {}) {
|
|
|
14324
15674
|
return null;
|
|
14325
15675
|
}
|
|
14326
15676
|
}
|
|
14327
|
-
var
|
|
15677
|
+
var LEGACY_PREFIX = "js-bao-format2-";
|
|
15678
|
+
var SCOPE_PREFIX = "js-bao-format2s-";
|
|
15679
|
+
var UNSCOPED_NAME = `${SCOPE_PREFIX}unscoped`;
|
|
14328
15680
|
function encodePart(value) {
|
|
14329
15681
|
let out = "";
|
|
14330
15682
|
for (const char of value) {
|
|
@@ -14337,10 +15689,20 @@ function encodePart(value) {
|
|
|
14337
15689
|
}
|
|
14338
15690
|
return out;
|
|
14339
15691
|
}
|
|
14340
|
-
function format2StoreNamespace(
|
|
15692
|
+
function format2StoreNamespace(scope = {}) {
|
|
15693
|
+
const parts = [scope.server ?? "", scope.app ?? "", scope.user ?? ""];
|
|
15694
|
+
const name = parts.some((part) => part !== "") ? SCOPE_PREFIX + parts.map(encodePart).join("-") : UNSCOPED_NAME;
|
|
15695
|
+
return {
|
|
15696
|
+
poolName: name,
|
|
15697
|
+
dbFile: "/store.sqlite3",
|
|
15698
|
+
lockName: name,
|
|
15699
|
+
poolLockName: `${name}:pool`
|
|
15700
|
+
};
|
|
15701
|
+
}
|
|
15702
|
+
function format2LegacyStoreNamespace(docId, scope = {}) {
|
|
14341
15703
|
const parts = [scope.server ?? "", scope.app ?? "", scope.user ?? ""];
|
|
14342
15704
|
const prefix = parts.some((part) => part !== "") ? parts.map(encodePart).join("-") + "-" : "";
|
|
14343
|
-
const name =
|
|
15705
|
+
const name = LEGACY_PREFIX + prefix + encodePart(docId);
|
|
14344
15706
|
return {
|
|
14345
15707
|
poolName: name,
|
|
14346
15708
|
dbFile: "/store.sqlite3",
|
|
@@ -14490,7 +15852,7 @@ var OpfsEngine = class {
|
|
|
14490
15852
|
* refusal, raised before the models are wired, with one grep-able line.
|
|
14491
15853
|
*/
|
|
14492
15854
|
async createFormat2StoreHost(docId, options = {}) {
|
|
14493
|
-
const namespace = format2StoreNamespace(
|
|
15855
|
+
const namespace = format2StoreNamespace(options.scope ?? {});
|
|
14494
15856
|
const documentHeld = await this.holdShared(
|
|
14495
15857
|
format2DocumentLockName(namespace, docId)
|
|
14496
15858
|
);
|
|
@@ -14500,7 +15862,7 @@ var OpfsEngine = class {
|
|
|
14500
15862
|
};
|
|
14501
15863
|
let entry;
|
|
14502
15864
|
try {
|
|
14503
|
-
entry = await this.ensureWorker(
|
|
15865
|
+
entry = await this.ensureWorker(namespace);
|
|
14504
15866
|
} catch (error) {
|
|
14505
15867
|
releaseDocument();
|
|
14506
15868
|
const reason = error?.message ?? String(error);
|
|
@@ -14512,56 +15874,192 @@ var OpfsEngine = class {
|
|
|
14512
15874
|
const clientId = options.clientId;
|
|
14513
15875
|
let host;
|
|
14514
15876
|
try {
|
|
14515
|
-
host = entry.attachment.serveStore(docId, clientId);
|
|
14516
|
-
await host.dispatch({ kind: "storedEpoch" });
|
|
15877
|
+
host = entry.attachment.serveStore(docId, clientId);
|
|
15878
|
+
await host.dispatch({ kind: "storedEpoch" });
|
|
15879
|
+
} catch (error) {
|
|
15880
|
+
releaseDocument();
|
|
15881
|
+
if (error instanceof Format2StoreOutdatedError) {
|
|
15882
|
+
await host?.close().catch(() => {
|
|
15883
|
+
});
|
|
15884
|
+
throw error;
|
|
15885
|
+
}
|
|
15886
|
+
const reason = error?.message ?? String(error);
|
|
15887
|
+
Logger.error(
|
|
15888
|
+
`[OpfsEngine] ${docId}: the worker could not open its store: ${reason}`
|
|
15889
|
+
);
|
|
15890
|
+
throw new Format2StorageCapabilityError("not-persistent", 0, null, []);
|
|
15891
|
+
}
|
|
15892
|
+
await this.adoptLegacyPool(docId, options.scope ?? {}, entry).catch(
|
|
15893
|
+
(error) => {
|
|
15894
|
+
Logger.warn(
|
|
15895
|
+
`[OpfsEngine] ${docId}: the pre-amendment pool could not be adopted (${error?.message ?? String(error)}); its writes stay there and the next open tries again.`
|
|
15896
|
+
);
|
|
15897
|
+
}
|
|
15898
|
+
);
|
|
15899
|
+
entry.docIds.add(docId);
|
|
15900
|
+
const routing = this.docs.get(docId);
|
|
15901
|
+
if (routing && routing.entry === entry) routing.sessions++;
|
|
15902
|
+
else this.docs.set(docId, { entry, sessions: 1 });
|
|
15903
|
+
const held = clientId ? this.holdPresence(format2ClientLockName(namespace, docId, clientId)) : null;
|
|
15904
|
+
if (held) this.presence.add(held);
|
|
15905
|
+
const releasePresence = () => {
|
|
15906
|
+
if (held && this.presence.delete(held)) held();
|
|
15907
|
+
releaseDocument();
|
|
15908
|
+
};
|
|
15909
|
+
const engine = this;
|
|
15910
|
+
const closeOnce = async () => {
|
|
15911
|
+
const routing2 = engine.docs.get(docId);
|
|
15912
|
+
if (routing2 && routing2.entry === entry && --routing2.sessions <= 0) {
|
|
15913
|
+
entry.docIds.delete(docId);
|
|
15914
|
+
engine.docs.delete(docId);
|
|
15915
|
+
}
|
|
15916
|
+
releasePresence();
|
|
15917
|
+
await host.close();
|
|
15918
|
+
};
|
|
15919
|
+
let closing = null;
|
|
15920
|
+
return {
|
|
15921
|
+
dispatch: (command) => host.dispatch(command),
|
|
15922
|
+
directStore: () => null,
|
|
15923
|
+
holders: () => entry.attachment.holders(docId),
|
|
15924
|
+
clientIsLive: (other) => engine.probeLock(format2ClientLockName(namespace, docId, other)),
|
|
15925
|
+
// Forwarded, not answered here: the SESSION is what knows it has been
|
|
15926
|
+
// served again by a worker that never knew it, which is the moment the
|
|
15927
|
+
// document's projection marks stop vouching for anything (finding
|
|
15928
|
+
// 3431-R12).
|
|
15929
|
+
onReattached: (handler) => host.onReattached ? host.onReattached(handler) : () => {
|
|
15930
|
+
},
|
|
15931
|
+
close: () => (
|
|
15932
|
+
// Idempotent: a second close of one host must not give up a second
|
|
15933
|
+
// session, which would unroute a document another host still holds.
|
|
15934
|
+
closing ??= closeOnce()
|
|
15935
|
+
)
|
|
15936
|
+
};
|
|
15937
|
+
}
|
|
15938
|
+
/**
|
|
15939
|
+
* Take over a pre-amendment per-document pool's debt for `docId` (#3760).
|
|
15940
|
+
*
|
|
15941
|
+
* Before the 2026-09-24 amendment a large document's unacknowledged writes
|
|
15942
|
+
* lived in a pool named by document. This build names a pool by scope alone,
|
|
15943
|
+
* so those writes would simply stop being found — and they were accepted from
|
|
15944
|
+
* the app and never acknowledged by the server, which is the one thing the
|
|
15945
|
+
* intent's 0.7.0 rule does not let go of. The ROWS are not migrated (they
|
|
15946
|
+
* re-download through the ordinary cold start); the DEBT is.
|
|
15947
|
+
*
|
|
15948
|
+
* Runs on EVERY open, because a pre-amendment tab may still be running and
|
|
15949
|
+
* writing to that pool. With nothing new to take over it costs one directory
|
|
15950
|
+
* probe, one bounded spawn and two commands, and imports nothing. With no
|
|
15951
|
+
* legacy directory at all it costs the probe alone.
|
|
15952
|
+
*
|
|
15953
|
+
* The order is import-then-drop, and never the other way round: a crash
|
|
15954
|
+
* between them leaves the write in both databases, which the `_legacy_adopted`
|
|
15955
|
+
* ledger makes harmless (the next open imports nothing and drops the copy),
|
|
15956
|
+
* whereas dropping first would lose it outright.
|
|
15957
|
+
*/
|
|
15958
|
+
async adoptLegacyPool(docId, scope, scopeEntry) {
|
|
15959
|
+
const legacy = format2LegacyStoreNamespace(docId, scope);
|
|
15960
|
+
await this.ensureRoot();
|
|
15961
|
+
const root = this.opfsRoot();
|
|
15962
|
+
if (!root) return;
|
|
15963
|
+
const directory = format2PoolDirectory(legacy.poolName);
|
|
15964
|
+
if (!await this.directoryExists(root, directory)) return;
|
|
15965
|
+
let entry;
|
|
15966
|
+
try {
|
|
15967
|
+
entry = await this.openLegacyWorker(legacy);
|
|
15968
|
+
} catch (error) {
|
|
15969
|
+
Logger.warn(
|
|
15970
|
+
`[OpfsEngine] ${docId}: the pre-amendment pool '${directory}' could not be opened (${error?.message ?? String(error)}); its writes stay there and the next open tries again.`
|
|
15971
|
+
);
|
|
15972
|
+
return;
|
|
15973
|
+
}
|
|
15974
|
+
try {
|
|
15975
|
+
const rows = await entry.attachment.exportLegacy(docId);
|
|
15976
|
+
const report = await scopeEntry.attachment.importLegacy({
|
|
15977
|
+
docId,
|
|
15978
|
+
pool: legacy.poolName,
|
|
15979
|
+
legacyEpoch: rows.epoch,
|
|
15980
|
+
rows
|
|
15981
|
+
});
|
|
15982
|
+
if (report.imported.length > 0) {
|
|
15983
|
+
Logger.info(
|
|
15984
|
+
`[OpfsEngine] ${docId}: adopted ${report.imported.length} unacknowledged write(s) from the pre-amendment pool '${directory}', made on epoch ${rows.epoch}.`
|
|
15985
|
+
);
|
|
15986
|
+
}
|
|
15987
|
+
if (this.__failLegacyDropOnce) {
|
|
15988
|
+
this.__failLegacyDropOnce = false;
|
|
15989
|
+
throw new Error("the drop was interrupted");
|
|
15990
|
+
}
|
|
15991
|
+
if (report.settled.length > 0 || report.discontinuities.length > 0) {
|
|
15992
|
+
await entry.attachment.dropLegacy(
|
|
15993
|
+
docId,
|
|
15994
|
+
report.settled,
|
|
15995
|
+
report.discontinuities
|
|
15996
|
+
);
|
|
15997
|
+
}
|
|
15998
|
+
} finally {
|
|
15999
|
+
await this.closeLegacyWorker(legacy, entry);
|
|
16000
|
+
}
|
|
16001
|
+
}
|
|
16002
|
+
/**
|
|
16003
|
+
* A transient attachment over a legacy namespace, bounded (#3760).
|
|
16004
|
+
*
|
|
16005
|
+
* Deliberately NOT `ensureWorker`: this worker is not the scope's, it must
|
|
16006
|
+
* not be remembered in `workers` (a leader change has nothing to hand over
|
|
16007
|
+
* here), and its open budget is capped so a legacy pool another tab holds
|
|
16008
|
+
* costs an open two seconds rather than the engine's whole budget.
|
|
16009
|
+
*/
|
|
16010
|
+
async openLegacyWorker(namespace) {
|
|
16011
|
+
const openTimeoutMs = Math.min(
|
|
16012
|
+
this.openTimeoutMs,
|
|
16013
|
+
FORMAT2_LEGACY_OPEN_BOUND_MS
|
|
16014
|
+
);
|
|
16015
|
+
const attachment = new Format2NamespaceAttachment({
|
|
16016
|
+
namespace,
|
|
16017
|
+
clientId: this.tabId,
|
|
16018
|
+
openRequest: {
|
|
16019
|
+
poolName: namespace.poolName,
|
|
16020
|
+
dbFile: namespace.dbFile,
|
|
16021
|
+
poolLockName: namespace.poolLockName,
|
|
16022
|
+
openTimeoutMs,
|
|
16023
|
+
...this.wasmURL ? { wasmURL: this.wasmURL } : {}
|
|
16024
|
+
},
|
|
16025
|
+
spawnWorker: this.spawn,
|
|
16026
|
+
connectBroker: this.connectBroker,
|
|
16027
|
+
brokerHint: this.brokerHint,
|
|
16028
|
+
locks: this.locks(),
|
|
16029
|
+
openTimeoutMs,
|
|
16030
|
+
closeBoundMs: this.internals.closeBoundMs ?? FORMAT2_WORKER_CLOSE_BOUND_MS,
|
|
16031
|
+
sleep: this.internals.sleep,
|
|
16032
|
+
// One spawn: a legacy pool that will not open is not worth retrying
|
|
16033
|
+
// inside an open, and the next open tries again anyway.
|
|
16034
|
+
maxSpawns: 1,
|
|
16035
|
+
backoffMs: this.internals.backoffMs,
|
|
16036
|
+
signal: this.teardown.signal
|
|
16037
|
+
});
|
|
16038
|
+
this.attachments.add(attachment);
|
|
16039
|
+
try {
|
|
16040
|
+
await attachment.ensureAttached();
|
|
14517
16041
|
} catch (error) {
|
|
14518
|
-
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
|
|
14522
|
-
);
|
|
14523
|
-
throw new Format2StorageCapabilityError("not-persistent", 0, null, []);
|
|
16042
|
+
this.attachments.delete(attachment);
|
|
16043
|
+
await attachment.destroy().catch(() => {
|
|
16044
|
+
});
|
|
16045
|
+
throw error;
|
|
14524
16046
|
}
|
|
14525
|
-
entry.docIds.add(docId);
|
|
14526
|
-
const routing = this.docs.get(docId);
|
|
14527
|
-
if (routing && routing.entry === entry) routing.sessions++;
|
|
14528
|
-
else this.docs.set(docId, { entry, sessions: 1 });
|
|
14529
|
-
const held = clientId ? this.holdPresence(format2ClientLockName(namespace, clientId)) : null;
|
|
14530
|
-
if (held) this.presence.add(held);
|
|
14531
|
-
const releasePresence = () => {
|
|
14532
|
-
if (held && this.presence.delete(held)) held();
|
|
14533
|
-
releaseDocument();
|
|
14534
|
-
};
|
|
14535
|
-
const engine = this;
|
|
14536
|
-
const closeOnce = async (closeOptions) => {
|
|
14537
|
-
const routing2 = engine.docs.get(docId);
|
|
14538
|
-
if (routing2 && routing2.entry === entry && --routing2.sessions <= 0) {
|
|
14539
|
-
entry.docIds.delete(docId);
|
|
14540
|
-
engine.docs.delete(docId);
|
|
14541
|
-
}
|
|
14542
|
-
releasePresence();
|
|
14543
|
-
return host.close(closeOptions);
|
|
14544
|
-
};
|
|
14545
|
-
let closing = null;
|
|
14546
16047
|
return {
|
|
14547
|
-
|
|
14548
|
-
|
|
14549
|
-
|
|
14550
|
-
|
|
14551
|
-
// Forwarded, not answered here: the SESSION is what knows it has been
|
|
14552
|
-
// served again by a worker that never knew it, which is the moment the
|
|
14553
|
-
// document's projection marks stop vouching for anything (finding
|
|
14554
|
-
// 3431-R12).
|
|
14555
|
-
onReattached: (handler) => host.onReattached ? host.onReattached(handler) : () => {
|
|
14556
|
-
},
|
|
14557
|
-
close: (closeOptions = {}) => (
|
|
14558
|
-
// Idempotent: a second close of one host must not give up a second
|
|
14559
|
-
// session, which would unroute a document another host still holds and
|
|
14560
|
-
// let its `release` wipe the query tables as the last holder.
|
|
14561
|
-
closing ??= closeOnce(closeOptions)
|
|
14562
|
-
)
|
|
16048
|
+
poolName: namespace.poolName,
|
|
16049
|
+
attachment,
|
|
16050
|
+
engine: new RemoteQueryEngine(attachment),
|
|
16051
|
+
docIds: /* @__PURE__ */ new Set()
|
|
14563
16052
|
};
|
|
14564
16053
|
}
|
|
16054
|
+
/** Pause and let go of a transient legacy worker; never remove its pool. */
|
|
16055
|
+
async closeLegacyWorker(namespace, entry) {
|
|
16056
|
+
this.attachments.delete(entry.attachment);
|
|
16057
|
+
await entry.attachment.destroy().catch((error) => {
|
|
16058
|
+
Logger.debug(
|
|
16059
|
+
`[OpfsEngine] the pre-amendment pool '${namespace.poolName}' did not close cleanly: ${error?.message ?? String(error)}`
|
|
16060
|
+
);
|
|
16061
|
+
});
|
|
16062
|
+
}
|
|
14565
16063
|
/**
|
|
14566
16064
|
* The epoch `docId`'s worker-side store follows, asked before the document
|
|
14567
16065
|
* is bound. `null` only when the worker cannot be reached; a fresh store
|
|
@@ -14569,12 +16067,25 @@ var OpfsEngine = class {
|
|
|
14569
16067
|
*/
|
|
14570
16068
|
async format2StoredEpoch(docId, scope) {
|
|
14571
16069
|
try {
|
|
14572
|
-
const entry = await this.ensureWorker(
|
|
14573
|
-
return await entry.attachment.storedEpoch();
|
|
16070
|
+
const entry = await this.ensureWorker(format2StoreNamespace(scope ?? {}));
|
|
16071
|
+
return await entry.attachment.storedEpoch(docId);
|
|
14574
16072
|
} catch {
|
|
14575
16073
|
return null;
|
|
14576
16074
|
}
|
|
14577
16075
|
}
|
|
16076
|
+
/**
|
|
16077
|
+
* Every large document this scope's store holds, asked of its worker
|
|
16078
|
+
* (#3760).
|
|
16079
|
+
*
|
|
16080
|
+
* The wipe's first question — a scope store is one pool holding many
|
|
16081
|
+
* documents, so "what is in it" is no longer readable off the OPFS root's
|
|
16082
|
+
* directory names — and the only window onto a shared store's contents from
|
|
16083
|
+
* outside it.
|
|
16084
|
+
*/
|
|
16085
|
+
async format2ListDocuments(scope) {
|
|
16086
|
+
const entry = await this.ensureWorker(format2StoreNamespace(scope ?? {}));
|
|
16087
|
+
return entry.attachment.listDocuments();
|
|
16088
|
+
}
|
|
14578
16089
|
// ── Removing a document's store, and the pool holding it (#3754) ──
|
|
14579
16090
|
/**
|
|
14580
16091
|
* Remove `docId`'s local data, and its pool's directory once it holds
|
|
@@ -14599,21 +16110,26 @@ var OpfsEngine = class {
|
|
|
14599
16110
|
);
|
|
14600
16111
|
}
|
|
14601
16112
|
await this.ensureRoot();
|
|
14602
|
-
const namespace = format2StoreNamespace(
|
|
16113
|
+
const namespace = format2StoreNamespace(options.scope ?? {});
|
|
14603
16114
|
if (this.docs.has(docId)) {
|
|
14604
16115
|
Logger.warn(
|
|
14605
16116
|
`[OpfsEngine] ${docId}: purge refused: held (this tab has it open).`
|
|
14606
16117
|
);
|
|
14607
16118
|
return { purged: false, reason: "held" };
|
|
14608
16119
|
}
|
|
16120
|
+
return this.purgeInNamespace(docId, namespace, options.force);
|
|
16121
|
+
}
|
|
16122
|
+
/**
|
|
16123
|
+
* Purge `docId` out of ONE named store, under its presence lock.
|
|
16124
|
+
*
|
|
16125
|
+
* Parameterized by namespace since #3760, because there are now two kinds
|
|
16126
|
+
* of store to reach: the scope store an evict and an open both use, and a
|
|
16127
|
+
* pre-amendment per-document pool, which only the wipe does.
|
|
16128
|
+
*/
|
|
16129
|
+
async purgeInNamespace(docId, namespace, force) {
|
|
14609
16130
|
const taken = await this.withExclusive(
|
|
14610
16131
|
format2DocumentLockName(namespace, docId),
|
|
14611
|
-
() => this.purgeUnderDocumentLock(
|
|
14612
|
-
docId,
|
|
14613
|
-
namespace,
|
|
14614
|
-
options.scope ?? {},
|
|
14615
|
-
options.force
|
|
14616
|
-
)
|
|
16132
|
+
() => this.purgeUnderDocumentLock(docId, namespace, force)
|
|
14617
16133
|
);
|
|
14618
16134
|
if (!taken.got) {
|
|
14619
16135
|
Logger.warn(
|
|
@@ -14624,13 +16140,28 @@ var OpfsEngine = class {
|
|
|
14624
16140
|
return taken.value;
|
|
14625
16141
|
}
|
|
14626
16142
|
/**
|
|
14627
|
-
* Remove every large document of one scope except `keep` (#3754
|
|
16143
|
+
* Remove every large document of one scope except `keep` (#3754, re-shaped
|
|
16144
|
+
* by #3760).
|
|
14628
16145
|
*
|
|
14629
|
-
*
|
|
14630
|
-
*
|
|
14631
|
-
*
|
|
14632
|
-
*
|
|
14633
|
-
*
|
|
16146
|
+
* Two families of store to reach, and they are swept differently because
|
|
16147
|
+
* they are shaped differently.
|
|
16148
|
+
*
|
|
16149
|
+
* The SCOPE store is one pool holding many documents, so what is in it is
|
|
16150
|
+
* no longer readable off the OPFS root's directory names: the worker is
|
|
16151
|
+
* asked (`listDocuments`). Each document that is not kept then has its
|
|
16152
|
+
* presence lock taken EXCLUSIVE with `ifAvailable` ON THE MAIN THREAD,
|
|
16153
|
+
* BEFORE any worker command — because a frozen tab after a leader change
|
|
16154
|
+
* registers no session anywhere, and that lock is the only thing that
|
|
16155
|
+
* speaks for it (finding 3760-R1, purge-3754 behavior 9b). Only the
|
|
16156
|
+
* documents whose locks were granted are purged, in one queued `purgeAll`
|
|
16157
|
+
* command, and the locks are held across the command, the retire and the
|
|
16158
|
+
* directory removal.
|
|
16159
|
+
*
|
|
16160
|
+
* A PRE-AMENDMENT per-document pool is still exactly one document's data,
|
|
16161
|
+
* and goes through #3754's per-pool path unchanged, on the legacy
|
|
16162
|
+
* namespace: a wipe is the user asking for their data to go, and the docs
|
|
16163
|
+
* promise it reaches a store an older client left behind (decision
|
|
16164
|
+
* 3760-SO-08).
|
|
14634
16165
|
*/
|
|
14635
16166
|
async format2PurgeAll(options) {
|
|
14636
16167
|
if (this.destroyed) {
|
|
@@ -14640,29 +16171,27 @@ var OpfsEngine = class {
|
|
|
14640
16171
|
}
|
|
14641
16172
|
await this.ensureRoot();
|
|
14642
16173
|
const scope = options.scope ?? {};
|
|
14643
|
-
const prefix = format2PoolDirectory(format2StoreNamespacePrefix(scope));
|
|
14644
|
-
const keepDirectories = new Set(
|
|
14645
|
-
(options.keep ?? []).map(
|
|
14646
|
-
(id) => format2PoolDirectory(format2StoreNamespace(id, scope).poolName)
|
|
14647
|
-
)
|
|
14648
|
-
);
|
|
14649
16174
|
const removed = [];
|
|
14650
16175
|
const skipped = [];
|
|
14651
|
-
|
|
14652
|
-
|
|
14653
|
-
|
|
14654
|
-
|
|
14655
|
-
|
|
16176
|
+
const purged = [];
|
|
16177
|
+
const namespace = format2StoreNamespace(scope);
|
|
16178
|
+
const scopeDirectory = format2PoolDirectory(namespace.poolName);
|
|
16179
|
+
const legacyPrefix = format2PoolDirectory(format2LegacyPoolPrefix(scope));
|
|
16180
|
+
for (const directory of await this.listPoolDirectories(legacyPrefix)) {
|
|
16181
|
+
if (directory === scopeDirectory) continue;
|
|
14656
16182
|
const poolName = directory.slice(1);
|
|
14657
16183
|
const docId = format2DocumentIdFromPoolName(poolName, scope);
|
|
14658
16184
|
if (docId === null) {
|
|
14659
16185
|
skipped.push({ pool: directory, reason: "unrecognized" });
|
|
14660
16186
|
continue;
|
|
14661
16187
|
}
|
|
14662
|
-
|
|
14663
|
-
|
|
14664
|
-
|
|
14665
|
-
}
|
|
16188
|
+
if ((options.keep ?? []).includes(docId)) {
|
|
16189
|
+
skipped.push({ pool: directory, reason: "keep" });
|
|
16190
|
+
continue;
|
|
16191
|
+
}
|
|
16192
|
+
const legacy = format2LegacyStoreNamespace(docId, scope);
|
|
16193
|
+
const result = await this.purgeInNamespace(docId, legacy, options.force);
|
|
16194
|
+
if (result.purged) purged.push(docId);
|
|
14666
16195
|
if (result.purged && result.poolRemoved) {
|
|
14667
16196
|
removed.push(directory);
|
|
14668
16197
|
continue;
|
|
@@ -14670,13 +16199,170 @@ var OpfsEngine = class {
|
|
|
14670
16199
|
const reason = result.purged ? "kept-pool" : result.reason;
|
|
14671
16200
|
skipped.push({ pool: directory, reason });
|
|
14672
16201
|
Logger.warn(
|
|
14673
|
-
`[OpfsEngine] the sweep skipped the pool '${directory}': ${reason}.`
|
|
16202
|
+
`[OpfsEngine] the sweep skipped the pre-amendment pool '${directory}': ${reason}.`
|
|
14674
16203
|
);
|
|
14675
16204
|
}
|
|
16205
|
+
await this.sweepScopeStore(namespace, options, {
|
|
16206
|
+
removed,
|
|
16207
|
+
skipped,
|
|
16208
|
+
purged
|
|
16209
|
+
});
|
|
14676
16210
|
Logger.info(
|
|
14677
|
-
`[OpfsEngine] large-document sweep removed ${removed.length} pool(s), skipped ${skipped.length}.`
|
|
16211
|
+
`[OpfsEngine] large-document sweep purged ${purged.length} document(s), removed ${removed.length} pool(s), skipped ${skipped.length}.`
|
|
16212
|
+
);
|
|
16213
|
+
return { removed, skipped, purged };
|
|
16214
|
+
}
|
|
16215
|
+
/**
|
|
16216
|
+
* The scope store's half of a sweep: its documents, its locks, one command
|
|
16217
|
+
* (#3760).
|
|
16218
|
+
*/
|
|
16219
|
+
async sweepScopeStore(namespace, options, report) {
|
|
16220
|
+
const directory = format2PoolDirectory(namespace.poolName);
|
|
16221
|
+
const root = this.opfsRoot();
|
|
16222
|
+
if (root && !await this.directoryExists(root, directory)) {
|
|
16223
|
+
Logger.debug(
|
|
16224
|
+
`[OpfsEngine] the scope store '${directory}' holds nothing (no pool directory), so the sweep skips it.`
|
|
16225
|
+
);
|
|
16226
|
+
return;
|
|
16227
|
+
}
|
|
16228
|
+
let entry;
|
|
16229
|
+
try {
|
|
16230
|
+
entry = await this.ensureWorker(namespace);
|
|
16231
|
+
} catch (error) {
|
|
16232
|
+
const reason = error?.message ?? String(error);
|
|
16233
|
+
Logger.warn(
|
|
16234
|
+
`[OpfsEngine] the scope store '${directory}' could not be reached (${reason}), so nothing in it was swept.`
|
|
16235
|
+
);
|
|
16236
|
+
report.skipped.push({ pool: directory, reason: "unreachable" });
|
|
16237
|
+
return;
|
|
16238
|
+
}
|
|
16239
|
+
let stored;
|
|
16240
|
+
try {
|
|
16241
|
+
stored = await entry.attachment.listDocuments();
|
|
16242
|
+
} catch (error) {
|
|
16243
|
+
const reason = error?.message ?? String(error);
|
|
16244
|
+
Logger.warn(
|
|
16245
|
+
`[OpfsEngine] the scope store '${directory}' could not list its documents (${reason}), so nothing in it was swept.`
|
|
16246
|
+
);
|
|
16247
|
+
report.skipped.push({ pool: directory, reason: "unreachable" });
|
|
16248
|
+
return;
|
|
16249
|
+
}
|
|
16250
|
+
const keep = new Set(options.keep ?? []);
|
|
16251
|
+
const wanted = [];
|
|
16252
|
+
for (const docId of stored) {
|
|
16253
|
+
if (keep.has(docId)) report.skipped.push({ pool: docId, reason: "keep" });
|
|
16254
|
+
else wanted.push(docId);
|
|
16255
|
+
}
|
|
16256
|
+
await this.withExclusiveAll(
|
|
16257
|
+
wanted.map((docId) => format2DocumentLockName(namespace, docId)),
|
|
16258
|
+
async (granted) => {
|
|
16259
|
+
const acquired = wanted.filter((_, at) => granted[at]);
|
|
16260
|
+
for (const [at, docId] of wanted.entries()) {
|
|
16261
|
+
if (granted[at]) continue;
|
|
16262
|
+
Logger.warn(
|
|
16263
|
+
`[OpfsEngine] ${docId}: the sweep skipped it: held (another tab holds its presence lock, so it still has the document open).`
|
|
16264
|
+
);
|
|
16265
|
+
report.skipped.push({ pool: docId, reason: "held" });
|
|
16266
|
+
}
|
|
16267
|
+
let reply;
|
|
16268
|
+
try {
|
|
16269
|
+
reply = await entry.attachment.purgeAll(acquired, options.force);
|
|
16270
|
+
} catch (error) {
|
|
16271
|
+
const reason = error?.message ?? String(error);
|
|
16272
|
+
Logger.warn(
|
|
16273
|
+
`[OpfsEngine] the scope store '${directory}' could not be swept (${reason}).`
|
|
16274
|
+
);
|
|
16275
|
+
report.skipped.push({ pool: directory, reason: "unreachable" });
|
|
16276
|
+
return;
|
|
16277
|
+
}
|
|
16278
|
+
report.purged.push(...reply.purged);
|
|
16279
|
+
for (const kept of reply.kept) {
|
|
16280
|
+
Logger.warn(
|
|
16281
|
+
`[OpfsEngine] ${kept.docId}: the sweep kept it: ${kept.reason}` + (kept.reason === "pending" ? ` (${kept.pendingOps ?? 0} unacknowledged write(s); sweep with force to drop them)` : ` (${kept.holders ?? 0} session(s) on the pool's worker)`) + "."
|
|
16282
|
+
);
|
|
16283
|
+
report.skipped.push({ pool: kept.docId, reason: kept.reason });
|
|
16284
|
+
}
|
|
16285
|
+
if (reply.remainingDocuments > 0) {
|
|
16286
|
+
Logger.debug(
|
|
16287
|
+
`[OpfsEngine] the scope store still holds ${reply.remainingDocuments} document(s), so '${directory}' stays.`
|
|
16288
|
+
);
|
|
16289
|
+
report.skipped.push({ pool: directory, reason: "kept-pool" });
|
|
16290
|
+
return;
|
|
16291
|
+
}
|
|
16292
|
+
const owing = await this.legacyPoolsStillOnDisk(reply.legacyPools ?? []);
|
|
16293
|
+
if (owing.length > 0) {
|
|
16294
|
+
Logger.info(
|
|
16295
|
+
`[OpfsEngine] the scope store '${directory}' holds no documents, but it stays: it is the only record that ${owing.join(", ")} has already been adopted from.`
|
|
16296
|
+
);
|
|
16297
|
+
report.skipped.push({ pool: directory, reason: "kept-pool" });
|
|
16298
|
+
return;
|
|
16299
|
+
}
|
|
16300
|
+
const gone = await this.retireAndRemove(namespace, entry, directory);
|
|
16301
|
+
if (gone) report.removed.push(directory);
|
|
16302
|
+
else report.skipped.push({ pool: directory, reason: "kept-pool" });
|
|
16303
|
+
}
|
|
16304
|
+
);
|
|
16305
|
+
}
|
|
16306
|
+
/**
|
|
16307
|
+
* Retire a namespace's worker and remove its directory, under the leader
|
|
16308
|
+
* lock this tab already holds (#3754, lifted out for the scope sweep).
|
|
16309
|
+
*/
|
|
16310
|
+
async retireAndRemove(namespace, entry, directory) {
|
|
16311
|
+
if (!entry.attachment.leads) {
|
|
16312
|
+
Logger.debug(
|
|
16313
|
+
`[OpfsEngine] another tab leads '${directory}', so its directory is left to that tab.`
|
|
16314
|
+
);
|
|
16315
|
+
return false;
|
|
16316
|
+
}
|
|
16317
|
+
let retired;
|
|
16318
|
+
try {
|
|
16319
|
+
retired = await entry.attachment.retire();
|
|
16320
|
+
} catch (error) {
|
|
16321
|
+
Logger.warn(
|
|
16322
|
+
`[OpfsEngine] '${directory}' could not be retired (${error?.message ?? String(error)}), so its directory stays.`
|
|
16323
|
+
);
|
|
16324
|
+
return false;
|
|
16325
|
+
}
|
|
16326
|
+
if (!retired.retired) {
|
|
16327
|
+
Logger.warn(
|
|
16328
|
+
`[OpfsEngine] '${directory}' was not retired \u2014 ${retired.holders} session(s) on its worker still hold a document open, so its directory stays.`
|
|
16329
|
+
);
|
|
16330
|
+
return false;
|
|
16331
|
+
}
|
|
16332
|
+
const poolRemoved = await this.removePoolDirectory(
|
|
16333
|
+
namespace,
|
|
16334
|
+
namespace.poolName,
|
|
16335
|
+
entry
|
|
14678
16336
|
);
|
|
14679
|
-
|
|
16337
|
+
this.workers.delete(namespace.poolName);
|
|
16338
|
+
this.attachments.delete(entry.attachment);
|
|
16339
|
+
await entry.attachment.destroy({ alreadyRetired: true }).catch(() => {
|
|
16340
|
+
});
|
|
16341
|
+
return poolRemoved;
|
|
16342
|
+
}
|
|
16343
|
+
/**
|
|
16344
|
+
* Hold every name in `names` exclusively (with `ifAvailable`) for the
|
|
16345
|
+
* length of `run`, telling it which were granted (#3760).
|
|
16346
|
+
*
|
|
16347
|
+
* Nested rather than sequential: the whole point is that the locks are all
|
|
16348
|
+
* held AT ONCE, across the one `purgeAll` command, the retire and the
|
|
16349
|
+
* directory removal, so a tab cannot open a document between the moment the
|
|
16350
|
+
* sweep decided to take it and the moment its pool is gone.
|
|
16351
|
+
*/
|
|
16352
|
+
async withExclusiveAll(names, run2) {
|
|
16353
|
+
const granted = [];
|
|
16354
|
+
const next = async (at) => {
|
|
16355
|
+
if (at >= names.length) return run2(granted);
|
|
16356
|
+
const taken = await this.withExclusive(names[at], async () => {
|
|
16357
|
+
granted[at] = true;
|
|
16358
|
+
await next(at + 1);
|
|
16359
|
+
});
|
|
16360
|
+
if (!taken.got) {
|
|
16361
|
+
granted[at] = false;
|
|
16362
|
+
await next(at + 1);
|
|
16363
|
+
}
|
|
16364
|
+
};
|
|
16365
|
+
await next(0);
|
|
14680
16366
|
}
|
|
14681
16367
|
/**
|
|
14682
16368
|
* The purge itself, with this tab holding the document lock exclusively.
|
|
@@ -14686,16 +16372,25 @@ var OpfsEngine = class {
|
|
|
14686
16372
|
* `routing.sessions` say what THIS tab serves, while the worker's session map
|
|
14687
16373
|
* says what every tab does (finding 3754-SO-03).
|
|
14688
16374
|
*/
|
|
14689
|
-
async purgeUnderDocumentLock(docId, namespace,
|
|
16375
|
+
async purgeUnderDocumentLock(docId, namespace, force) {
|
|
14690
16376
|
const existing = this.workers.get(namespace.poolName);
|
|
14691
16377
|
const attached = existing && !existing.attachment.died ? existing : null;
|
|
14692
16378
|
if (!attached) {
|
|
14693
|
-
return this.purgeWithoutAttachment(docId, namespace,
|
|
16379
|
+
return this.purgeWithoutAttachment(docId, namespace, force);
|
|
14694
16380
|
}
|
|
14695
16381
|
return this.purgeThroughAttachment(docId, namespace, attached, force);
|
|
14696
16382
|
}
|
|
14697
|
-
/**
|
|
14698
|
-
|
|
16383
|
+
/**
|
|
16384
|
+
* No attachment for the namespace: what is on disk decides what happens.
|
|
16385
|
+
*
|
|
16386
|
+
* There is no forced shortcut that removes the directory instead of purging
|
|
16387
|
+
* the store. #3754 had one, on the premise that "a per-document pool IS
|
|
16388
|
+
* exactly this document's data" — under one store per scope that premise is
|
|
16389
|
+
* false, and taking the directory would take every other document of the
|
|
16390
|
+
* user with it. The worker is spawned and the rows purged through it,
|
|
16391
|
+
* forced or not (decision 3760-SO-06).
|
|
16392
|
+
*/
|
|
16393
|
+
async purgeWithoutAttachment(docId, namespace, force) {
|
|
14699
16394
|
const directory = format2PoolDirectory(namespace.poolName);
|
|
14700
16395
|
const root = this.opfsRoot();
|
|
14701
16396
|
if (root) {
|
|
@@ -14706,24 +16401,10 @@ var OpfsEngine = class {
|
|
|
14706
16401
|
);
|
|
14707
16402
|
return { purged: true, nothingStored: true, poolRemoved: false };
|
|
14708
16403
|
}
|
|
14709
|
-
if (force) {
|
|
14710
|
-
const under = await this.withExclusive(
|
|
14711
|
-
namespace.lockName,
|
|
14712
|
-
() => this.removePoolDirectory(namespace, docId, null)
|
|
14713
|
-
);
|
|
14714
|
-
if (under.got && under.value) {
|
|
14715
|
-
return { purged: true, poolRemoved: true, pendingOps: 0 };
|
|
14716
|
-
}
|
|
14717
|
-
if (under.got) {
|
|
14718
|
-
Logger.warn(
|
|
14719
|
-
`[OpfsEngine] ${docId}: its pool directory could not be removed directly, so its store is purged through a worker instead.`
|
|
14720
|
-
);
|
|
14721
|
-
}
|
|
14722
|
-
}
|
|
14723
16404
|
}
|
|
14724
16405
|
let entry;
|
|
14725
16406
|
try {
|
|
14726
|
-
entry = await this.ensureWorker(
|
|
16407
|
+
entry = await this.ensureWorker(namespace);
|
|
14727
16408
|
} catch (error) {
|
|
14728
16409
|
const reason = error?.message ?? String(error);
|
|
14729
16410
|
Logger.warn(
|
|
@@ -14770,6 +16451,13 @@ var OpfsEngine = class {
|
|
|
14770
16451
|
);
|
|
14771
16452
|
return { ...purgedReport(reply), poolRemoved: false };
|
|
14772
16453
|
}
|
|
16454
|
+
const owing = await this.legacyPoolsStillOnDisk(reply.legacyPools ?? []);
|
|
16455
|
+
if (owing.length > 0) {
|
|
16456
|
+
Logger.info(
|
|
16457
|
+
`[OpfsEngine] ${docId}: its store holds no documents, but its directory stays: it is the only record that ${owing.join(", ")} has already been adopted from. A wipe of this user's data reclaims both.`
|
|
16458
|
+
);
|
|
16459
|
+
return { ...purgedReport(reply), poolRemoved: false };
|
|
16460
|
+
}
|
|
14773
16461
|
if (!entry.attachment.leads) {
|
|
14774
16462
|
Logger.debug(
|
|
14775
16463
|
`[OpfsEngine] ${docId}: another tab leads its pool, so its directory is left to that tab.`
|
|
@@ -14860,10 +16548,10 @@ var OpfsEngine = class {
|
|
|
14860
16548
|
/**
|
|
14861
16549
|
* Resolve `navigator.storage.getDirectory()` once, if this platform has it.
|
|
14862
16550
|
*
|
|
14863
|
-
* Awaited at the top of every purge
|
|
14864
|
-
* is built before anybody has asked it for
|
|
14865
|
-
* kept on a field would be an unhandled
|
|
14866
|
-
*
|
|
16551
|
+
* Awaited at the top of every purge and of the legacy-pool probe rather than
|
|
16552
|
+
* in the constructor: an engine is built before anybody has asked it for
|
|
16553
|
+
* anything, and a rejected promise kept on a field would be an unhandled
|
|
16554
|
+
* rejection in every app that never reaches the root at all.
|
|
14867
16555
|
*/
|
|
14868
16556
|
async ensureRoot() {
|
|
14869
16557
|
if (this.internals.opfsRoot !== void 0 || this.resolvedRoot) return;
|
|
@@ -14877,6 +16565,26 @@ var OpfsEngine = class {
|
|
|
14877
16565
|
);
|
|
14878
16566
|
}
|
|
14879
16567
|
}
|
|
16568
|
+
/**
|
|
16569
|
+
* Which of `pools` still has a directory in the OPFS root (#3760).
|
|
16570
|
+
*
|
|
16571
|
+
* A ledger row for a pool whose directory is gone says nothing anybody can
|
|
16572
|
+
* act on — there is no source left to take a write from — so it does not keep
|
|
16573
|
+
* the scope store's directory alive. With no root at all nothing names any
|
|
16574
|
+
* of them either: `removePoolDirectory` cannot remove a directory it has no
|
|
16575
|
+
* root for, so the ledger is in no danger and this answers none.
|
|
16576
|
+
*/
|
|
16577
|
+
async legacyPoolsStillOnDisk(pools) {
|
|
16578
|
+
if (pools.length === 0) return [];
|
|
16579
|
+
const root = this.opfsRoot();
|
|
16580
|
+
if (!root) return [];
|
|
16581
|
+
const present = [];
|
|
16582
|
+
for (const pool of pools) {
|
|
16583
|
+
const directory = format2PoolDirectory(pool);
|
|
16584
|
+
if (await this.directoryExists(root, directory)) present.push(directory);
|
|
16585
|
+
}
|
|
16586
|
+
return present;
|
|
16587
|
+
}
|
|
14880
16588
|
/** Whether `name` is a directory of the root, without creating it. */
|
|
14881
16589
|
async directoryExists(root, name) {
|
|
14882
16590
|
try {
|
|
@@ -15109,18 +16817,20 @@ var OpfsEngine = class {
|
|
|
15109
16817
|
await this.mirror.close();
|
|
15110
16818
|
}
|
|
15111
16819
|
/**
|
|
15112
|
-
* The attachment for
|
|
15113
|
-
* it yet.
|
|
16820
|
+
* The attachment for one scope's namespace, joined if this tab has not
|
|
16821
|
+
* joined it yet.
|
|
15114
16822
|
*
|
|
15115
16823
|
* One attachment per namespace, whatever role this tab ends up in: a
|
|
15116
16824
|
* document is routed to the attachment, not to a worker, because a leader
|
|
15117
|
-
* change replaces the worker under it and the rows do not move.
|
|
16825
|
+
* change replaces the worker under it and the rows do not move. Since
|
|
16826
|
+
* #3760 a namespace is a SCOPE, so every large document of one user shares
|
|
16827
|
+
* this entry — and because the attachment re-serves every session on each
|
|
16828
|
+
* new generation, a leader change hands over all of them at once.
|
|
15118
16829
|
*/
|
|
15119
|
-
ensureWorker(
|
|
16830
|
+
ensureWorker(namespace) {
|
|
15120
16831
|
if (this.destroyed) {
|
|
15121
16832
|
return Promise.reject(new Error("the opfs engine was destroyed"));
|
|
15122
16833
|
}
|
|
15123
|
-
const namespace = format2StoreNamespace(docId, scope ?? {});
|
|
15124
16834
|
const existing = this.workers.get(namespace.poolName);
|
|
15125
16835
|
if (existing && !existing.attachment.died) return Promise.resolve(existing);
|
|
15126
16836
|
const pending = this.opening.get(namespace.poolName);
|
|
@@ -15130,7 +16840,6 @@ var OpfsEngine = class {
|
|
|
15130
16840
|
namespace,
|
|
15131
16841
|
clientId: this.tabId,
|
|
15132
16842
|
openRequest: {
|
|
15133
|
-
docId,
|
|
15134
16843
|
poolName: namespace.poolName,
|
|
15135
16844
|
dbFile: namespace.dbFile,
|
|
15136
16845
|
poolLockName: namespace.poolLockName,
|
|
@@ -15408,12 +17117,13 @@ init_BaseModel();
|
|
|
15408
17117
|
init_OrmContext();
|
|
15409
17118
|
|
|
15410
17119
|
// src/models/format2DocumentBinding.ts
|
|
15411
|
-
var
|
|
17120
|
+
var Y9 = __toESM(require("yjs"), 1);
|
|
15412
17121
|
init_BaseModel();
|
|
15413
17122
|
init_OrmContext();
|
|
15414
17123
|
init_format2RecordStoreAsync();
|
|
15415
17124
|
init_format2StoreHost();
|
|
15416
17125
|
init_format2Observer();
|
|
17126
|
+
init_format2OverlayKeysSince();
|
|
15417
17127
|
|
|
15418
17128
|
// src/models/format2PendingRestore.ts
|
|
15419
17129
|
init_largeDocuments();
|
|
@@ -15617,7 +17327,14 @@ function sameOverlayValue(a, b) {
|
|
|
15617
17327
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
15618
17328
|
}
|
|
15619
17329
|
function decideDeferredReplay(note, epoch, ackedSeq) {
|
|
15620
|
-
if (!note
|
|
17330
|
+
if (!note) return { kind: "none" };
|
|
17331
|
+
if (note.kind === "legacy") {
|
|
17332
|
+
if (note.throughSeq <= ackedSeq) {
|
|
17333
|
+
return { kind: "clear", reason: "acknowledged", note };
|
|
17334
|
+
}
|
|
17335
|
+
return { kind: "honour", note, clamped: false };
|
|
17336
|
+
}
|
|
17337
|
+
if (note.kind !== "ordinary") return { kind: "none" };
|
|
15621
17338
|
let resolved = note;
|
|
15622
17339
|
let clamped = false;
|
|
15623
17340
|
if (note.toEpoch > epoch) {
|
|
@@ -15969,8 +17686,12 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
15969
17686
|
* first one and answer out of a database it has nothing to do with.
|
|
15970
17687
|
*/
|
|
15971
17688
|
opened = /* @__PURE__ */ new Map();
|
|
15972
|
-
/**
|
|
15973
|
-
|
|
17689
|
+
/**
|
|
17690
|
+
* Models a fold batch touched, per document, awaiting their one
|
|
17691
|
+
* notification (#3782). A batch reports its records one by one; the
|
|
17692
|
+
* listeners hear once per model per batch, after the projection landed.
|
|
17693
|
+
*/
|
|
17694
|
+
foldedModels = /* @__PURE__ */ new Map();
|
|
15974
17695
|
/**
|
|
15975
17696
|
* Refuse a large document this device cannot keep, before anything is wired
|
|
15976
17697
|
* to it (#2816, sponsor decision B of 2026-08-27).
|
|
@@ -16035,19 +17756,16 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
16035
17756
|
if (Number.isInteger(epoch) && epoch > store.epoch()) {
|
|
16036
17757
|
store.setEpoch(epoch);
|
|
16037
17758
|
}
|
|
17759
|
+
store.trackOverlay(yDoc);
|
|
16038
17760
|
this.unobservers.set(
|
|
16039
17761
|
docId,
|
|
16040
17762
|
observeFormat2Overlay(
|
|
16041
17763
|
yDoc,
|
|
16042
17764
|
store,
|
|
16043
|
-
(modelName
|
|
17765
|
+
(modelName) => this.noteFolded(docId, modelName)
|
|
16044
17766
|
)
|
|
16045
17767
|
);
|
|
16046
|
-
|
|
16047
|
-
yDoc,
|
|
16048
|
-
store,
|
|
16049
|
-
(modelName, recordId) => this.enqueueProjection(docId, modelName, recordId)
|
|
16050
|
-
);
|
|
17768
|
+
this.catchUp(docId, yDoc, store);
|
|
16051
17769
|
await store.whenBatchesSettled();
|
|
16052
17770
|
if (options.restorePending !== false) {
|
|
16053
17771
|
const verdicts = await this.adoptOrphanedWrites(docId, yDoc, store);
|
|
@@ -16071,11 +17789,7 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
16071
17789
|
}
|
|
16072
17790
|
const adoptedWasRestored = verdicts !== void 0 && restored.restored.some((seq) => verdicts.has(seq));
|
|
16073
17791
|
if (adoptedWasRestored) {
|
|
16074
|
-
|
|
16075
|
-
yDoc,
|
|
16076
|
-
store,
|
|
16077
|
-
(modelName, recordId) => this.enqueueProjection(docId, modelName, recordId)
|
|
16078
|
-
);
|
|
17792
|
+
this.catchUp(docId, yDoc, store);
|
|
16079
17793
|
await store.whenBatchesSettled();
|
|
16080
17794
|
}
|
|
16081
17795
|
if (restored.unreproducible.length > 0) {
|
|
@@ -16092,6 +17806,42 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
16092
17806
|
store.onReattached(() => this.revalidateQueryTables(docId, store))
|
|
16093
17807
|
);
|
|
16094
17808
|
}
|
|
17809
|
+
/**
|
|
17810
|
+
* Fold what `yDoc` holds that the store does not (#3782).
|
|
17811
|
+
*
|
|
17812
|
+
* - Nothing is known about what the store holds (no folded state), or the
|
|
17813
|
+
* facade is fold-broken: the WHOLE overlay, as every bind did before —
|
|
17814
|
+
* for a broken fold that is the repair.
|
|
17815
|
+
* - Otherwise the items above the stored vector name the keys that moved;
|
|
17816
|
+
* those are folded as one ordinary batch (materialized and completed by
|
|
17817
|
+
* the observer's own path), with any hydrated model the stored state
|
|
17818
|
+
* never covered folded whole. A document that did not move is sent
|
|
17819
|
+
* nothing at all.
|
|
17820
|
+
*
|
|
17821
|
+
* Either way the batch is a bind's: a document behind the store is folded
|
|
17822
|
+
* forced rather than held, because a bind resolves with what local
|
|
17823
|
+
* persistence gave it.
|
|
17824
|
+
*/
|
|
17825
|
+
catchUp(docId, yDoc, store) {
|
|
17826
|
+
const onFolded = (modelName) => this.noteFolded(docId, modelName);
|
|
17827
|
+
const stored = store.foldedState();
|
|
17828
|
+
if (stored === null || store.health() === "fold-broken") {
|
|
17829
|
+
foldFormat2Overlay(yDoc, store, onFolded, { atBind: true });
|
|
17830
|
+
return;
|
|
17831
|
+
}
|
|
17832
|
+
const diff = overlayKeysSince(yDoc, stored, (model) => store.isHydrated(model));
|
|
17833
|
+
if (!diff.moved) {
|
|
17834
|
+
Logger.debug(
|
|
17835
|
+
`[format2] ${docId}: the overlay is unchanged since the last fold; nothing to fold.`
|
|
17836
|
+
);
|
|
17837
|
+
return;
|
|
17838
|
+
}
|
|
17839
|
+
const keys = new Map(diff.keys);
|
|
17840
|
+
for (const model of diff.uncovered) {
|
|
17841
|
+
keys.set(model, new Set(yDoc.getMap(model).keys()));
|
|
17842
|
+
}
|
|
17843
|
+
store.enqueueOverlayBatch(yDoc, keys, onFolded, { atBind: true });
|
|
17844
|
+
}
|
|
16095
17845
|
/**
|
|
16096
17846
|
* Settle what a deferred judgement means for THIS bind (#3755).
|
|
16097
17847
|
*
|
|
@@ -16139,10 +17889,10 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
16139
17889
|
*
|
|
16140
17890
|
* Ordinarily nothing: {@link hydrateQueryTables} skips every model whose
|
|
16141
17891
|
* projection mark is current, so a re-attachment that lost nothing costs one
|
|
16142
|
-
* mark read per model.
|
|
16143
|
-
*
|
|
16144
|
-
* the marks are gone with the rows and the
|
|
16145
|
-
*
|
|
17892
|
+
* mark read per model. A close removes nothing (#3756), but a PURGE can run
|
|
17893
|
+
* while this tab is frozen — it counted the sessions of a worker this tab
|
|
17894
|
+
* had not reached yet — and then the marks are gone with the rows and the
|
|
17895
|
+
* tables are rebuilt from the merged view.
|
|
16146
17896
|
*
|
|
16147
17897
|
* Serialized per document and never awaited by the engine: the re-attachment
|
|
16148
17898
|
* is a notification, not a command, and a rebuild that takes a while must not
|
|
@@ -16185,13 +17935,9 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
16185
17935
|
`[DocumentManager] Cannot apply a sealed overlay to ${docId}: it has no merged record view`
|
|
16186
17936
|
);
|
|
16187
17937
|
}
|
|
16188
|
-
const doc = new
|
|
16189
|
-
|
|
16190
|
-
foldFormat2Overlay(
|
|
16191
|
-
doc,
|
|
16192
|
-
store,
|
|
16193
|
-
(modelName, recordId) => this.enqueueProjection(docId, modelName, recordId)
|
|
16194
|
-
);
|
|
17938
|
+
const doc = new Y9.Doc();
|
|
17939
|
+
Y9.applyUpdate(doc, update);
|
|
17940
|
+
foldFormat2Overlay(doc, store, (modelName) => this.noteFolded(docId, modelName));
|
|
16195
17941
|
await store.whenBatchesSettled();
|
|
16196
17942
|
doc.destroy();
|
|
16197
17943
|
await this.whenProjectionSettled();
|
|
@@ -16217,11 +17963,7 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
16217
17963
|
`[DocumentManager] Cannot re-fold the overlay of ${docId}: it has no merged record view`
|
|
16218
17964
|
);
|
|
16219
17965
|
}
|
|
16220
|
-
foldFormat2Overlay(
|
|
16221
|
-
yDoc,
|
|
16222
|
-
store,
|
|
16223
|
-
(modelName, recordId) => this.enqueueProjection(docId, modelName, recordId)
|
|
16224
|
-
);
|
|
17966
|
+
foldFormat2Overlay(yDoc, store, (modelName) => this.noteFolded(docId, modelName));
|
|
16225
17967
|
await this.whenProjectionSettled();
|
|
16226
17968
|
}
|
|
16227
17969
|
/**
|
|
@@ -16374,7 +18116,10 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
16374
18116
|
);
|
|
16375
18117
|
return;
|
|
16376
18118
|
}
|
|
16377
|
-
await this.hydrateQueryTables(docId, store, {
|
|
18119
|
+
await this.hydrateQueryTables(docId, store, {
|
|
18120
|
+
only: modelName,
|
|
18121
|
+
reason: "registered on a running instance"
|
|
18122
|
+
});
|
|
16378
18123
|
await this.whenProjectionSettled();
|
|
16379
18124
|
}
|
|
16380
18125
|
localModels(docId) {
|
|
@@ -16421,28 +18166,54 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
16421
18166
|
/**
|
|
16422
18167
|
* Release `docId`. Safe on a document that was never format 2.
|
|
16423
18168
|
*
|
|
16424
|
-
*
|
|
16425
|
-
*
|
|
16426
|
-
*
|
|
18169
|
+
* A release removes nothing (#3756): the records, the unacknowledged
|
|
18170
|
+
* writes, the query tables and their projection marks all stay, so the next
|
|
18171
|
+
* open projects nothing — exactly as a reload that never called close. The
|
|
18172
|
+
* store purge behind the eviction doors is what removes them.
|
|
18173
|
+
*
|
|
18174
|
+
* A kept mark must vouch only for rows that LANDED, so the order matters
|
|
18175
|
+
* (decision D4):
|
|
18176
|
+
*
|
|
18177
|
+
* 1. stop listening, so no NEW fold is queued;
|
|
18178
|
+
* 2. settle the local writes already admitted — commit and projection — and
|
|
18179
|
+
* the folds already queued on the facade, while the facade is
|
|
18180
|
+
* still registered — a fold notifies its records after its
|
|
18181
|
+
* `applyRemoteBatch`, and a projection enqueued against an unregistered
|
|
18182
|
+
* document is dropped silently;
|
|
18183
|
+
* 3. settle the projection queue those folds fed. A projection that fails
|
|
18184
|
+
* here withdraws its model's mark through the still-open facade, and the
|
|
18185
|
+
* next bind rebuilds that model;
|
|
18186
|
+
* 4. only then unregister the facade and close it (its close drains what
|
|
18187
|
+
* the app still has in flight).
|
|
16427
18188
|
*/
|
|
16428
|
-
async disconnect(docId
|
|
18189
|
+
async disconnect(docId) {
|
|
16429
18190
|
this.unobservers.get(docId)?.();
|
|
16430
18191
|
this.unobservers.delete(docId);
|
|
16431
18192
|
this.reattachSubscriptions.get(docId)?.();
|
|
16432
18193
|
this.reattachSubscriptions.delete(docId);
|
|
16433
18194
|
const store = this.opened.get(docId);
|
|
18195
|
+
if (!store) return;
|
|
18196
|
+
try {
|
|
18197
|
+
do {
|
|
18198
|
+
await store.whenLocalWritesSettled();
|
|
18199
|
+
await store.whenBatchesSettled();
|
|
18200
|
+
} while (store.localWritesInFlight() > 0);
|
|
18201
|
+
await store.flushLocalStamp();
|
|
18202
|
+
} catch (error) {
|
|
18203
|
+
Logger.error(
|
|
18204
|
+
`[DocumentManager] ${docId}: settling its queued folds before the release failed:`,
|
|
18205
|
+
error
|
|
18206
|
+
);
|
|
18207
|
+
}
|
|
18208
|
+
store.refuseLocalWrites();
|
|
16434
18209
|
this.opened.delete(docId);
|
|
16435
18210
|
if (this.ormContext.format2Documents.get(docId) === store) {
|
|
16436
18211
|
this.ormContext.format2Documents.delete(docId);
|
|
16437
18212
|
}
|
|
16438
|
-
|
|
16439
|
-
const result = await store.close(
|
|
16440
|
-
options.wipeIfLastModels ? { wipeIfLast: { models: options.wipeIfLastModels } } : {}
|
|
16441
|
-
);
|
|
18213
|
+
await store.close();
|
|
16442
18214
|
Logger.info(
|
|
16443
|
-
`[DocumentManager] ${docId}: released its merged view;
|
|
18215
|
+
`[DocumentManager] ${docId}: released its merged view; its query tables and projection marks are kept for the next open.`
|
|
16444
18216
|
);
|
|
16445
|
-
return result;
|
|
16446
18217
|
}
|
|
16447
18218
|
/**
|
|
16448
18219
|
* Take over the unacknowledged writes of clients that are gone, and decide
|
|
@@ -16511,28 +18282,6 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
16511
18282
|
`[DocumentManager] ${docId}: adopted write #${op.seq} (${where}, from client ${op.fromClientId} seq ${op.fromSeq}) is restored in part: ${kept.join(", ")} are still owed, and its other keys were overwritten after it.`
|
|
16512
18283
|
);
|
|
16513
18284
|
}
|
|
16514
|
-
/**
|
|
16515
|
-
* Remove `docId` from `models`' query tables, in THIS thread.
|
|
16516
|
-
*
|
|
16517
|
-
* The in-process host's `wipeIfLast` hook. Exactly the two steps
|
|
16518
|
-
* `removeDocumentData` used to take for a large document, moved here so the
|
|
16519
|
-
* observable result is the same wherever the store runs.
|
|
16520
|
-
*/
|
|
16521
|
-
async wipeQueryTables(docId, models) {
|
|
16522
|
-
const classes = this.modelRegistry.getActiveModels();
|
|
16523
|
-
for (const modelName of models) {
|
|
16524
|
-
const modelClass = classes.get(modelName);
|
|
16525
|
-
try {
|
|
16526
|
-
const engine = typeof modelClass?.queryEngineFor === "function" && modelClass?.dbInstance ? await modelClass.queryEngineFor([docId], modelName) : modelClass?.dbInstance ?? this.dbEngine;
|
|
16527
|
-
await engine.deleteByDocumentId(modelName, docId);
|
|
16528
|
-
} catch (error) {
|
|
16529
|
-
Logger.error(
|
|
16530
|
-
`[DocumentManager] ${docId}: could not remove '${modelName}' rows:`,
|
|
16531
|
-
error
|
|
16532
|
-
);
|
|
16533
|
-
}
|
|
16534
|
-
}
|
|
16535
|
-
}
|
|
16536
18285
|
/**
|
|
16537
18286
|
* Close every open document. Used by `destroy()` and `resetJsBao()`.
|
|
16538
18287
|
*
|
|
@@ -16572,7 +18321,6 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
16572
18321
|
for (const store of Array.from(this.opened.values())) {
|
|
16573
18322
|
await store.whenBatchesSettled();
|
|
16574
18323
|
}
|
|
16575
|
-
await this.queue;
|
|
16576
18324
|
}
|
|
16577
18325
|
/**
|
|
16578
18326
|
* Rebuild the derived query tables from the merged view.
|
|
@@ -16639,56 +18387,67 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
16639
18387
|
continue;
|
|
16640
18388
|
}
|
|
16641
18389
|
await store.forgetQueryProjection(modelName);
|
|
18390
|
+
const started = Date.now();
|
|
18391
|
+
let rows = 0;
|
|
16642
18392
|
let after = null;
|
|
16643
18393
|
for (; ; ) {
|
|
16644
|
-
const page = await store.
|
|
18394
|
+
const page = await store.recordIdsAfter(
|
|
16645
18395
|
modelName,
|
|
16646
18396
|
after,
|
|
16647
18397
|
QUERY_HYDRATION_PAGE_ROWS
|
|
16648
18398
|
);
|
|
16649
18399
|
if (page.length === 0) break;
|
|
16650
|
-
await modelClass.projectFormat2Records(
|
|
16651
|
-
|
|
16652
|
-
|
|
16653
|
-
);
|
|
16654
|
-
after = String(page[page.length - 1].id);
|
|
18400
|
+
await modelClass.projectFormat2Records(docId, page);
|
|
18401
|
+
rows += page.length;
|
|
18402
|
+
after = page[page.length - 1];
|
|
16655
18403
|
if (page.length < QUERY_HYDRATION_PAGE_ROWS) break;
|
|
16656
18404
|
}
|
|
16657
18405
|
await store.markQueryProjectionCurrent(modelName);
|
|
18406
|
+
Logger.info(
|
|
18407
|
+
`[format2] ${docId}: rebuilt '${modelName}' query tables from ${rows} rows in ${Date.now() - started} ms (${forced ? scope.reason ?? "replaced by a snapshot load" : "no projection mark"})`
|
|
18408
|
+
);
|
|
16658
18409
|
}
|
|
16659
18410
|
}
|
|
16660
18411
|
/**
|
|
16661
|
-
*
|
|
16662
|
-
*
|
|
16663
|
-
*
|
|
16664
|
-
*
|
|
16665
|
-
*
|
|
16666
|
-
* A failure here cannot be swallowed. The mark this model carries says its
|
|
16667
|
-
* query tables hold the merged view, and every later session reads that mark
|
|
16668
|
-
* and SKIPS the rebuild — so a projection that failed would otherwise leave
|
|
16669
|
-
* `query()` answering from a row `find()` disagrees with, for the life of
|
|
16670
|
-
* the database. Dropping the mark costs one rebuild at the next bind, which
|
|
16671
|
-
* is the repair.
|
|
18412
|
+
* A fold batch landed records of `modelName` — rows AND query rows, since
|
|
18413
|
+
* the projection rides in the batch's own command (#3782). The listeners
|
|
18414
|
+
* hear once per model per batch: the batch reports its records in one
|
|
18415
|
+
* synchronous pass, and the notification is taken a microtask later.
|
|
16672
18416
|
*/
|
|
16673
|
-
|
|
16674
|
-
|
|
16675
|
-
if (!
|
|
16676
|
-
|
|
16677
|
-
|
|
16678
|
-
|
|
16679
|
-
|
|
16680
|
-
|
|
16681
|
-
|
|
16682
|
-
|
|
16683
|
-
|
|
16684
|
-
|
|
16685
|
-
}
|
|
16686
|
-
|
|
16687
|
-
|
|
16688
|
-
|
|
16689
|
-
|
|
16690
|
-
|
|
16691
|
-
|
|
18417
|
+
noteFolded(docId, modelName) {
|
|
18418
|
+
let models = this.foldedModels.get(docId);
|
|
18419
|
+
if (!models) {
|
|
18420
|
+
models = /* @__PURE__ */ new Set();
|
|
18421
|
+
this.foldedModels.set(docId, models);
|
|
18422
|
+
queueMicrotask(() => {
|
|
18423
|
+
const touched = this.foldedModels.get(docId);
|
|
18424
|
+
this.foldedModels.delete(docId);
|
|
18425
|
+
for (const model of touched ?? []) {
|
|
18426
|
+
const modelClass = this.modelRegistry.getModelClass(model);
|
|
18427
|
+
modelClass?.notifyFormat2Folded();
|
|
18428
|
+
}
|
|
18429
|
+
});
|
|
18430
|
+
}
|
|
18431
|
+
models.add(modelName);
|
|
18432
|
+
}
|
|
18433
|
+
/**
|
|
18434
|
+
* Which query tables a fold of `docId` projects into (#3782): one spec per
|
|
18435
|
+
* model with an active class, held on this device, while this binding's
|
|
18436
|
+
* facade is the one registered for the document. A released facade projects
|
|
18437
|
+
* nothing, as a projection enqueued against an unregistered document never
|
|
18438
|
+
* did.
|
|
18439
|
+
*/
|
|
18440
|
+
projectionSpecs(docId, store) {
|
|
18441
|
+
return (models) => {
|
|
18442
|
+
if (this.ormContext.format2Documents.get(docId) !== store) return [];
|
|
18443
|
+
const specs = [];
|
|
18444
|
+
for (const model of models) {
|
|
18445
|
+
if (!store.isHydrated(model)) continue;
|
|
18446
|
+
const spec = this.modelRegistry.getModelClass(model)?.format2ProjectionSpec(docId);
|
|
18447
|
+
if (spec) specs.push(spec);
|
|
18448
|
+
}
|
|
18449
|
+
return specs;
|
|
18450
|
+
};
|
|
16692
18451
|
}
|
|
16693
18452
|
/**
|
|
16694
18453
|
* What this device can hold a large document in — the PLATFORM's quota and
|
|
@@ -16730,6 +18489,7 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
16730
18489
|
}
|
|
16731
18490
|
const host = await this.createHost(docId);
|
|
16732
18491
|
const store = await Format2RecordStoreAsync.open(host, docId);
|
|
18492
|
+
store.setProjectionProvider(this.projectionSpecs(docId, store));
|
|
16733
18493
|
this.opened.set(docId, store);
|
|
16734
18494
|
this.ormContext.format2Documents.set(docId, store);
|
|
16735
18495
|
return store;
|
|
@@ -16751,17 +18511,12 @@ var Format2DocumentBinding = class _Format2DocumentBinding {
|
|
|
16751
18511
|
`[DocumentManager] ${docId} is a large document on a store that does not survive this process (${this.dbEngine.constructor.name}): its records and its unacknowledged writes are lost on restart, and a base snapshot cannot be loaded into it.`
|
|
16752
18512
|
);
|
|
16753
18513
|
}
|
|
16754
|
-
|
|
18514
|
+
return new InProcessFormat2StoreHost(
|
|
16755
18515
|
this.dbEngine.createFormat2Sql(),
|
|
16756
18516
|
docId,
|
|
16757
18517
|
void 0,
|
|
16758
18518
|
this.ormContext.format2ClientId
|
|
16759
18519
|
);
|
|
16760
|
-
host.onWipe(async (models) => {
|
|
16761
|
-
await this.wipeQueryTables(docId, models);
|
|
16762
|
-
await host.dispatch({ kind: "projectionMark", op: "forget", docId });
|
|
16763
|
-
});
|
|
16764
|
-
return host;
|
|
16765
18520
|
}
|
|
16766
18521
|
};
|
|
16767
18522
|
function withinBound3(promise, boundMs) {
|
|
@@ -17241,14 +18996,10 @@ var DocumentManagerImpl = class {
|
|
|
17241
18996
|
}
|
|
17242
18997
|
Logger.info(`[DocumentManager] Disconnecting document ${docId}`);
|
|
17243
18998
|
const large = this.ormContext.getFormat2StoreAsync(docId) !== null;
|
|
17244
|
-
const wipeModels = large ? Array.from(this.modelRegistry.getActiveModels().keys()) : void 0;
|
|
17245
18999
|
await this.modelRegistry.removeDocumentData(docId, this.dbEngine, {
|
|
17246
19000
|
skipQueryTableWipe: large
|
|
17247
19001
|
});
|
|
17248
|
-
await this.format2.disconnect(
|
|
17249
|
-
docId,
|
|
17250
|
-
wipeModels ? { wipeIfLastModels: wipeModels } : {}
|
|
17251
|
-
);
|
|
19002
|
+
await this.format2.disconnect(docId);
|
|
17252
19003
|
this.connectedDocuments.delete(docId);
|
|
17253
19004
|
for (const docIds of this.runtimeInitialized.values()) {
|
|
17254
19005
|
docIds.delete(docId);
|
|
@@ -17745,7 +19496,7 @@ function attachAndRegisterModel(modelClass, schema) {
|
|
|
17745
19496
|
}
|
|
17746
19497
|
|
|
17747
19498
|
// src/utils/yDocSchema.ts
|
|
17748
|
-
var
|
|
19499
|
+
var Y10 = __toESM(require("yjs"), 1);
|
|
17749
19500
|
init_yjsValues();
|
|
17750
19501
|
function discoverSchema(yDoc) {
|
|
17751
19502
|
const models = {};
|
|
@@ -17780,7 +19531,7 @@ function discoverModelNames(yDoc) {
|
|
|
17780
19531
|
function materializeMap(yDoc, key) {
|
|
17781
19532
|
try {
|
|
17782
19533
|
const map = yDoc.getMap(key);
|
|
17783
|
-
return map instanceof
|
|
19534
|
+
return map instanceof Y10.Map ? map : null;
|
|
17784
19535
|
} catch {
|
|
17785
19536
|
return null;
|
|
17786
19537
|
}
|
|
@@ -17790,11 +19541,11 @@ function readModelMeta(metaMap) {
|
|
|
17790
19541
|
let constraints;
|
|
17791
19542
|
let relationships;
|
|
17792
19543
|
for (const [key, value] of metaMap.entries()) {
|
|
17793
|
-
if (key === "_constraints" && value instanceof
|
|
19544
|
+
if (key === "_constraints" && value instanceof Y10.Map) {
|
|
17794
19545
|
constraints = readConstraints(value);
|
|
17795
|
-
} else if (key === "_relationships" && value instanceof
|
|
19546
|
+
} else if (key === "_relationships" && value instanceof Y10.Map) {
|
|
17796
19547
|
relationships = readRelationships(value);
|
|
17797
|
-
} else if (value instanceof
|
|
19548
|
+
} else if (value instanceof Y10.Map) {
|
|
17798
19549
|
fields[key] = readFieldMeta(value);
|
|
17799
19550
|
}
|
|
17800
19551
|
}
|
|
@@ -17829,7 +19580,7 @@ function inferModelFromData(dataMap) {
|
|
|
17829
19580
|
const fields = {};
|
|
17830
19581
|
let sampled = 0;
|
|
17831
19582
|
for (const [_recordId, recordValue] of dataMap.entries()) {
|
|
17832
|
-
if (!(recordValue instanceof
|
|
19583
|
+
if (!(recordValue instanceof Y10.Map)) continue;
|
|
17833
19584
|
if (++sampled > 5) break;
|
|
17834
19585
|
for (const [fieldName, value] of recordValue.entries()) {
|
|
17835
19586
|
if (fieldName.startsWith("_")) continue;
|
|
@@ -17857,7 +19608,7 @@ function inferTypeFromValue(value) {
|
|
|
17857
19608
|
function readConstraints(constraintsMap) {
|
|
17858
19609
|
const out = {};
|
|
17859
19610
|
for (const [name, value] of constraintsMap.entries()) {
|
|
17860
|
-
if (!(value instanceof
|
|
19611
|
+
if (!(value instanceof Y10.Map)) continue;
|
|
17861
19612
|
let fields = [];
|
|
17862
19613
|
const rawFields = value.get("fields");
|
|
17863
19614
|
if (typeof rawFields === "string") {
|
|
@@ -17876,7 +19627,7 @@ function readConstraints(constraintsMap) {
|
|
|
17876
19627
|
function readRelationships(relsMap) {
|
|
17877
19628
|
const out = {};
|
|
17878
19629
|
for (const [name, value] of relsMap.entries()) {
|
|
17879
|
-
if (!(value instanceof
|
|
19630
|
+
if (!(value instanceof Y10.Map)) continue;
|
|
17880
19631
|
const rel = {};
|
|
17881
19632
|
for (const [k, v] of value.entries()) {
|
|
17882
19633
|
rel[k] = v;
|
|
@@ -18344,7 +20095,7 @@ function suppressSupersededKeys(carried, later) {
|
|
|
18344
20095
|
}
|
|
18345
20096
|
|
|
18346
20097
|
// src/models/format2EpochRotation.ts
|
|
18347
|
-
var
|
|
20098
|
+
var Y11 = __toESM(require("yjs"), 1);
|
|
18348
20099
|
init_largeDocuments();
|
|
18349
20100
|
async function followEpoch(host, nextEpoch, options = {}) {
|
|
18350
20101
|
const sealed = host.overlayDoc();
|
|
@@ -18385,7 +20136,7 @@ async function followEpoch(host, nextEpoch, options = {}) {
|
|
|
18385
20136
|
suppressDeletes: owed.suppressDeletes
|
|
18386
20137
|
});
|
|
18387
20138
|
}
|
|
18388
|
-
const fresh = new
|
|
20139
|
+
const fresh = new Y11.Doc();
|
|
18389
20140
|
fresh.documentId = sealed.documentId;
|
|
18390
20141
|
if (carried.length > 0) {
|
|
18391
20142
|
fresh.transact(() => {
|
|
@@ -18449,7 +20200,7 @@ async function writeDeferredReplay(host, ops, owedOverlay, resolveReplay) {
|
|
|
18449
20200
|
}
|
|
18450
20201
|
|
|
18451
20202
|
// src/models/format2FastForward.ts
|
|
18452
|
-
var
|
|
20203
|
+
var Y12 = __toESM(require("yjs"), 1);
|
|
18453
20204
|
function planFastForward(options) {
|
|
18454
20205
|
const { current, target } = options;
|
|
18455
20206
|
if (!Number.isInteger(target) || target <= 0) {
|
|
@@ -18513,8 +20264,8 @@ async function applyFastForward(plan, host) {
|
|
|
18513
20264
|
for (const [index, step] of plan.steps.entries()) {
|
|
18514
20265
|
try {
|
|
18515
20266
|
const update = await host.fetchArchive(step);
|
|
18516
|
-
const doc = new
|
|
18517
|
-
|
|
20267
|
+
const doc = new Y12.Doc();
|
|
20268
|
+
Y12.applyUpdate(doc, update);
|
|
18518
20269
|
await host.applyOverlay(doc, step.epoch);
|
|
18519
20270
|
doc.destroy();
|
|
18520
20271
|
} catch (error) {
|
|
@@ -18658,8 +20409,12 @@ function meets(coverage, range) {
|
|
|
18658
20409
|
return true;
|
|
18659
20410
|
}
|
|
18660
20411
|
|
|
20412
|
+
// src/browser.ts
|
|
20413
|
+
init_format2FoldedState();
|
|
20414
|
+
init_format2OverlayKeysSince();
|
|
20415
|
+
|
|
18661
20416
|
// src/models/format2OfflineReplay.ts
|
|
18662
|
-
var
|
|
20417
|
+
var Y13 = __toESM(require("yjs"), 1);
|
|
18663
20418
|
init_largeDocuments();
|
|
18664
20419
|
init_format2Observer();
|
|
18665
20420
|
var recordKey = (model, recordId) => `${model}\0${recordId}`;
|
|
@@ -18701,7 +20456,7 @@ var OfflineConflictLedger = class {
|
|
|
18701
20456
|
for (const modelName of Array.from(doc.share.keys())) {
|
|
18702
20457
|
if (!isFormat2ModelMap(modelName)) continue;
|
|
18703
20458
|
const modelMap = doc.getMap(modelName);
|
|
18704
|
-
if (!(modelMap instanceof
|
|
20459
|
+
if (!(modelMap instanceof Y13.Map) || modelMap.size === 0) continue;
|
|
18705
20460
|
const entries = [];
|
|
18706
20461
|
for (const [key, value] of modelMap.entries()) {
|
|
18707
20462
|
if (!parseOverlayKey(key)) continue;
|