@cleocode/core 2026.10.7 → 2026.10.8
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/build-provenance.json +2 -2
- package/dist/cloud/nexus-link.d.ts +15 -0
- package/dist/cloud/nexus-link.d.ts.map +1 -1
- package/dist/cloud/nexus-link.js +25 -0
- package/dist/cloud/nexus-link.js.map +1 -1
- package/dist/cloud/nexus-vault.d.ts +14 -1
- package/dist/cloud/nexus-vault.d.ts.map +1 -1
- package/dist/cloud/nexus-vault.js +72 -5
- package/dist/cloud/nexus-vault.js.map +1 -1
- package/dist/cloud/replica-rebind.d.ts +51 -0
- package/dist/cloud/replica-rebind.d.ts.map +1 -0
- package/dist/cloud/replica-rebind.js +101 -0
- package/dist/cloud/replica-rebind.js.map +1 -0
- package/dist/doctor/container-reaper.d.ts +107 -0
- package/dist/doctor/container-reaper.d.ts.map +1 -0
- package/dist/doctor/container-reaper.js +194 -0
- package/dist/doctor/container-reaper.js.map +1 -0
- package/dist/store/sync/apply/applier.d.ts +3 -0
- package/dist/store/sync/apply/applier.d.ts.map +1 -1
- package/dist/store/sync/apply/applier.js +208 -85
- package/dist/store/sync/apply/applier.js.map +1 -1
- package/dist/store/sync/capture.d.ts +23 -3
- package/dist/store/sync/capture.d.ts.map +1 -1
- package/dist/store/sync/capture.js +31 -3
- package/dist/store/sync/capture.js.map +1 -1
- package/dist/store/sync/collision-settle.d.ts +62 -0
- package/dist/store/sync/collision-settle.d.ts.map +1 -0
- package/dist/store/sync/collision-settle.js +156 -0
- package/dist/store/sync/collision-settle.js.map +1 -0
- package/dist/store/sync/conflicts.d.ts +11 -0
- package/dist/store/sync/conflicts.d.ts.map +1 -1
- package/dist/store/sync/conflicts.js +18 -0
- package/dist/store/sync/conflicts.js.map +1 -1
- package/dist/store/sync/inherit.d.ts +15 -8
- package/dist/store/sync/inherit.d.ts.map +1 -1
- package/dist/store/sync/inherit.js +28 -11
- package/dist/store/sync/inherit.js.map +1 -1
- package/dist/store/sync/merge/types.d.ts +6 -0
- package/dist/store/sync/merge/types.d.ts.map +1 -1
- package/dist/store/sync/pull.d.ts +23 -2
- package/dist/store/sync/pull.d.ts.map +1 -1
- package/dist/store/sync/pull.js +27 -2
- package/dist/store/sync/pull.js.map +1 -1
- package/dist/store/sync/rebind.d.ts +112 -0
- package/dist/store/sync/rebind.d.ts.map +1 -0
- package/dist/store/sync/rebind.js +187 -0
- package/dist/store/sync/rebind.js.map +1 -0
- package/dist/store/sync/reconcile.d.ts +93 -0
- package/dist/store/sync/reconcile.d.ts.map +1 -0
- package/dist/store/sync/reconcile.js +330 -0
- package/dist/store/sync/reconcile.js.map +1 -0
- package/dist/store/sync/repair.d.ts +5 -0
- package/dist/store/sync/repair.d.ts.map +1 -1
- package/dist/store/sync/repair.js +2 -2
- package/dist/store/sync/repair.js.map +1 -1
- package/dist/store/sync/replica.d.ts +28 -1
- package/dist/store/sync/replica.d.ts.map +1 -1
- package/dist/store/sync/replica.js +17 -3
- package/dist/store/sync/replica.js.map +1 -1
- package/dist/store/sync/retire.d.ts +161 -0
- package/dist/store/sync/retire.d.ts.map +1 -0
- package/dist/store/sync/retire.js +193 -0
- package/dist/store/sync/retire.js.map +1 -0
- package/dist/store/sync/schema.d.ts +27 -0
- package/dist/store/sync/schema.d.ts.map +1 -1
- package/dist/store/sync/schema.js +43 -0
- package/dist/store/sync/schema.js.map +1 -1
- package/dist/store/sync/sealer.d.ts +10 -0
- package/dist/store/sync/sealer.d.ts.map +1 -1
- package/dist/store/sync/sealer.js +51 -5
- package/dist/store/sync/sealer.js.map +1 -1
- package/dist/store/sync/segments.d.ts.map +1 -1
- package/dist/store/sync/segments.js +6 -2
- package/dist/store/sync/segments.js.map +1 -1
- package/dist/store/sync/uid-alias.d.ts +100 -0
- package/dist/store/sync/uid-alias.d.ts.map +1 -0
- package/dist/store/sync/uid-alias.js +169 -0
- package/dist/store/sync/uid-alias.js.map +1 -0
- package/dist/store/table-classification.d.ts.map +1 -1
- package/dist/store/table-classification.js +48 -0
- package/dist/store/table-classification.js.map +1 -1
- package/dist/tasks/change-set.d.ts.map +1 -1
- package/dist/tasks/change-set.js +16 -5
- package/dist/tasks/change-set.js.map +1 -1
- package/dist/tasks/complete-ci.d.ts.map +1 -1
- package/dist/tasks/complete-ci.js +7 -1
- package/dist/tasks/complete-ci.js.map +1 -1
- package/dist/tasks/done-plan.d.ts.map +1 -1
- package/dist/tasks/done-plan.js +24 -17
- package/dist/tasks/done-plan.js.map +1 -1
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +3 -2
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/tasks/gate-runner.d.ts +20 -0
- package/dist/tasks/gate-runner.d.ts.map +1 -1
- package/dist/tasks/gate-runner.js +141 -19
- package/dist/tasks/gate-runner.js.map +1 -1
- package/dist/validation/engine-ops.d.ts.map +1 -1
- package/dist/validation/engine-ops.js +2 -0
- package/dist/validation/engine-ops.js.map +1 -1
- package/migrations/sync-journal/20261009130000_t13278-replica-retire/migration.sql +29 -0
- package/migrations/sync-journal/20261009140000_t13366-retire-confirm/migration.sql +18 -0
- package/migrations/sync-journal/20261010150000_t13397-uid-alias/migration.sql +32 -0
- package/migrations/sync-journal/20261010160000_t13399-rekey-follow/migration.sql +57 -0
- package/package.json +13 -13
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
* so the sealer echoes nothing; row meta is set to the engine's state,
|
|
40
40
|
* conflicts are recorded, and the transaction is marked `applied`,
|
|
41
41
|
* `conflict` (something recorded) or `void` (every op refused).
|
|
42
|
+
* A late transaction of a replica retired earlier in the stream is
|
|
43
|
+
* inherited history (§3.5 D5, T13278): applied the same way, but its merge
|
|
44
|
+
* conflicts are not recorded against the successor's reconcile.
|
|
42
45
|
*
|
|
43
46
|
* Passes repeat while a pass applies something, so a transaction pending on
|
|
44
47
|
* a row a later transaction inserts applies in the same call.
|
|
@@ -58,7 +61,8 @@
|
|
|
58
61
|
*/
|
|
59
62
|
import { BIRTH_FP_COLUMN, UID_COLUMN } from '../../row-identity-registry.js';
|
|
60
63
|
import { captureTableDef } from '../capture.js';
|
|
61
|
-
import {
|
|
64
|
+
import { announcePlacedRekeys, settleLostUidCollisions } from '../collision-settle.js';
|
|
65
|
+
import { recordConflictOnce, recordConflicts, resolveHeldConflicts } from '../conflicts.js';
|
|
62
66
|
import { clearFieldLeaves, readFieldFrontiers, readFieldLeaves, recordFieldLeaves, restoreFieldState, setFieldFrontiers, } from '../field-leave.js';
|
|
63
67
|
import { accountRebaseRows, heldOp, holdOp, txnHasHold, unholdOp } from '../held.js';
|
|
64
68
|
import { encodeHlc, genesisHlc, isWithinSkew, parseHlc } from '../hlc.js';
|
|
@@ -67,16 +71,19 @@ import { applyOp, checkSchemaVersion } from '../merge/engine.js';
|
|
|
67
71
|
import { mergeSpecFor } from '../merge/rules.js';
|
|
68
72
|
import { UNSEEN_ROW, } from '../merge/types.js';
|
|
69
73
|
import { remapPending } from '../remap.js';
|
|
74
|
+
import { isInheritedHistory } from '../retire.js';
|
|
70
75
|
import { fieldHlcsOf, readRowMetaFull, restoreRowMeta } from '../row-meta.js';
|
|
71
76
|
import { hasTable } from '../schema.js';
|
|
72
77
|
import { canonicalJson } from '../sealer-values.js';
|
|
73
78
|
import { capturePosition, markSequenced, ownEchoFastPath, readRowUndo, recordForeignTouches, recordUndoBudget, resnapshotRowUndo, unsequencedLocalTxn, unsequencedLocalTxns, } from '../sequencing.js';
|
|
74
79
|
import { withTriggersSuspended } from '../trigger-classes.js';
|
|
80
|
+
import { followUidAliases, markAliasPlaced, recordUidAlias } from '../uid-alias.js';
|
|
75
81
|
import { widenFootprint } from './footprints.js';
|
|
76
82
|
import { withApplyFrame } from './frame.js';
|
|
77
83
|
import { parentDeletePolicy } from './parent-delete.js';
|
|
78
84
|
import { checkApplyPreconditions, checkTaskTreeShape } from './post-apply.js';
|
|
79
85
|
import { resolveRef, uidOfKey } from './refs.js';
|
|
86
|
+
import { wireToSql } from './write-api.js';
|
|
80
87
|
const rowKey = (t, u) => `${t}\u0000${u}`;
|
|
81
88
|
const keyText = (k) => `${k.stream}:${k.seq}:${k.txnIdx}`;
|
|
82
89
|
/**
|
|
@@ -161,6 +168,63 @@ function uidCollision(db, def, op, resolution) {
|
|
|
161
168
|
opHlc: op.h,
|
|
162
169
|
};
|
|
163
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* The local-key collision an insert would hit, or null (T12341 §6.4 step 7,
|
|
173
|
+
* T13397): a minted row whose local key (a display id, never the uid or a
|
|
174
|
+
* device-local rowid) a live row of another uid holds here. The insert is
|
|
175
|
+
* held, never voided by the UNIQUE constraint: a re-mint of the key settles
|
|
176
|
+
* it (§9.2).
|
|
177
|
+
*/
|
|
178
|
+
function keyCollision(db, def, op, goneInTxn) {
|
|
179
|
+
if (op.o !== 'I' || !op.a || !def.identity.includes(BIRTH_FP_COLUMN))
|
|
180
|
+
return null;
|
|
181
|
+
const cols = def.key.filter((k) => k !== UID_COLUMN && k !== def.localRowid);
|
|
182
|
+
if (cols.length === 0 || cols.some((k) => op.a?.[k] === undefined || op.a?.[k] === null)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const row = db
|
|
186
|
+
.prepare(`SELECT "${UID_COLUMN}" AS uid FROM main."${def.table.replaceAll('"', '""')}" WHERE ${cols
|
|
187
|
+
.map((k) => `"${k.replaceAll('"', '""')}" = ?`)
|
|
188
|
+
.join(' AND ')}`)
|
|
189
|
+
.get(...cols.map((k) => wireToSql(op.a?.[k])));
|
|
190
|
+
// The holder is this row, or a row the transaction deletes first (a key re-used).
|
|
191
|
+
if (!row || row.uid === op.u || (row.uid !== null && goneInTxn(row.uid)))
|
|
192
|
+
return null;
|
|
193
|
+
return {
|
|
194
|
+
kind: 'key-collision',
|
|
195
|
+
table: op.t,
|
|
196
|
+
uid: op.u,
|
|
197
|
+
columns: cols,
|
|
198
|
+
resolution: 'op-held',
|
|
199
|
+
opHlc: op.h,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* A reference of `op` to a uid in an open uid collision here, or null
|
|
204
|
+
* (T13397). Until the collision settles the uid names two rows, so the
|
|
205
|
+
* reference waits rather than bind to the wrong one; after the loser's K its
|
|
206
|
+
* origin's earlier references are read through the alias.
|
|
207
|
+
*/
|
|
208
|
+
function collidingRef(db, op, def, inTxn) {
|
|
209
|
+
if (!op.a || !hasTable(db, '_sync_conflict'))
|
|
210
|
+
return null;
|
|
211
|
+
const open = db.prepare(`SELECT 1 FROM _sync_conflict WHERE kind = 'uid-collision' AND tbl = ? AND uid = ? AND resolved_at IS NULL LIMIT 1`);
|
|
212
|
+
for (const [col, v] of Object.entries(op.a)) {
|
|
213
|
+
const target = def.refs.get(col);
|
|
214
|
+
if (!target || typeof v !== 'string' || inTxn.has(rowKey(target.table, v)))
|
|
215
|
+
continue;
|
|
216
|
+
if (open.get(target.table, v) !== undefined)
|
|
217
|
+
return { col, uid: v };
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
/** Whether a uid collision on (table, uid) was ever recorded here (T13399). */
|
|
222
|
+
function hadUidCollision(db, table, uid) {
|
|
223
|
+
return (hasTable(db, '_sync_conflict') &&
|
|
224
|
+
db
|
|
225
|
+
.prepare(`SELECT 1 FROM _sync_conflict WHERE kind = 'uid-collision' AND tbl = ? AND uid = ? LIMIT 1`)
|
|
226
|
+
.get(table, uid) !== undefined);
|
|
227
|
+
}
|
|
164
228
|
/**
|
|
165
229
|
* A missing reference of `op` (a target never seen), or null. A target the
|
|
166
230
|
* transaction itself inserts or re-keys to counts as present wherever its op
|
|
@@ -236,6 +300,25 @@ function planTxn(db, api, st, defs, localReplica) {
|
|
|
236
300
|
holds: holds(),
|
|
237
301
|
};
|
|
238
302
|
}
|
|
303
|
+
const ambiguous = collidingRef(db, op, def, txnRows);
|
|
304
|
+
if (ambiguous !== null) {
|
|
305
|
+
return {
|
|
306
|
+
kind: 'pending',
|
|
307
|
+
reason: `${op.t}/${op.u}: reference ${ambiguous.col} to ${ambiguous.uid}, a uid in an open collision`,
|
|
308
|
+
holds: holds(),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const keyClash = states.has(k)
|
|
312
|
+
? null
|
|
313
|
+
: keyCollision(db, def, op, (uid) => states.get(rowKey(op.t, uid))?.live === false);
|
|
314
|
+
if (keyClash !== null) {
|
|
315
|
+
return {
|
|
316
|
+
kind: 'pending',
|
|
317
|
+
reason: `${op.t}/${op.u}: local key ${keyClash.columns.join(', ')} is held by another row`,
|
|
318
|
+
holds: holds(),
|
|
319
|
+
collision: { opIdx, conflict: keyClash },
|
|
320
|
+
};
|
|
321
|
+
}
|
|
239
322
|
const before = states.get(k) ?? loadRowState(db, api, def, op.u, localReplica);
|
|
240
323
|
let spec = specs.get(op.t);
|
|
241
324
|
if (!spec) {
|
|
@@ -460,8 +543,15 @@ function applyRekey(c, opIdx, op) {
|
|
|
460
543
|
const nu = op.nu ?? op.u;
|
|
461
544
|
const oldLive = rowExists(c.api, op.t, op.u);
|
|
462
545
|
const newLive = nu !== op.u && rowExists(c.api, op.t, nu);
|
|
546
|
+
// The re-keyed row's later ops, and its origin's earlier references, follow
|
|
547
|
+
// it (T12341 §6.4 step 1, T13397).
|
|
548
|
+
const oldFp = op.obfp ?? op.bfp;
|
|
549
|
+
if (oldFp && nu !== op.u) {
|
|
550
|
+
recordUidAlias(c.db, { table: op.t, oldUid: op.u, oldBfp: oldFp, newUid: nu, origin: c.st.replicaId, hlc: op.h }, c.nowIso);
|
|
551
|
+
}
|
|
552
|
+
// Already re-keyed, deleted, or never placed here (a held loser): the alias is all.
|
|
463
553
|
if (!oldLive)
|
|
464
|
-
return { result: 'skipped', conflicts: 0 };
|
|
554
|
+
return { result: 'skipped', conflicts: 0 };
|
|
465
555
|
// A K names the row it moves by (uid, old fingerprint): a live row holding
|
|
466
556
|
// that uid with another fingerprint is the collision's winner, and a re-key
|
|
467
557
|
// of the other row never touches it (T12341 §6.4, T13394).
|
|
@@ -484,6 +574,12 @@ function applyRekey(c, opIdx, op) {
|
|
|
484
574
|
// and it would no longer match its `*K` intent.
|
|
485
575
|
remapPending(c.db, { table: op.t, oldUid: op.u, newUid: nu, newBfp: op.bfp ?? null });
|
|
486
576
|
c.api.rekeyRow(op.t, op.u, nu, op.bfp ?? null);
|
|
577
|
+
// Another replica settled a uid collision by re-keying a loser this replica
|
|
578
|
+
// had placed: its own earlier references to the old uid meant this row, so
|
|
579
|
+
// it owes the re-key's announcement (T13399).
|
|
580
|
+
if (op.obfp && nu !== op.u && c.st.replicaId !== c.replica && hadUidCollision(c.db, op.t, op.u)) {
|
|
581
|
+
markAliasPlaced(c.db, op.t, op.u, op.obfp);
|
|
582
|
+
}
|
|
487
583
|
return { result: 'applied', conflicts: 0 };
|
|
488
584
|
}
|
|
489
585
|
/**
|
|
@@ -599,7 +695,9 @@ function applyOne(c, opIdx, op) {
|
|
|
599
695
|
const st = c.replay
|
|
600
696
|
? { ...c.st, replicaId: c.replica, txn: { ...c.st.txn, actor: c.replay.actor } }
|
|
601
697
|
: c.st;
|
|
602
|
-
const
|
|
698
|
+
const history = c.history === true && out.status !== 'void';
|
|
699
|
+
const recorded = effect(c.db, c.api, st, opIdx, { op, def, before, out, refKeys }, c.nowIso, c.replay !== undefined || history);
|
|
700
|
+
const n = history ? 0 : recorded;
|
|
603
701
|
c.db.exec(`RELEASE ${sp}`);
|
|
604
702
|
const result = out.status === 'applied' || out.status === 'partial'
|
|
605
703
|
? 'applied'
|
|
@@ -1085,7 +1183,15 @@ function applyInPage(x, st) {
|
|
|
1085
1183
|
const local = x.sequencingOn && st.replicaId === opts.replica ? unsequencedLocalTxn(db, st.txn.txn) : null;
|
|
1086
1184
|
// An own echo whose transaction the page rewound applies it at its stream position.
|
|
1087
1185
|
const rewoundEcho = local !== null && x.rewound.has(local.txn);
|
|
1088
|
-
const c = {
|
|
1186
|
+
const c = {
|
|
1187
|
+
db,
|
|
1188
|
+
api,
|
|
1189
|
+
st,
|
|
1190
|
+
defs,
|
|
1191
|
+
replica: opts.replica,
|
|
1192
|
+
nowIso,
|
|
1193
|
+
history: isInheritedHistory(db, opts.stream, st.replicaId, st.key.seq),
|
|
1194
|
+
};
|
|
1089
1195
|
let n = 0;
|
|
1090
1196
|
// Gate C (§3.6): the whole transaction is one savepoint, so a broken
|
|
1091
1197
|
// multi-row invariant rolls all of it back (the page's rewind stays).
|
|
@@ -1165,6 +1271,8 @@ function applyInPage(x, st) {
|
|
|
1165
1271
|
reason: n > 0 ? `${n} conflict(s) recorded` : null,
|
|
1166
1272
|
nowIso,
|
|
1167
1273
|
});
|
|
1274
|
+
// A collision it was held on has settled: it went through (T13397).
|
|
1275
|
+
resolveHeldConflicts(db, st.key, nowIso);
|
|
1168
1276
|
return { status, holds: [], n, applied: true, decided: rewoundEcho && local ? local.txn : null };
|
|
1169
1277
|
}
|
|
1170
1278
|
/**
|
|
@@ -1216,94 +1324,109 @@ export function applyStagedTxns(db, opts) {
|
|
|
1216
1324
|
let passes = 0;
|
|
1217
1325
|
let rebased = 0;
|
|
1218
1326
|
const heldTxns = new Set();
|
|
1219
|
-
|
|
1220
|
-
progress =
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
api,
|
|
1241
|
-
st: page[0].st,
|
|
1242
|
-
defs,
|
|
1243
|
-
replica: opts.replica,
|
|
1244
|
-
nowIso: pageIso,
|
|
1245
|
-
};
|
|
1246
|
-
const rw = rebase ? rewindTxns(c0, rebase) : { kept: new Map(), after: new Map() };
|
|
1247
|
-
const rewound = new Set(rebase?.rewind.map((l) => l.txn) ?? []);
|
|
1248
|
-
const decided = new Set(); // rewound locals whose echo this page applied
|
|
1249
|
-
// The time bound covers applying, not the rewind: a large scope must
|
|
1250
|
-
// not shrink every page to one transaction that rewinds it again.
|
|
1251
|
-
const applyStart = now();
|
|
1252
|
-
let taken = 0;
|
|
1253
|
-
for (const { st } of page) {
|
|
1254
|
-
// The time bound cuts a page between transactions (never inside one).
|
|
1255
|
-
if (taken > 0 && now() - applyStart > (opts.pageMs ?? REBASE_PAGE_MS))
|
|
1256
|
-
break;
|
|
1257
|
-
taken += 1;
|
|
1258
|
-
const nowMs = now();
|
|
1259
|
-
const nowIso = new Date(nowMs).toISOString();
|
|
1260
|
-
const id = keyText(st.key);
|
|
1261
|
-
const result = applyInPage({
|
|
1327
|
+
const runPasses = () => {
|
|
1328
|
+
for (let progress = true; progress && passes < maxPasses;) {
|
|
1329
|
+
progress = false;
|
|
1330
|
+
passes += 1;
|
|
1331
|
+
const heldReplicas = new Set();
|
|
1332
|
+
const held = new Set(); // rows written by a pending transaction
|
|
1333
|
+
const staged = stagedTxns(db, opts.stream).map((st) => followUidAliases(db, withoutLocalRowids(st, defs), defs));
|
|
1334
|
+
// Each page is one frame and one scoped rebase (§3.5 Rule 3): rewind the
|
|
1335
|
+
// page's scope once, apply its transactions in stream order, replay once.
|
|
1336
|
+
for (let at = 0; at < staged.length;) {
|
|
1337
|
+
const page = takePage(staged, at, opts.pageOps ?? REBASE_PAGE_OPS);
|
|
1338
|
+
const pageStart = now();
|
|
1339
|
+
const done = withApplyFrame(db, opts.scope, page[0]?.actor ?? null, (api) => {
|
|
1340
|
+
const pageIso = new Date(pageStart).toISOString();
|
|
1341
|
+
for (const p of page)
|
|
1342
|
+
adoptNaturalRows(db, api, defs, p.st.txn.ops);
|
|
1343
|
+
const planned = sequencingOn
|
|
1344
|
+
? planPageRebase(db, page.map((p) => p.st), defs, opts.replica)
|
|
1345
|
+
: { plan: null, inPlace: new Set() };
|
|
1346
|
+
const rebase = planned.plan;
|
|
1347
|
+
const c0 = {
|
|
1262
1348
|
db,
|
|
1263
1349
|
api,
|
|
1350
|
+
st: page[0].st,
|
|
1264
1351
|
defs,
|
|
1265
|
-
opts,
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
for (const
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1352
|
+
replica: opts.replica,
|
|
1353
|
+
nowIso: pageIso,
|
|
1354
|
+
};
|
|
1355
|
+
const rw = rebase
|
|
1356
|
+
? rewindTxns(c0, rebase)
|
|
1357
|
+
: { kept: new Map(), after: new Map() };
|
|
1358
|
+
const rewound = new Set(rebase?.rewind.map((l) => l.txn) ?? []);
|
|
1359
|
+
const decided = new Set(); // rewound locals whose echo this page applied
|
|
1360
|
+
// The time bound covers applying, not the rewind: a large scope must
|
|
1361
|
+
// not shrink every page to one transaction that rewinds it again.
|
|
1362
|
+
const applyStart = now();
|
|
1363
|
+
let taken = 0;
|
|
1364
|
+
for (const { st } of page) {
|
|
1365
|
+
// The time bound cuts a page between transactions (never inside one).
|
|
1366
|
+
if (taken > 0 && now() - applyStart > (opts.pageMs ?? REBASE_PAGE_MS))
|
|
1367
|
+
break;
|
|
1368
|
+
taken += 1;
|
|
1369
|
+
const nowMs = now();
|
|
1370
|
+
const nowIso = new Date(nowMs).toISOString();
|
|
1371
|
+
const id = keyText(st.key);
|
|
1372
|
+
const result = applyInPage({
|
|
1373
|
+
db,
|
|
1374
|
+
api,
|
|
1375
|
+
defs,
|
|
1376
|
+
opts,
|
|
1377
|
+
sequencingOn,
|
|
1378
|
+
rewound,
|
|
1379
|
+
inPlace: planned.inPlace,
|
|
1380
|
+
rw,
|
|
1381
|
+
nowMs,
|
|
1382
|
+
nowIso,
|
|
1383
|
+
held,
|
|
1384
|
+
heldReplicas,
|
|
1385
|
+
}, st);
|
|
1386
|
+
if (result.decided)
|
|
1387
|
+
decided.add(result.decided);
|
|
1388
|
+
for (const t of result.heldTxns ?? [])
|
|
1389
|
+
heldTxns.add(t);
|
|
1390
|
+
if (result.status !== null)
|
|
1391
|
+
last.set(id, result.status);
|
|
1392
|
+
conflicts += result.n;
|
|
1393
|
+
if (rebase && result.applied)
|
|
1394
|
+
rebased += 1;
|
|
1395
|
+
if (result.status === 'held-skew')
|
|
1396
|
+
heldReplicas.add(st.replicaId);
|
|
1397
|
+
if (result.status === 'pending')
|
|
1398
|
+
for (const k of result.holds)
|
|
1399
|
+
held.add(k);
|
|
1400
|
+
if (result.status === 'applied' ||
|
|
1401
|
+
result.status === 'conflict' ||
|
|
1402
|
+
result.status === 'void') {
|
|
1403
|
+
progress = true;
|
|
1404
|
+
}
|
|
1293
1405
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1406
|
+
// Replay what the page rewound and did not decide by its echo.
|
|
1407
|
+
if (rebase) {
|
|
1408
|
+
const replay = rebase.rewind.filter((l) => !decided.has(l.txn));
|
|
1409
|
+
for (const t of replayTxns(c0, { rewind: rebase.rewind, replay, own: null }, rw)) {
|
|
1410
|
+
heldTxns.add(t);
|
|
1411
|
+
}
|
|
1300
1412
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1413
|
+
return taken;
|
|
1414
|
+
});
|
|
1415
|
+
at += done;
|
|
1416
|
+
}
|
|
1305
1417
|
}
|
|
1418
|
+
};
|
|
1419
|
+
runPasses();
|
|
1420
|
+
// The origin of a lost uid collision re-keys its row, then the held winner
|
|
1421
|
+
// places under the freed uid (T12341 §6.4, T13397).
|
|
1422
|
+
if (settleLostUidCollisions(db, opts).length > 0) {
|
|
1423
|
+
opts.seal?.();
|
|
1424
|
+
runPasses();
|
|
1306
1425
|
}
|
|
1426
|
+
// Every other replica that had placed a re-keyed loser announces the
|
|
1427
|
+
// re-key, so its earlier references follow the loser too (T13399).
|
|
1428
|
+
if (announcePlacedRekeys(db, opts, new Date(now()).toISOString()) > 0)
|
|
1429
|
+
opts.seal?.();
|
|
1307
1430
|
const count = (s) => [...last.values()].filter((v) => v === s).length;
|
|
1308
1431
|
return {
|
|
1309
1432
|
applied: count('applied'),
|