rapier-markdown-kit 1.2.7 → 1.2.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/README.md +32 -30
- package/dist/agent/will.mjs +1 -6
- package/dist/images/gif.mjs +0 -3
- package/dist/images/playback.mjs +1 -10
- package/dist/kit/assets.mjs +0 -1
- package/dist/kit/conformance/measurer.mjs +0 -0
- package/dist/kit/conformance/run.mjs +1 -8
- package/dist/kit/export-fonts.mjs +0 -14
- package/dist/kit/index.mjs +1 -1
- package/dist/kit/ledger/authored-origin.mjs +0 -5
- package/dist/kit/ledger/authorship.mjs +0 -2
- package/dist/kit/ledger/carried.d.mts +20 -0
- package/dist/kit/ledger/carried.mjs +4 -7
- package/dist/kit/ledger/data.mjs +0 -8
- package/dist/kit/ledger/format.mjs +0 -25
- package/dist/kit/ledger/hash.mjs +11 -7
- package/dist/kit/ledger/history.mjs +10 -29
- package/dist/kit/ledger/inverse-source.mjs +0 -46
- package/dist/kit/ledger/journal-records.mjs +24 -56
- package/dist/kit/ledger/merge.mjs +0 -27
- package/dist/kit/ledger/text-merge.mjs +0 -20
- package/dist/kit/ledger/transport.mjs +0 -47
- package/dist/kit/ledger.d.mts +88 -0
- package/dist/kit/ledger.mjs +14 -0
- package/dist/kit/markdown-it.d.mts +4 -0
- package/dist/kit/markdown-it.mjs +36 -0
- package/dist/kit/pdf-glyphs.mjs +39 -45
- package/dist/kit/pdf-source.d.mts +5 -0
- package/dist/kit/pdf-source.mjs +125 -0
- package/dist/kit/pdf-vectors.mjs +0 -3
- package/dist/kit/print-capitals.mjs +0 -4
- package/dist/kit/render-entry.mjs +0 -1
- package/dist/kit/render-ink.mjs +0 -5
- package/dist/kit/render-markdown.mjs +15 -119
- package/dist/kit/render-print.mjs +4 -49
- package/dist/kit/render-sanitize.mjs +22 -58
- package/dist/kit/render-styles.mjs +0 -7
- package/dist/kit/render-work.mjs +0 -12
- package/dist/kit/render.mjs +18 -173
- package/dist/kit/will.d.mts +42 -0
- package/dist/layout/hyphenation.mjs +0 -23
- package/dist/layout/line-plan.mjs +0 -20
- package/dist/layout/model.mjs +0 -55
- package/dist/layout/paragraph-fit.mjs +50 -60
- package/dist/layout/preparation.mjs +23 -56
- package/dist/layout/prepared-paragraph.mjs +0 -25
- package/dist/layout/typography.mjs +103 -41
- package/dist/packages/rapier-diff/concerns.mjs +0 -16
- package/dist/packages/rapier-diff/diff.mjs +0 -7
- package/dist/packages/rapier-diff/index.mjs +0 -1
- package/dist/packages/rapier-diff/matching.mjs +0 -19
- package/dist/packages/rapier-diff/merge.mjs +0 -9
- package/dist/packages/rapier-diff/moves.mjs +0 -7
- package/dist/packages/rapier-diff/structure.mjs +0 -2
- package/dist/packages/rapier-diff/tokens.mjs +0 -6
- package/dist/packages/rapier-diff/walk.mjs +0 -12
- package/dist/spec/document-fonts.mjs +0 -9
- package/dist/spec/document-settings.mjs +6 -25
- package/dist/spec/frontmatter.mjs +0 -1
- package/dist/spec/html-blocks.mjs +3 -37
- package/dist/spec/md-assets.mjs +2 -61
- package/dist/spec/md-layout.mjs +0 -34
- package/dist/spec/md-marks.mjs +0 -51
- package/dist/spec/md-source.mjs +0 -16
- package/dist/spec/word-sidecar.mjs +0 -2
- package/package.json +21 -4
- package/style.css +38 -21
package/dist/kit/ledger/hash.mjs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// The editor's path-dependent root, plus synchronous SHA-256 for file readers (including offline wrappers).
|
|
3
2
|
export function textRoot(text) {
|
|
4
3
|
return textRootParts([text]);
|
|
5
4
|
}
|
|
6
|
-
// The same UTF-16 root over shared source pieces, without joining a historical body.
|
|
7
5
|
export function textRootParts(parts) {
|
|
8
6
|
let fnv = 0x811c9dc5, a = 1, b = 0, length = 0;
|
|
9
7
|
for (const text of parts) {
|
|
@@ -30,7 +28,6 @@ const K = new Uint32Array([
|
|
|
30
28
|
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2,
|
|
31
29
|
]);
|
|
32
30
|
const rotate = (x, n) => (x >>> n) | (x << (32 - n));
|
|
33
|
-
// One bounded SHA-256 compression state for file bytes and encoded source text.
|
|
34
31
|
export function sha256State() {
|
|
35
32
|
const h = new Uint32Array([0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]);
|
|
36
33
|
const w = new Uint32Array(64), block = new Uint8Array(64);
|
|
@@ -82,14 +79,11 @@ export function sha256State() {
|
|
|
82
79
|
get bytes() { return bytes; },
|
|
83
80
|
};
|
|
84
81
|
}
|
|
85
|
-
// UTF-16 chunks share that byte state. Retain a trailing high surrogate so a
|
|
86
|
-
// caller can split a character between updates without changing its UTF-8 digest.
|
|
87
82
|
export function createSHA256() {
|
|
88
83
|
const hash = sha256State(), encoded = new Uint8Array(65536), encoder = new TextEncoder();
|
|
89
84
|
let high = '', finished = false;
|
|
90
85
|
const write = text => {
|
|
91
86
|
for (let at = 0; at < text.length;) {
|
|
92
|
-
// Bound both the substring and byte allocation; encodeInto never cuts a scalar.
|
|
93
87
|
let end = Math.min(text.length, at + 32768);
|
|
94
88
|
if (end < text.length && (text.charCodeAt(end - 1) & 0xfc00) === 0xd800 && (text.charCodeAt(end) & 0xfc00) === 0xdc00) end--;
|
|
95
89
|
const part = encoder.encodeInto(text.slice(at, end), encoded);
|
|
@@ -117,4 +111,14 @@ export function sha256Parts(parts) {
|
|
|
117
111
|
for (const part of parts) hash.update(part);
|
|
118
112
|
return hash.digest();
|
|
119
113
|
}
|
|
120
|
-
|
|
114
|
+
const LONG_TEXT = 65536, DIGESTS = new Map();
|
|
115
|
+
export function sha256(text) {
|
|
116
|
+
if (typeof text !== 'string' || text.length < LONG_TEXT) return createSHA256().update(text).digest();
|
|
117
|
+
let digest = DIGESTS.get(text);
|
|
118
|
+
if (digest === undefined) {
|
|
119
|
+
digest = createSHA256().update(text).digest();
|
|
120
|
+
if (DIGESTS.size >= 4) DIGESTS.delete(DIGESTS.keys().next().value);
|
|
121
|
+
} else DIGESTS.delete(text);
|
|
122
|
+
DIGESTS.set(text, digest);
|
|
123
|
+
return digest;
|
|
124
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Transient projections and selective inverses of the existing canonical journal.
|
|
3
2
|
import {_rapierTransformSplices, _rapierValidMetadata, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState, _rapierPrepareHistoryRecords} from './journal-records.mjs';
|
|
4
3
|
import {projectInverseSource, HistorySource} from './inverse-source.mjs';
|
|
5
4
|
import {textRootParts, rootAfter} from './hash.mjs';
|
|
@@ -41,16 +40,12 @@ export function groupHistoryActs(acts) {
|
|
|
41
40
|
return groups;
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
// A picture's codec replacement travels with its original navigation step.
|
|
45
|
-
// Match every complete replacement exactly before moving any source bytes.
|
|
46
43
|
export function pictureHistorySplices(input, target, redo) {
|
|
47
44
|
const {records} = input;
|
|
48
45
|
const id = target?.transaction?.id, at = records.indexOf(target);
|
|
49
46
|
if (!id || at < 0 || target.transaction.operation === 'document.finish-picture' || !Array.isArray(target.splices)) return null;
|
|
50
47
|
const later = records.slice(at + 1).filter(row => row?.transaction?.operation === 'document.finish-picture' && Array.isArray(row.splices) && row.splices.length);
|
|
51
48
|
if (!later.length) return null;
|
|
52
|
-
// A caller may hold a persistent source tree: request its text only when
|
|
53
|
-
// a later picture replacement can actually affect this navigation step.
|
|
54
49
|
const source = input.source;
|
|
55
50
|
const own = new Set(later.filter(row => row.transaction.sourceTransactionId === id));
|
|
56
51
|
const step = redo ? target.splices : target.splices.slice().reverse().map(row => ({pos: row.pos, removed: row.inserted, inserted: row.removed}));
|
|
@@ -78,9 +73,6 @@ export function pictureHistorySplices(input, target, redo) {
|
|
|
78
73
|
try { return plan(true); } catch (_) { return null; }
|
|
79
74
|
}
|
|
80
75
|
|
|
81
|
-
// Keep only the latest source observation. Inactive live documents must not retain
|
|
82
|
-
// every revision's source tree. The weak key also lets the latest input be collected.
|
|
83
|
-
// Mutable caller data is compared/captured before this cache can be consulted.
|
|
84
76
|
let preparedHistories = new WeakMap();
|
|
85
77
|
function prepareHistory(input) {
|
|
86
78
|
try {
|
|
@@ -123,7 +115,6 @@ function prepareHistory(input) {
|
|
|
123
115
|
row.beforeSource = initial; row.beforeMetadata = initialMetadata;
|
|
124
116
|
}
|
|
125
117
|
const earliestRevision = records[0]?.transaction.baseRevision ?? revision;
|
|
126
|
-
// The window's owner may name its root (the source store mints it once per text); otherwise hash the initial text.
|
|
127
118
|
let root = earliestRevision === 0 ? (typeof input.earliestRoot === 'string' && input.earliestRoot || textRootParts(initial.parts())) : null,
|
|
128
119
|
rootRevision = earliestRevision;
|
|
129
120
|
for (const {record, splices} of rows) {
|
|
@@ -131,8 +122,6 @@ function prepareHistory(input) {
|
|
|
131
122
|
if (hashed && (![record.beforeHash, record.afterHash].every(value =>
|
|
132
123
|
typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value)) ||
|
|
133
124
|
(splices.length ? record.beforeHash === record.afterHash : record.beforeHash !== record.afterHash))) return historyFailure('history_invalid');
|
|
134
|
-
// Revision gaps begin a new path root; a continuous revision-zero prefix
|
|
135
|
-
// still binds every untouched source byte through the initial text root.
|
|
136
125
|
if (record.transaction.baseRevision !== rootRevision) root = null;
|
|
137
126
|
if (hashed) {
|
|
138
127
|
if (root !== null && record.beforeHash !== root) return historyFailure('history_invalid');
|
|
@@ -155,12 +144,18 @@ function prepareHistory(input) {
|
|
|
155
144
|
} catch (_) { return historyFailure('history_invalid'); }
|
|
156
145
|
}
|
|
157
146
|
|
|
147
|
+
function settle(result, value) {
|
|
148
|
+
Object.defineProperty(result, 'initialSource', {value, writable: true, enumerable: true, configurable: true});
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
|
|
158
152
|
export function replayHistory(input) {
|
|
159
153
|
const prepared = prepareHistory(input);
|
|
160
154
|
if (!prepared.ok) return prepared;
|
|
161
|
-
const {source,
|
|
155
|
+
const {source, metadata, initialMetadata, revision, earliestRevision, complete, rows, effects} = prepared;
|
|
162
156
|
const acts = rows.filter(({record}) => !effects.pictures.has(record.transaction.id)).map(({record}) => historyAct(record, effects));
|
|
163
|
-
return {ok: true, source, initialSource,
|
|
157
|
+
return {ok: true, source, get initialSource() { return settle(this, prepared.initialSource); }, set initialSource(value) { settle(this, value); },
|
|
158
|
+
metadata: {...metadata}, initialMetadata: {...initialMetadata}, revision, earliestRevision, complete, acts,
|
|
164
159
|
effectiveActIds: effects.effectiveActIds.slice(), groups: groupHistoryActs(acts)};
|
|
165
160
|
}
|
|
166
161
|
|
|
@@ -218,7 +213,6 @@ export function historyProjection(input, {from = 0, to = input?.source?.length,
|
|
|
218
213
|
return projectHistoryRange(prepared, from, to, actId);
|
|
219
214
|
}
|
|
220
215
|
|
|
221
|
-
// Several ranges share one proved source snapshot for this call.
|
|
222
216
|
export function historyProjections(input, ranges) {
|
|
223
217
|
const prepared = prepareHistory(input);
|
|
224
218
|
if (!prepared.ok) return prepared;
|
|
@@ -227,8 +221,6 @@ export function historyProjections(input, ranges) {
|
|
|
227
221
|
projectHistoryRange(prepared, from, to, actId))};
|
|
228
222
|
}
|
|
229
223
|
|
|
230
|
-
// Block intervals stay ordered under reverse splices. A fixed balanced tree can
|
|
231
|
-
// shift an untouched suffix lazily and visit only intervals crossing the edit.
|
|
232
224
|
function historyBlockIndex(projected) {
|
|
233
225
|
const items = projected.map(block => ({block, range: {from: block.from, to: block.to}, shift: 0}))
|
|
234
226
|
.sort((a, b) => a.range.from - b.range.from);
|
|
@@ -278,8 +270,6 @@ export function historyPlaces(input, blocks) {
|
|
|
278
270
|
const {record, splices, beforeSource, afterSource} = prepared.rows[index], touched = new Map();
|
|
279
271
|
for (let at = splices.length - 1; at >= 0; at--) ranges.move(splices[at], node => {
|
|
280
272
|
if (touched.has(node) || prepared.effects.pictures.has(record.transaction.id)) return;
|
|
281
|
-
// Earlier splices in this reverse walk only shifted this interval. Recover
|
|
282
|
-
// its coordinates at the record's after-state before recording the first touch.
|
|
283
273
|
let after = {...node.range};
|
|
284
274
|
for (let prior = at + 1; prior < splices.length; prior++) {
|
|
285
275
|
const row = splices[prior];
|
|
@@ -290,37 +280,31 @@ export function historyPlaces(input, blocks) {
|
|
|
290
280
|
}
|
|
291
281
|
touched.set(node, after);
|
|
292
282
|
});
|
|
293
|
-
// Blocks one splice covers share its range (a whole-document delete maps every block to all it removed): slice each range once.
|
|
294
283
|
const slices = new Map(), cut = (source, side, from, to) => {
|
|
295
284
|
const key = side + from + ':' + to;
|
|
296
285
|
if (!slices.has(key)) slices.set(key, source.slice(from, to));
|
|
297
286
|
return slices.get(key);
|
|
298
287
|
};
|
|
299
|
-
// One record's act is read once; the blocks it touched share its fields (a whole-document delete touches every block).
|
|
300
288
|
let act = null;
|
|
301
289
|
for (const [node, afterRange] of touched) {
|
|
302
290
|
const beforeRange = ranges.range(node), previous = {...beforeRange, source: cut(beforeSource, 'b', beforeRange.from, beforeRange.to)},
|
|
303
291
|
after = {...afterRange, source: cut(afterSource, 'a', afterRange.from, afterRange.to)};
|
|
304
|
-
// A point (where words were deleted) has no words to compare: the act that touched it wrote there.
|
|
305
292
|
if (previous.source !== after.source || afterRange.from === afterRange.to) node.block.acts.push({...(act ||= historyAct(record)), before: previous, after});
|
|
306
293
|
}
|
|
307
294
|
}
|
|
308
295
|
|
|
309
296
|
const acts = prepared.rows.filter(({record}) => !prepared.effects.pictures.has(record.transaction.id)).map(({record}) => historyAct(record));
|
|
310
|
-
// Native Redo consumes its Undo as navigation while keeping that receipt in
|
|
311
|
-
// the reading history. All material withdrawals use the canonical blockers,
|
|
312
|
-
// including independent inverses holding the same original act suppressed.
|
|
313
297
|
const reversed = new Set();
|
|
314
298
|
for (const {record} of prepared.rows) if (prepared.effects.active.has(record.transaction.id) && !record.transaction.reapplies)
|
|
315
299
|
for (const id of prepared.effects.references.get(record.transaction.id) || []) reversed.add(id);
|
|
316
300
|
|
|
317
301
|
for (const block of projected) block.acts.reverse();
|
|
318
|
-
return {ok: true, source: prepared.source, initialSource
|
|
302
|
+
return {ok: true, source: prepared.source, get initialSource() { return settle(this, prepared.initialSource); },
|
|
303
|
+
set initialSource(value) { settle(this, value); }, revision: prepared.revision,
|
|
319
304
|
earliestRevision: prepared.earliestRevision, complete: prepared.complete, acts, blocks: projected,
|
|
320
305
|
reversed: acts.filter(act => reversed.has(act.id)).map(act => act.id)};
|
|
321
306
|
}
|
|
322
307
|
|
|
323
|
-
// Runs retain only exact replacement ancestry. A later replacement owns an ambiguous hunk whole.
|
|
324
308
|
export function selectiveUndo(input, actIds, options = {}) {
|
|
325
309
|
const prepared = prepareHistory(input);
|
|
326
310
|
if (!prepared.ok) return prepared;
|
|
@@ -340,9 +324,6 @@ export function selectiveUndo(input, actIds, options = {}) {
|
|
|
340
324
|
const metadata = _rapierMetadataDelta(prepared.metadata, afterMetadata), effectChanged = selected.size > 0;
|
|
341
325
|
const sourceEffectChanged = records.some(record => record.splices.length && !prepared.effects.references.get(record.transaction.id)?.length &&
|
|
342
326
|
prepared.effects.active.has(record.transaction.id) !== projectedEffects.active.has(record.transaction.id));
|
|
343
|
-
// One shared replacement is visible while any of its authors is active. That
|
|
344
|
-
// includes an author stored with no second physical splice, and an inverse
|
|
345
|
-
// whose only effect is to make such an author active again.
|
|
346
327
|
let coverVisibilityChanged = false;
|
|
347
328
|
if (!sourceEffectChanged && selected.size) {
|
|
348
329
|
let origins = null;
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Exact source projection shared by canonical history and causal transport.
|
|
3
|
-
// Callers admit and order the records first; this projection retains no state.
|
|
4
2
|
import {_rapierTransformSplices, _rapierHistoryEffects} from './journal-records.mjs';
|
|
5
3
|
import {authoredPlacements} from './authored-origin.mjs';
|
|
6
4
|
import {canonicalJSON} from './data.mjs';
|
|
@@ -11,8 +9,6 @@ const historyBoundary = (source, at) => Number.isSafeInteger(at) && at >= 0 && !
|
|
|
11
9
|
!(at > 0 && at < source.length && (source.charCodeAt(at - 1) & 0xFC00) === 0xD800 &&
|
|
12
10
|
(source.charCodeAt(at) & 0xFC00) === 0xDC00);
|
|
13
11
|
|
|
14
|
-
// Persistent AVL pieces retain original strings and coordinates. A revision costs
|
|
15
|
-
// only the edited paths; reading a range visits its leaves, never earlier bodies.
|
|
16
12
|
const sourceLeaf = (text, from = 0, to = text.length) => to > from
|
|
17
13
|
? {text, from, length: to - from, height: 1} : null;
|
|
18
14
|
const sourceHeight = node => node?.height || 0;
|
|
@@ -101,8 +97,6 @@ export class HistorySource {
|
|
|
101
97
|
for (const text of parts) for (let at = 0; at < text.length;) {
|
|
102
98
|
if (!leaf) return false;
|
|
103
99
|
const length = Math.min(leaf.length - offset, text.length - at), from = leaf.from + offset;
|
|
104
|
-
// Untouched runs share their string and coordinates. Only changed fragments
|
|
105
|
-
// require a text comparison; no full-body join is needed to prove the plan.
|
|
106
100
|
if ((leaf.text !== text || from !== at) && leaf.text.slice(from, from + length) !== text.slice(at, at + length)) return false;
|
|
107
101
|
offset += length; at += length;
|
|
108
102
|
if (offset === leaf.length) { leaf = leaves.next().value; offset = 0; }
|
|
@@ -113,8 +107,6 @@ export class HistorySource {
|
|
|
113
107
|
const [left, rest] = sourceSplit(this.#root, pos), [, right] = sourceSplit(rest, length);
|
|
114
108
|
return new HistorySource('', sourceJoin(sourceJoin(left, sourceLeaf(text)), right));
|
|
115
109
|
}
|
|
116
|
-
// Undoing an admitted repair is observation: its after-state can join a scalar
|
|
117
|
-
// across the deleted run. Prove the forward boundaries on the reconstructed view.
|
|
118
110
|
before(splices) {
|
|
119
111
|
let source = this;
|
|
120
112
|
for (let index = splices.length - 1; index >= 0; index--) {
|
|
@@ -151,8 +143,6 @@ function minimalHistorySplice(before, after, pos = 0) {
|
|
|
151
143
|
return {pos: pos + first, removed: before.slice(first, a), inserted: after.slice(first, b)};
|
|
152
144
|
}
|
|
153
145
|
|
|
154
|
-
// Split recorded replacements only at exact unchanged word or line tokens.
|
|
155
|
-
// Shared letters inside changed words do not establish surviving ownership.
|
|
156
146
|
function historyHunks(row) {
|
|
157
147
|
if (row.removed === row.inserted) return [];
|
|
158
148
|
if (!row.removed || !row.inserted) return [{...row}];
|
|
@@ -166,8 +156,6 @@ function historyHunks(row) {
|
|
|
166
156
|
}
|
|
167
157
|
|
|
168
158
|
|
|
169
|
-
// Rows are already proved source placements, optionally carrying sourceIndices from
|
|
170
|
-
// causal projection. Multiple inverse blockers can change history with no source edit.
|
|
171
159
|
export function projectInverseSource(prepared, selected, projectedEffects, options = {}) {
|
|
172
160
|
const records = prepared.records || prepared.rows.map(({record, splices}) => record.splices === splices ? record : {...record, splices});
|
|
173
161
|
if (!records.some(record => !prepared.effects.references.get(record.transaction.id)?.length &&
|
|
@@ -179,8 +167,6 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
|
|
|
179
167
|
const recordsForCover = prepared.records || prepared.rows.map(({record, splices}) => record.splices === splices ? record : {...record, splices});
|
|
180
168
|
const coverGroups = new Map();
|
|
181
169
|
let coverOrigins = null;
|
|
182
|
-
// An empty document is a real causal source. Fresh notes start there, and a
|
|
183
|
-
// shared replacement authored against that source still has to stay covered.
|
|
184
170
|
if (typeof prepared.initialSource === 'string' && recordsForCover.some(record => record.authored?.splices?.some(row => row.removed.length))) {
|
|
185
171
|
try { coverOrigins = authoredPlacements(prepared.initialSource, recordsForCover); } catch { coverOrigins = null; }
|
|
186
172
|
}
|
|
@@ -254,9 +240,6 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
|
|
|
254
240
|
return next && effects.active.has(next.actId) && !prepared.effects.references.get(next.actId)?.length;
|
|
255
241
|
});
|
|
256
242
|
const reactivate = (actIds, effects) => {
|
|
257
|
-
// Any author of one shared removal or replacement restores that one physical
|
|
258
|
-
// hunk, including an author who stored no splice. A later independent act
|
|
259
|
-
// keeps the bytes it wrote until that act itself is withdrawn.
|
|
260
243
|
const wanted = new Set(actIds);
|
|
261
244
|
for (const id of actIds) for (const hunk of all) {
|
|
262
245
|
if (hunk.reversedHunk || hunk.derived || !hunk.coverIds?.includes(id) || wanted.has(hunk.actId)) continue;
|
|
@@ -269,8 +252,6 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
|
|
|
269
252
|
(!hunk.inserted && hunk.active)) continue;
|
|
270
253
|
const marker = pieces.indexOf(hunk.marker);
|
|
271
254
|
if (marker < 0) {
|
|
272
|
-
// A zero-row withdrawal can release source still carried by another
|
|
273
|
-
// inverse's marker. Its exact ancestry also preserves later inserted runs.
|
|
274
255
|
const inverse = all.findLast(prior => prior.reversedHunk === hunk.id && pieces.includes(prior.marker));
|
|
275
256
|
if (!inverse) throw Error('reactivation');
|
|
276
257
|
const runs = all.filter(prior => prior.actId === inverse.actId && prior.reversedHunk === hunk.id && pieces.includes(prior.marker));
|
|
@@ -302,8 +283,6 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
|
|
|
302
283
|
processed.push(record);
|
|
303
284
|
const picture = prepared.effects.pictures.get(tx.id);
|
|
304
285
|
if (picture) {
|
|
305
|
-
// The new codec bytes keep the exact inserted run's ownership. Do not
|
|
306
|
-
// cross a later edit, combine authors, or create another reversible act.
|
|
307
286
|
for (const row of splices) {
|
|
308
287
|
boundary(row.pos + row.removed.length);
|
|
309
288
|
const start = boundary(row.pos), end = boundary(row.pos + row.removed.length);
|
|
@@ -318,15 +297,11 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
|
|
|
318
297
|
continue;
|
|
319
298
|
}
|
|
320
299
|
if (references.size) {
|
|
321
|
-
// Exact inverse rows restore the previous run identities. Retain the inverse's
|
|
322
|
-
// own transient marker too, so reversing it restores that same ancestry again.
|
|
323
300
|
const invert = targets => {
|
|
324
301
|
const restored = pieces.slice(), inverses = [], priorSplices = options.keepHunk ? [] : null;
|
|
325
302
|
for (let index = targets.length - 1; index >= 0; index--) {
|
|
326
303
|
const target = targets[index], first = restored.indexOf(target.marker);
|
|
327
304
|
if (first < 0) return null;
|
|
328
|
-
// Each run of the target's own pieces inverts alone: a later author's pieces between
|
|
329
|
-
// its runs stay in place, outside every inverse, so reversing it cannot move them.
|
|
330
305
|
const runs = [];
|
|
331
306
|
for (let at = first; at < restored.length; at++) if (owns(restored[at], target))
|
|
332
307
|
if (runs.at(-1)?.[1] === at) runs.at(-1)[1]++; else runs.push([at, at + 1]);
|
|
@@ -346,8 +321,6 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
|
|
|
346
321
|
hunk.chosen = !keep;
|
|
347
322
|
}
|
|
348
323
|
hunk.kept = selected.has(tx.id) && !hunk.chosen;
|
|
349
|
-
// A semantic owner retaining this inverse also retains the exact ancestors
|
|
350
|
-
// it restored; effect reactivation must not change them behind that owner.
|
|
351
324
|
if (hunk.kept) for (let prior = target; prior; prior = byId.get(prior.reversedHunk)) keptInverseAncestors.add(prior.id);
|
|
352
325
|
hunk.marker = {mark: hunk};
|
|
353
326
|
const insertion = replacement.map(piece => piece.text ? {...piece,
|
|
@@ -359,10 +332,6 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
|
|
|
359
332
|
}
|
|
360
333
|
return replays(pieces, restored, splices) ? {restored, inverses} : null;
|
|
361
334
|
};
|
|
362
|
-
// An inverse taken while a later edit had replaced some of the act's hunks restored only
|
|
363
|
-
// the rest. Those replaced hunks survive it: no later projection treats them as undone.
|
|
364
|
-
// Recorded splices decide. An author who stored no splice still inverts the one
|
|
365
|
-
// shared hunk; an empty inverse does not match that text change and leaves it.
|
|
366
335
|
const direct = all.filter(hunk => hunk.active && references.has(hunk.actId));
|
|
367
336
|
const targets = direct.length ? direct : all.filter(hunk => hunk.active && !hunk.reversedHunk && !hunk.derived &&
|
|
368
337
|
hunk.coverIds?.some(id => references.has(id)));
|
|
@@ -399,16 +368,12 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
|
|
|
399
368
|
}
|
|
400
369
|
if (!references.size) coverIdsFor(tx.id);
|
|
401
370
|
const priorSplices = options.keepHunk ? [] : null;
|
|
402
|
-
// An admitted codec finish replaces the complete embedded definition/reference.
|
|
403
|
-
// Keep its inserted run whole, including byte-identical prefixes of a re-edit;
|
|
404
|
-
// the previous picture stays in `before` for Undo, not as mixed live ownership.
|
|
405
371
|
const hunks = row => pictureInsertions.get(tx.id)?.some(text => row.inserted.includes(text)) ? [{...row}] : historyHunks(row);
|
|
406
372
|
for (let rowIndex = 0; rowIndex < splices.length; rowIndex++) {
|
|
407
373
|
const original = splices[rowIndex];
|
|
408
374
|
let rows;
|
|
409
375
|
if (options.splitSplice) {
|
|
410
376
|
const beforeSource = textOf(pieces), parts = options.splitSplice({actId: tx.id, beforeSource, splice: original});
|
|
411
|
-
// Semantic boundaries only refine this act's exact bytes, never its history.
|
|
412
377
|
if (!Array.isArray(parts) || _rapierTransformSplices(beforeSource, parts) !== _rapierTransformSplices(beforeSource, [original]))
|
|
413
378
|
throw Error('hunk_projection');
|
|
414
379
|
rows = parts.flatMap(hunks);
|
|
@@ -421,14 +386,11 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
|
|
|
421
386
|
const start = boundary(row.pos), end = boundary(row.pos + row.removed.length);
|
|
422
387
|
let at = start.last, count = Math.max(0, end.first - at);
|
|
423
388
|
if (!row.removed.length) {
|
|
424
|
-
// A new insertion at a replacement's left boundary stays before that replacement.
|
|
425
389
|
const marker = pieces.slice(start.first, start.last).findIndex(item => item.mark?.inserted.length);
|
|
426
390
|
at = marker < 0 ? start.last : start.first + marker; count = 0;
|
|
427
391
|
}
|
|
428
392
|
const removed = pieces.slice(at, at + count);
|
|
429
393
|
if (textOf(removed) !== row.removed) throw Error('replay');
|
|
430
|
-
// A semantic owner may retain a drawing hunk while prose in that same act undoes.
|
|
431
|
-
// The predicate only selects transient hunks; every original row still replays.
|
|
432
394
|
if (chosen && options.keepHunk) {
|
|
433
395
|
const keep = options.keepHunk({actId: tx.id, spliceIndex: rowIndex, hunkIndex: index,
|
|
434
396
|
splice: {pos: row.pos, removed: row.removed, inserted: row.inserted}, beforeSource: textOf(pieces), actSource, priorSplices});
|
|
@@ -459,10 +421,6 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
|
|
|
459
421
|
let offset = 0;
|
|
460
422
|
for (const piece of pieces) if (piece.text) { origins.set(piece, offset); offset += piece.text.length; }
|
|
461
423
|
const superseded = [];
|
|
462
|
-
// Survival means an inverse left this hunk in place because a later independent
|
|
463
|
-
// edit still covered it. Once that edit is gone, deactivating its last owner through
|
|
464
|
-
// any inverse path withdraws every exposed hunk. Undoing only the later
|
|
465
|
-
// edit reveals the survived insertion and does not discard it.
|
|
466
424
|
for (const hunk of all) if (hunk.survived) {
|
|
467
425
|
const covered = [...hunk.supersededBy].some(id => {
|
|
468
426
|
const next = byId.get(id);
|
|
@@ -486,12 +444,8 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
|
|
|
486
444
|
reason: hunk.active ? 'later_edit' : 'already_undone'});
|
|
487
445
|
continue;
|
|
488
446
|
}
|
|
489
|
-
// Restoring an inverse can restore a deletion marker without any owned text.
|
|
490
447
|
restoreHunk(hunk);
|
|
491
448
|
}
|
|
492
|
-
// A hidden withdrawal can have no splice of its own. Reversing that withdrawal
|
|
493
|
-
// reactivates the shared hunk through the inverse that still carries it.
|
|
494
|
-
// Withdrawing a later act reveals that same hunk while any original author remains.
|
|
495
449
|
const reactivated = new Set(records.filter(record => !prepared.effects.active.has(record.transaction.id) &&
|
|
496
450
|
projectedEffects.active.has(record.transaction.id)).map(record => record.transaction.id));
|
|
497
451
|
for (const hunk of all) if (!hunk.reversedHunk && !hunk.derived && hunk.coverIds?.length > 1 &&
|
|
@@ -5,14 +5,10 @@ const _RAPIER_TRANSACTION_ACTOR_LIMIT = 160;
|
|
|
5
5
|
const _RAPIER_TRANSACTION_OPERATION_LIMIT = 96;
|
|
6
6
|
const _RAPIER_TRANSACTION_REQUEST_LIMIT = 160;
|
|
7
7
|
|
|
8
|
-
/* Validate every row, not only the final string: a half-pair replacement can produce a whole
|
|
9
|
-
character while leaving unencodable fragments in the ledger; two invalid rows can also repair
|
|
10
|
-
each other. Refusal is atomic. Pre-existing damage elsewhere stays untouched and editable. */
|
|
11
8
|
function _rapierTransformSplices(text, splices, inverse = false) {
|
|
12
9
|
try {
|
|
13
10
|
if (!Array.isArray(splices)) return null;
|
|
14
11
|
let value = String(text == null ? '' : text);
|
|
15
|
-
// In Unicode mode a complete pair is one scalar, outside the surrogate range.
|
|
16
12
|
const whole = text => typeof text === 'string' && !/[\uD800-\uDFFF]/u.test(text);
|
|
17
13
|
const splitsPair = at => at > 0 && at < value.length &&
|
|
18
14
|
(value.charCodeAt(at - 1) & 0xFC00) === 0xD800 &&
|
|
@@ -81,7 +77,6 @@ function _rapierTransformMetadata(value, effect, inverse = false) {
|
|
|
81
77
|
return next;
|
|
82
78
|
}
|
|
83
79
|
|
|
84
|
-
// An origin binds transported placement to exact authored rows and their causal basis.
|
|
85
80
|
function _rapierValidAuthored(origin) {
|
|
86
81
|
if (!_rapierPlainLedgerData(origin) || !origin || Array.isArray(origin) ||
|
|
87
82
|
Object.keys(origin).length !== 3 || !Array.isArray(origin.basis) ||
|
|
@@ -95,9 +90,6 @@ function _rapierValidAuthored(origin) {
|
|
|
95
90
|
!/[\uD800-\uDFFF]/u.test(row.removed) && !/[\uD800-\uDFFF]/u.test(row.inserted));
|
|
96
91
|
}
|
|
97
92
|
|
|
98
|
-
// The lookup is an observation of one call's supplied records. Material rows keep
|
|
99
|
-
// their existing borrowed-row API; an inverse owns fresh rows. Navigation only
|
|
100
|
-
// follows strictly earlier revisions, including when an unadmitted list is unordered.
|
|
101
93
|
function _rapierRecordIndex(records) {
|
|
102
94
|
const byId = new Map(), splices = new Map(), metadata = new Map();
|
|
103
95
|
for (const record of records) {
|
|
@@ -139,8 +131,6 @@ function _rapierRecordSplices(entry, ledger) {
|
|
|
139
131
|
} catch (_) { return null; }
|
|
140
132
|
}
|
|
141
133
|
|
|
142
|
-
// Source identifies a retained checkpoint even when host revision numbers differ.
|
|
143
|
-
// The History Slider compares now → earlier: that answer is the earlier → now walk, inverted.
|
|
144
134
|
function journalOperations(before, after, journal) {
|
|
145
135
|
if (typeof before !== 'string' || typeof after !== 'string' || !Array.isArray(journal)) return;
|
|
146
136
|
if (before === after) return [];
|
|
@@ -151,15 +141,34 @@ function journalOperations(before, after, journal) {
|
|
|
151
141
|
|
|
152
142
|
function journalWalk(before, after, journal) {
|
|
153
143
|
try {
|
|
154
|
-
|
|
144
|
+
const pieces = [after];
|
|
155
145
|
const lookup = _rapierRecordIndex(journal), groups = [];
|
|
146
|
+
let length = after.length;
|
|
147
|
+
const locate = at => { let index = 0; for (; index < pieces.length - 1 && at >= pieces[index].length; index++) at -= pieces[index].length; return [index, at]; };
|
|
148
|
+
const codeAt = at => { const [index, offset] = locate(at); return pieces[index].charCodeAt(offset); };
|
|
149
|
+
const read = (from, to) => { let out = ''; for (let [index, offset] = locate(from); from < to && index < pieces.length; index++, offset = 0) { const part = pieces[index].slice(offset, offset + to - from); out += part; from += part.length; } return out; };
|
|
150
|
+
const cut = at => { const [index, offset] = locate(at); if (offset > 0 && offset < pieces[index].length) pieces.splice(index, 1, pieces[index].slice(0, offset), pieces[index].slice(offset)); };
|
|
151
|
+
const whole = text => typeof text === 'string' && !/[\uD800-\uDFFF]/u.test(text);
|
|
152
|
+
const splitsPair = at => at > 0 && at < length && (codeAt(at - 1) & 0xFC00) === 0xD800 && (codeAt(at) & 0xFC00) === 0xDC00;
|
|
156
153
|
for (let index = journal.length - 1; index >= 0; index--) {
|
|
157
154
|
const rows = lookup.splices(journal[index]);
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
if (!Array.isArray(rows)) return;
|
|
156
|
+
for (const row of rows.slice().reverse()) {
|
|
157
|
+
if (!row || !Number.isSafeInteger(row.pos) || row.pos < 0 || Object.is(row.pos, -0)) return;
|
|
158
|
+
const pos = row.pos, removed = row.inserted, inserted = row.removed;
|
|
159
|
+
if (!whole(removed) || !whole(inserted) || pos > length || pos + removed.length > length ||
|
|
160
|
+
splitsPair(pos) || splitsPair(pos + removed.length) || read(pos, pos + removed.length) !== removed) return;
|
|
161
|
+
cut(pos); cut(pos + removed.length);
|
|
162
|
+
let [first] = locate(pos), count = 0;
|
|
163
|
+
if (pos === length) first = pieces.length;
|
|
164
|
+
else if (locate(pos)[1] !== 0) first++;
|
|
165
|
+
for (let left = removed.length; left > 0; count++) left -= pieces[first + count].length;
|
|
166
|
+
pieces.splice(first, count, ...(inserted ? [inserted] : []));
|
|
167
|
+
length += inserted.length - removed.length;
|
|
168
|
+
for (let at = pieces.length - 1; at >= 0; at--) if (!pieces[at] && pieces.length > 1) pieces.splice(at, 1);
|
|
169
|
+
}
|
|
160
170
|
groups.push(rows);
|
|
161
|
-
if (
|
|
162
|
-
text = prior;
|
|
171
|
+
if (length === before.length && pieces.join('') === before) return groups.reverse().flat().map(({pos, removed, inserted}) => ({pos, removed, inserted}));
|
|
163
172
|
}
|
|
164
173
|
} catch (_) { return; }
|
|
165
174
|
}
|
|
@@ -167,14 +176,11 @@ function journalWalk(before, after, journal) {
|
|
|
167
176
|
function _rapierRecordMetadata(entry, ledger) {
|
|
168
177
|
try {
|
|
169
178
|
if (Object.hasOwn(entry, 'metadata')) return _rapierValidMetadataEffect(entry.metadata) ? entry.metadata : undefined;
|
|
170
|
-
// An expanded record owns its complete effect; only compact navigation inherits one.
|
|
171
179
|
if (Array.isArray(entry.splices)) return null;
|
|
172
180
|
return _rapierRecordIndex(ledger || []).metadata(entry);
|
|
173
181
|
} catch (_) { return undefined; }
|
|
174
182
|
}
|
|
175
183
|
|
|
176
|
-
// A codec replacement owns only one inserted definition and its reference. Its
|
|
177
|
-
// source act remains the picture's author; an arbitrary edit cannot claim upkeep.
|
|
178
184
|
function _rapierPictureFinish(record, original, earlier, lookup) {
|
|
179
185
|
const tx = record?.transaction, rows = record?.splices;
|
|
180
186
|
if (tx?.operation !== 'document.finish-picture' || !tx.sourceTransactionId || tx.sourceTransactionIds ||
|
|
@@ -221,9 +227,6 @@ function _rapierPictureFinish(record, original, earlier, lookup) {
|
|
|
221
227
|
});
|
|
222
228
|
}
|
|
223
229
|
|
|
224
|
-
// Every active inverse keeps its original targets suppressed. Independent inverses
|
|
225
|
-
// can share a target; withdrawing one restores it only after the last blocker leaves.
|
|
226
|
-
// This includes assignments hidden by later choices. No second history is retained.
|
|
227
230
|
function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null) {
|
|
228
231
|
try {
|
|
229
232
|
if (!Array.isArray(records)) return {ok: false};
|
|
@@ -238,9 +241,6 @@ function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null
|
|
|
238
241
|
if (was !== node.active) for (const ref of node.references) pending.push([ref, node.active ? 1 : -1]);
|
|
239
242
|
}
|
|
240
243
|
};
|
|
241
|
-
// Most admission needs only the final effect graph. A backwards pass proves it
|
|
242
|
-
// in linear work; prefix activity is asked only by native Redo, picture custody,
|
|
243
|
-
// or a metadata assignment. The explicit stack also bounds long inverse chains.
|
|
244
244
|
const activity = new Map();
|
|
245
245
|
const active = id => {
|
|
246
246
|
if (visit) return nodes.get(id).active;
|
|
@@ -258,8 +258,6 @@ function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null
|
|
|
258
258
|
return activity.get(id);
|
|
259
259
|
};
|
|
260
260
|
const lastUndo = new Map(), contexts = new Map(), order = [];
|
|
261
|
-
// Native Redo names an original act. Its inverse target is the last Undo
|
|
262
|
-
// in the author's proved causal view, not an unrelated later arrival.
|
|
263
261
|
const contextOf = origin => {
|
|
264
262
|
if (!origin) return null;
|
|
265
263
|
if (!_rapierValidAuthored(origin)) throw Error('origin');
|
|
@@ -338,8 +336,6 @@ function _rapierHistoryEffects(records, visit = null) {
|
|
|
338
336
|
return _rapierWalkHistoryEffects(records, null, visit);
|
|
339
337
|
}
|
|
340
338
|
|
|
341
|
-
// Every transition constrains the two initial metadata scalars. This proves each
|
|
342
|
-
// inverse's result once, before the caller supplies the exact initial metadata.
|
|
343
339
|
function _rapierMetadataProof(lookup) {
|
|
344
340
|
const values = {}, required = {}, assignments = {filename: [], docKind: []};
|
|
345
341
|
const requireInitial = (key, value) => {
|
|
@@ -355,7 +351,6 @@ function _rapierMetadataProof(lookup) {
|
|
|
355
351
|
values[key] = pair.after;
|
|
356
352
|
if (!refs.length) assignments[key].push({id: record.transaction.id, value: pair.after});
|
|
357
353
|
}
|
|
358
|
-
// Source-only rows cannot change a previously proved metadata assignment.
|
|
359
354
|
if (!effect && !refs.length) return;
|
|
360
355
|
for (const key of ['filename', 'docKind']) {
|
|
361
356
|
const expected = assignments[key].findLast(row => active(row.id));
|
|
@@ -385,7 +380,6 @@ function _rapierMetadataState(initial, records, effects = _rapierHistoryEffects(
|
|
|
385
380
|
} catch (_) { return null; }
|
|
386
381
|
}
|
|
387
382
|
|
|
388
|
-
// Prove each recorded transition and each inverse's semantic result, not only the final values.
|
|
389
383
|
function _rapierReplayMetadata(initial, records) {
|
|
390
384
|
if (!_rapierValidMetadata(initial)) return null;
|
|
391
385
|
try {
|
|
@@ -405,16 +399,9 @@ function _rapierHasHistoryEffect(record, earlier = []) {
|
|
|
405
399
|
return _rapierHistoryEffects([...earlier, {...record, transaction: {...tx, id}}]).ok;
|
|
406
400
|
}
|
|
407
401
|
|
|
408
|
-
// A cached observation owns every byte-bearing record. Mutable graphs are compared
|
|
409
|
-
// through descriptors on every call; only a proved, recursively frozen graph can
|
|
410
|
-
// bypass that walk. Per-object weak entries keep one latest copy, not a revision log.
|
|
411
402
|
const _rapierPrepareHistoryRecords = (() => {
|
|
412
403
|
const copies = new WeakMap();
|
|
413
|
-
// Repeated queries share the latest proved snapshot. A live but inactive input
|
|
414
|
-
// does not need to retain its record/effect indexes; immutable copies remain weak.
|
|
415
404
|
let prepared = new WeakMap();
|
|
416
|
-
// Object.freeze does not freeze Map/Set entries. Keep those collections inside
|
|
417
|
-
// the preparation and expose only immutable values through reading closures.
|
|
418
405
|
const view = collection => Object.freeze({size: collection.size, has: key => collection.has(key),
|
|
419
406
|
...(collection instanceof Map ? {get: key => collection.get(key)} : {}),
|
|
420
407
|
keys: () => collection.keys(), values: () => collection.values(), entries: () => collection.entries(),
|
|
@@ -456,8 +443,6 @@ const _rapierPrepareHistoryRecords = (() => {
|
|
|
456
443
|
frame.previous = previous?.copy;
|
|
457
444
|
frame.height = 0;
|
|
458
445
|
if (previous?.frozen) {
|
|
459
|
-
// A frozen shell fixes its descriptors, but every mutable child is still
|
|
460
|
-
// observed afresh. Fixed scalar fields need no second Unicode/data proof.
|
|
461
446
|
frame.frozen = frame.immutable = true;
|
|
462
447
|
frame.descriptors = previous.descriptors; frame.fields = previous.fields;
|
|
463
448
|
frame.out = null; frame.leaving = true; active.add(value); pending.push(frame);
|
|
@@ -467,8 +452,6 @@ const _rapierPrepareHistoryRecords = (() => {
|
|
|
467
452
|
}
|
|
468
453
|
continue;
|
|
469
454
|
}
|
|
470
|
-
// Check frozen-ness before capturing descriptors. A caller freezing a mutable
|
|
471
|
-
// input later cannot retroactively certify an earlier view of its properties.
|
|
472
455
|
frame.frozen = frame.immutable = Object.isFrozen(value);
|
|
473
456
|
const array = Array.isArray(value), proto = Object.getPrototypeOf(value);
|
|
474
457
|
if (!array && !plainPrototype(proto)) throw Error('history_data');
|
|
@@ -535,12 +518,6 @@ const _rapierPrepareHistoryRecords = (() => {
|
|
|
535
518
|
};
|
|
536
519
|
})();
|
|
537
520
|
|
|
538
|
-
// A ledger record as the journal and the Notes return validate it below: plain data, nothing
|
|
539
|
-
// undefined anywhere in it. A live record could carry a row whose type or order is not yet known (a
|
|
540
|
-
// block just inserted, before its first render: `type: undefined`), and the validator would refuse
|
|
541
|
-
// the whole ledger for that one field -- silently at boot, where a refused ledger is cleared, and
|
|
542
|
-
// as "Rapier could not be reopened: the document history could not be restored" on a return from
|
|
543
|
-
// Notes. The copy is made once per record; records are frozen, so it never goes stale.
|
|
544
521
|
const _rapierJournalRecord = (() => {
|
|
545
522
|
const copies = new WeakMap();
|
|
546
523
|
return record => {
|
|
@@ -551,10 +528,6 @@ const _rapierJournalRecord = (() => {
|
|
|
551
528
|
})();
|
|
552
529
|
|
|
553
530
|
function _rapierValidLedgerRecord(record, prior, earlier, documentAuthority, maxDocumentBytes, utf8Length, allowGap = false) {
|
|
554
|
-
// Recovery records are data, not executable objects. Inspect descriptors without invoking
|
|
555
|
-
// accessors, and use an ancestor set (not a global seen set) so shared acyclic data stays valid.
|
|
556
|
-
// Iterative traversal refuses cycles without exhausting the call stack. Proxies/counters that
|
|
557
|
-
// throw are refused by the same gate rather than escaping into document recovery.
|
|
558
531
|
try {
|
|
559
532
|
if (!_rapierPlainLedgerData(record) || !Array.isArray(earlier)) return false;
|
|
560
533
|
const own = Array.isArray(record?.splices), lookup = own ? null : _rapierRecordIndex(earlier);
|
|
@@ -566,8 +539,6 @@ function _rapierValidLedgerRecord(record, prior, earlier, documentAuthority, max
|
|
|
566
539
|
} catch (_) { return false; }
|
|
567
540
|
}
|
|
568
541
|
|
|
569
|
-
// Both callers proved plain data themselves: the public single-row validator above,
|
|
570
|
-
// or the private owned-snapshot preparation. No caller-supplied trusted flag exists.
|
|
571
542
|
function _rapierCheckLedgerRecord(record, prior, documentAuthority, maxDocumentBytes, utf8Length, allowGap,
|
|
572
543
|
{splices, metadata, semantic, duplicate}) {
|
|
573
544
|
try {
|
|
@@ -579,8 +550,6 @@ function _rapierCheckLedgerRecord(record, prior, documentAuthority, maxDocumentB
|
|
|
579
550
|
if ((!own && !navigation) || (own && record.splices.length > 64) ||
|
|
580
551
|
(record.transaction?.reverts && record.transaction?.reapplies)) return false;
|
|
581
552
|
const metadataChanged = Object.values(metadata || {}).some(pair => pair.before !== pair.after);
|
|
582
|
-
// An identical concurrent replacement keeps its authored rows and an empty
|
|
583
|
-
// physical splice. The origin proof, not this gate, shows the effect is already present.
|
|
584
553
|
const coincidentWitness = !splices?.length && record.beforeHash === record.afterHash &&
|
|
585
554
|
record.authored && _rapierValidAuthored(record.authored) && record.authored.splices.some(row => row.removed.length);
|
|
586
555
|
if (metadata === undefined || (!splices?.length && !metadataChanged && !semantic && !coincidentWitness) ||
|
|
@@ -597,7 +566,6 @@ function _rapierCheckLedgerRecord(record, prior, documentAuthority, maxDocumentB
|
|
|
597
566
|
const transaction = record.transaction;
|
|
598
567
|
const nullableText = (value, limit, empty = true) => value === null ||
|
|
599
568
|
(typeof value === 'string' && value.length <= limit && (empty || value.length > 0));
|
|
600
|
-
// Identity fields are bounded individually; durable history has no record-count limit.
|
|
601
569
|
const optional = (value, valid) => value === undefined || valid(value);
|
|
602
570
|
const identity = (value, limit) => typeof value === 'string' && value.length > 0 && value.length <= limit;
|
|
603
571
|
if (!transaction || typeof transaction.id !== 'string' || !transaction.id.length ||
|