rapier-html 1.1.99 → 1.1.102
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/BUILD.json +95 -4
- package/README.md +23 -24
- package/dist/ledger/LICENSE +10 -2
- package/dist/ledger/data.mjs +1 -1
- package/dist/ledger/diff.mjs +610 -0
- package/dist/ledger/format.mjs +12 -5
- package/dist/ledger/hash.mjs +16 -22
- package/dist/ledger/history.mjs +309 -0
- package/dist/ledger/inverse-source.mjs +316 -0
- package/dist/ledger/journal-records.mjs +116 -12
- package/dist/ledger/transport.mjs +123 -19
- package/dist/rapier.html +399 -618
- package/dist/wrap.mjs +28 -4
- package/package.json +1 -1
package/dist/ledger/hash.mjs
CHANGED
|
@@ -21,33 +21,27 @@ const K = new Uint32Array([
|
|
|
21
21
|
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
|
|
22
22
|
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2,
|
|
23
23
|
]);
|
|
24
|
-
|
|
25
|
-
// faster than through destructured arrays, with the same digest.
|
|
26
|
-
const W = new Int32Array(64);
|
|
24
|
+
const rotate = (x, n) => (x >>> n) | (x << (32 - n));
|
|
27
25
|
export function sha256(text) {
|
|
28
|
-
const bytes = new TextEncoder().encode(text)
|
|
29
|
-
const padded = new Uint8Array(Math.ceil((length + 9) / 64) * 64)
|
|
30
|
-
padded.set(bytes); padded[length] = 0x80;
|
|
31
|
-
const
|
|
32
|
-
padded
|
|
33
|
-
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
for (let offset = 0; offset < end; offset += 64) {
|
|
37
|
-
for (let i = 0, j = offset; i < 16; i++, j += 4) w[i] = padded[j] << 24 | padded[j + 1] << 16 | padded[j + 2] << 8 | padded[j + 3];
|
|
26
|
+
const bytes = new TextEncoder().encode(text);
|
|
27
|
+
const padded = new Uint8Array(Math.ceil((bytes.length + 9) / 64) * 64);
|
|
28
|
+
padded.set(bytes); padded[bytes.length] = 0x80;
|
|
29
|
+
const data = new DataView(padded.buffer), bits = bytes.length * 8;
|
|
30
|
+
data.setUint32(padded.length - 8, Math.floor(bits / 0x100000000)); data.setUint32(padded.length - 4, bits >>> 0);
|
|
31
|
+
const h = new Uint32Array([0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]), w = new Uint32Array(64);
|
|
32
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
33
|
+
for (let i = 0; i < 16; i++) w[i] = data.getUint32(offset + i * 4);
|
|
38
34
|
for (let i = 16; i < 64; i++) {
|
|
39
35
|
const x = w[i - 15], y = w[i - 2];
|
|
40
|
-
w[i] =
|
|
36
|
+
w[i] = w[i - 16] + (rotate(x, 7) ^ rotate(x, 18) ^ (x >>> 3)) + w[i - 7] + (rotate(y, 17) ^ rotate(y, 19) ^ (y >>> 10));
|
|
41
37
|
}
|
|
42
|
-
let a
|
|
38
|
+
let [a,b,c,d,e,f,g,k] = h;
|
|
43
39
|
for (let i = 0; i < 64; i++) {
|
|
44
|
-
const t1 = (k + ((e
|
|
45
|
-
const t2 = (((a
|
|
46
|
-
k = g; g = f; f = e; e = (d + t1)
|
|
40
|
+
const t1 = (k + (rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i]) >>> 0;
|
|
41
|
+
const t2 = ((rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
|
|
42
|
+
k = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
|
|
47
43
|
}
|
|
48
|
-
|
|
44
|
+
for (const [i, value] of [a,b,c,d,e,f,g,k].entries()) h[i] += value;
|
|
49
45
|
}
|
|
50
|
-
|
|
51
|
-
for (const x of [h0, h1, h2, h3, h4, h5, h6, h7]) out += (x >>> 0).toString(16).padStart(8, '0');
|
|
52
|
-
return out;
|
|
46
|
+
return [...h].map(x => x.toString(16).padStart(8, '0')).join('');
|
|
53
47
|
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Transient projections and selective inverses of the existing canonical journal.
|
|
3
|
+
import {_rapierTransformSplices, _rapierRecordSplices, _rapierRecordMetadata, _rapierValidMetadata, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState, _rapierReplayMetadata} from './journal-records.mjs';
|
|
4
|
+
import {projectInverseSource} from './inverse-source.mjs';
|
|
5
|
+
import {textRoot, rootAfter} from './hash.mjs';
|
|
6
|
+
import {authoredHistory} from './transport.mjs';
|
|
7
|
+
import {plainData} from './data.mjs';
|
|
8
|
+
|
|
9
|
+
const historyFailure = reason => ({ok: false, reason});
|
|
10
|
+
const historyInteger = value => Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0);
|
|
11
|
+
const historyBoundary = (source, at) => historyInteger(at) && at <= source.length && !(at > 0 && at < source.length &&
|
|
12
|
+
(source.charCodeAt(at - 1) & 0xFC00) === 0xD800 && (source.charCodeAt(at) & 0xFC00) === 0xDC00);
|
|
13
|
+
|
|
14
|
+
function historyAct(record, effects = null) {
|
|
15
|
+
const tx = record.transaction;
|
|
16
|
+
return {id: tx.id, actor: {...tx.actor}, createdAt: tx.createdAt, baseRevision: tx.baseRevision, revision: tx.revision,
|
|
17
|
+
operation: tx.operation || 'document.change',
|
|
18
|
+
...(tx.turnId ? {turnId: tx.turnId} : {}),
|
|
19
|
+
...(tx.contribution ? {contribution: tx.contribution} : {}),
|
|
20
|
+
...(tx.sourceTransactionId ? {sourceTransactionId: tx.sourceTransactionId} : {}),
|
|
21
|
+
...(tx.sourceTransactionIds?.length ? {sourceTransactionIds: tx.sourceTransactionIds.slice()} : {}),
|
|
22
|
+
...(tx.reverts ? {reverts: tx.reverts} : {}), ...(tx.reapplies ? {reapplies: tx.reapplies} : {}),
|
|
23
|
+
...(record.changeSet && Object.hasOwn(record.changeSet, 'label') ? {label: record.changeSet.label} : {}),
|
|
24
|
+
...(Array.isArray(tx.affectedBlockIds) ? {affectedBlockIds: tx.affectedBlockIds.slice()} : {}),
|
|
25
|
+
...(record.metadata ? {metadata: plainData(record.metadata)} : {}),
|
|
26
|
+
...(effects ? {active: effects.active.has(tx.id)} : {})};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function groupHistoryActs(acts) {
|
|
30
|
+
const groups = [], grouped = new Map();
|
|
31
|
+
for (const act of acts) {
|
|
32
|
+
const key = JSON.stringify([act.actor.kind, act.actor.id, act.turnId || null, act.turnId ? null : act.id]);
|
|
33
|
+
let group = grouped.get(key);
|
|
34
|
+
if (!group) {
|
|
35
|
+
group = {id: act.turnId || act.id, actor: {...act.actor}, actIds: [], createdAt: act.createdAt,
|
|
36
|
+
...(act.turnId ? {turnId: act.turnId} : {})};
|
|
37
|
+
groups.push(group); grouped.set(key, group);
|
|
38
|
+
}
|
|
39
|
+
group.actIds.push(act.id);
|
|
40
|
+
}
|
|
41
|
+
return groups;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A picture's codec replacement travels with its original navigation step.
|
|
45
|
+
// Match every complete replacement exactly before moving any source bytes.
|
|
46
|
+
export function pictureHistorySplices({source, records}, target, redo) {
|
|
47
|
+
const id = target?.transaction?.id, at = records.indexOf(target);
|
|
48
|
+
if (!id || at < 0 || target.transaction.operation === 'document.finish-picture' || !Array.isArray(target.splices)) return null;
|
|
49
|
+
const later = records.slice(at + 1).filter(row => row?.transaction?.operation === 'document.finish-picture' && Array.isArray(row.splices) && row.splices.length);
|
|
50
|
+
if (!later.length) return null;
|
|
51
|
+
const own = new Set(later.filter(row => row.transaction.sourceTransactionId === id));
|
|
52
|
+
const step = redo ? target.splices : target.splices.slice().reverse().map(row => ({pos: row.pos, removed: row.inserted, inserted: row.removed}));
|
|
53
|
+
const plan = moveOthers => {
|
|
54
|
+
let text = source;
|
|
55
|
+
const rows = [], moved = new Set();
|
|
56
|
+
const only = needle => { const found = text.indexOf(needle); return found >= 0 && text.indexOf(needle, found + 1) < 0 ? found : -1; };
|
|
57
|
+
const apply = row => { const next = _rapierTransformSplices(text, [row]); if (next === null) throw Error('picture_target'); text = next; rows.push(row); };
|
|
58
|
+
const swap = (record, back) => {
|
|
59
|
+
const found = record.splices.map(splice => { const from = back ? splice.inserted : splice.removed; return from ? only(from) : -1; });
|
|
60
|
+
if (found.every(position => position < 0)) return false;
|
|
61
|
+
if (found.some(position => position < 0)) throw Error('partial_picture');
|
|
62
|
+
for (const splice of record.splices) {
|
|
63
|
+
const from = back ? splice.inserted : splice.removed, to = back ? splice.removed : splice.inserted;
|
|
64
|
+
apply({pos: only(from), removed: from, inserted: to});
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
};
|
|
68
|
+
for (const record of later.slice().reverse()) if ((moveOthers || own.has(record)) && swap(record, true)) moved.add(record);
|
|
69
|
+
for (const splice of step) apply(splice);
|
|
70
|
+
for (const record of later) if (own.has(record) ? redo : moved.has(record)) swap(record, false);
|
|
71
|
+
return rows;
|
|
72
|
+
};
|
|
73
|
+
try { const rows = plan(false); return !own.size && rows.length === step.length ? null : rows; } catch (_) {}
|
|
74
|
+
try { return plan(true); } catch (_) { return null; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Reading an admitted repair can recover pre-existing lone UTF-16 units. This is
|
|
78
|
+
// not a document mutation; the forward replay and roots still validate every row.
|
|
79
|
+
function historyPreviousSource(source, splices) {
|
|
80
|
+
let value = source;
|
|
81
|
+
for (let index = splices.length - 1; index >= 0; index--) {
|
|
82
|
+
const row = splices[index];
|
|
83
|
+
if (!row || !historyInteger(row.pos) || typeof row.removed !== 'string' || typeof row.inserted !== 'string' ||
|
|
84
|
+
row.pos > value.length || row.inserted.length > value.length - row.pos ||
|
|
85
|
+
value.slice(row.pos, row.pos + row.inserted.length) !== row.inserted) return null;
|
|
86
|
+
value = value.slice(0, row.pos) + row.removed + value.slice(row.pos + row.inserted.length);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// This is a projection of the existing records. No source snapshots or provenance survive the call.
|
|
92
|
+
function prepareHistory(input) {
|
|
93
|
+
try {
|
|
94
|
+
if (!input || typeof input.source !== 'string' || !Array.isArray(input.records) || !_rapierValidMetadata(input.metadata)) return historyFailure('history_invalid');
|
|
95
|
+
const source = input.source, records = plainData(input.records), ids = new Set(), rows = [];
|
|
96
|
+
const effects = _rapierHistoryEffects(records);
|
|
97
|
+
if (!effects.ok) return historyFailure('history_invalid');
|
|
98
|
+
const revision = input.revision ?? records.at(-1)?.transaction?.revision ?? 0;
|
|
99
|
+
if (!historyInteger(revision)) return historyFailure('history_invalid');
|
|
100
|
+
let prior = input.earliestRevision ?? records[0]?.transaction?.baseRevision ?? revision, complete = prior === 0;
|
|
101
|
+
if (!historyInteger(prior)) return historyFailure('history_invalid');
|
|
102
|
+
for (const record of records) {
|
|
103
|
+
const tx = record?.transaction, actor = tx?.actor;
|
|
104
|
+
if (!tx || typeof tx.id !== 'string' || !tx.id || ids.has(tx.id) || !historyInteger(tx.baseRevision) ||
|
|
105
|
+
!historyInteger(tx.revision) || tx.revision !== tx.baseRevision + 1 || tx.baseRevision < prior ||
|
|
106
|
+
!actor || !['human', 'agent', 'system'].includes(actor.kind) || typeof actor.id !== 'string' || !actor.id ||
|
|
107
|
+
!(tx.createdAt === null || historyInteger(tx.createdAt)) ||
|
|
108
|
+
(tx.turnId != null && (typeof tx.turnId !== 'string' || !tx.turnId || tx.turnId.length > 160))) return historyFailure('history_invalid');
|
|
109
|
+
if (tx.baseRevision !== prior) complete = false;
|
|
110
|
+
const splices = _rapierRecordSplices(record, records), metadata = _rapierRecordMetadata(record, records);
|
|
111
|
+
if (!splices || metadata === undefined || (!splices.length &&
|
|
112
|
+
!Object.values(metadata || {}).some(pair => pair.before !== pair.after) && !effects.references.get(tx.id)?.length)) return historyFailure('history_unavailable');
|
|
113
|
+
rows.push({record, splices, metadata}); ids.add(tx.id); prior = tx.revision;
|
|
114
|
+
}
|
|
115
|
+
if (prior !== revision) return historyFailure('history_unavailable');
|
|
116
|
+
let initialSource = source, initialMetadata = {...input.metadata};
|
|
117
|
+
for (let index = rows.length - 1; index >= 0; index--) {
|
|
118
|
+
initialSource = historyPreviousSource(initialSource, rows[index].splices);
|
|
119
|
+
initialMetadata = _rapierTransformMetadata(initialMetadata, rows[index].metadata, true);
|
|
120
|
+
if (initialSource === null || !initialMetadata) return historyFailure('history_invalid');
|
|
121
|
+
}
|
|
122
|
+
const earliestRevision = rows[0]?.record.transaction.baseRevision ?? revision;
|
|
123
|
+
let replay = initialSource, root = earliestRevision === 0 ? textRoot(initialSource) : null, rootRevision = earliestRevision;
|
|
124
|
+
for (const {record, splices} of rows) {
|
|
125
|
+
const hashed = Object.hasOwn(record, 'beforeHash') || Object.hasOwn(record, 'afterHash');
|
|
126
|
+
if (hashed && (![record.beforeHash, record.afterHash].every(value =>
|
|
127
|
+
typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value)) ||
|
|
128
|
+
(splices.length ? record.beforeHash === record.afterHash : record.beforeHash !== record.afterHash)))
|
|
129
|
+
return historyFailure('history_invalid');
|
|
130
|
+
// Roots bind revision zero to the full source. A retained suffix has only a path root;
|
|
131
|
+
// omitted neutral pairs change that root, so a stated revision gap begins a new chain.
|
|
132
|
+
if (record.transaction.baseRevision !== rootRevision) root = null;
|
|
133
|
+
if (hashed) {
|
|
134
|
+
if (root !== null && record.beforeHash !== root) return historyFailure('history_invalid');
|
|
135
|
+
root = record.beforeHash;
|
|
136
|
+
}
|
|
137
|
+
replay = _rapierTransformSplices(replay, splices);
|
|
138
|
+
if (replay === null) return historyFailure('history_invalid');
|
|
139
|
+
if (root !== null) for (const row of splices) root = rootAfter(root, row);
|
|
140
|
+
if (hashed && root !== record.afterHash) return historyFailure('history_invalid');
|
|
141
|
+
rootRevision = record.transaction.revision;
|
|
142
|
+
}
|
|
143
|
+
if (records.some(row => Object.hasOwn(row, 'authored'))) authoredHistory(initialSource, rows.map(({record, splices}) => ({...record, splices})));
|
|
144
|
+
const metadata = _rapierReplayMetadata(initialMetadata, records);
|
|
145
|
+
if (replay !== source || !metadata || ['filename', 'docKind'].some(key => metadata[key] !== input.metadata[key])) return historyFailure('history_invalid');
|
|
146
|
+
return {ok: true, source, initialSource, metadata, initialMetadata, effects, rows, revision, earliestRevision, complete: input.complete !== false && complete && earliestRevision === 0};
|
|
147
|
+
} catch (_) { return historyFailure('history_invalid'); }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function replayHistory(input) {
|
|
151
|
+
const prepared = prepareHistory(input);
|
|
152
|
+
if (!prepared.ok) return prepared;
|
|
153
|
+
const {source, initialSource, metadata, initialMetadata, revision, earliestRevision, complete, rows, effects} = prepared;
|
|
154
|
+
const acts = rows.filter(({record}) => !effects.pictures.has(record.transaction.id)).map(({record}) => historyAct(record, effects));
|
|
155
|
+
return {ok: true, source, initialSource, metadata, initialMetadata, revision, earliestRevision, complete, acts,
|
|
156
|
+
effectiveActIds: effects.effectiveActIds.slice(), groups: groupHistoryActs(acts)};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function sourceBefore(input, actId) {
|
|
160
|
+
const prepared = prepareHistory(input);
|
|
161
|
+
if (!prepared.ok) return prepared;
|
|
162
|
+
let source = prepared.source, metadata = {...prepared.metadata};
|
|
163
|
+
for (let index = prepared.rows.length - 1; index >= 0; index--) {
|
|
164
|
+
const {record, splices} = prepared.rows[index];
|
|
165
|
+
source = historyPreviousSource(source, splices);
|
|
166
|
+
if (source === null) return historyFailure('history_invalid');
|
|
167
|
+
metadata = _rapierTransformMetadata(metadata, prepared.rows[index].metadata, true);
|
|
168
|
+
if (record.transaction.id === actId) return {ok: true, source, metadata, revision: record.transaction.baseRevision,
|
|
169
|
+
act: historyAct(record, prepared.effects), complete: prepared.complete, earliestRevision: prepared.earliestRevision};
|
|
170
|
+
}
|
|
171
|
+
return historyFailure('act_unavailable');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function historyRangeBefore(range, splice) {
|
|
175
|
+
const start = splice.pos, end = start + splice.inserted.length, replacement = splice.removed.length;
|
|
176
|
+
const touched = range.from === range.to ? start <= range.from && end >= range.to
|
|
177
|
+
: start === end ? range.from <= start && start <= range.to : start < range.to && end > range.from;
|
|
178
|
+
if (!touched) {
|
|
179
|
+
const shift = end <= range.from ? replacement - splice.inserted.length : 0;
|
|
180
|
+
return {from: range.from + shift, to: range.to + shift, touched: false};
|
|
181
|
+
}
|
|
182
|
+
return {from: range.from <= start ? range.from : range.from >= end ? range.from + replacement - splice.inserted.length : start,
|
|
183
|
+
to: range.to >= end ? range.to + replacement - splice.inserted.length : range.to <= start ? range.to : start + replacement,
|
|
184
|
+
touched: true};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function projectHistoryRange(prepared, from, to, actId) {
|
|
188
|
+
if (!historyBoundary(prepared.source, from) || !historyBoundary(prepared.source, to) || to < from) return historyFailure('range_invalid');
|
|
189
|
+
let text = prepared.source, range = {from, to}, before = null;
|
|
190
|
+
const acts = [];
|
|
191
|
+
for (let index = prepared.rows.length - 1; index >= 0; index--) {
|
|
192
|
+
const {record, splices} = prepared.rows[index], after = {...range, source: text.slice(range.from, range.to)};
|
|
193
|
+
let touched = false;
|
|
194
|
+
for (let at = splices.length - 1; at >= 0; at--) {
|
|
195
|
+
const mapped = historyRangeBefore(range, splices[at]); touched ||= mapped.touched;
|
|
196
|
+
range = {from: mapped.from, to: mapped.to};
|
|
197
|
+
}
|
|
198
|
+
text = historyPreviousSource(text, splices);
|
|
199
|
+
if (text === null) return historyFailure('history_invalid');
|
|
200
|
+
const previous = {...range, source: text.slice(range.from, range.to)};
|
|
201
|
+
if (record.transaction.id === actId) before = previous;
|
|
202
|
+
if (!prepared.effects.pictures.has(record.transaction.id) && (touched || record.metadata || prepared.effects.references.get(record.transaction.id)?.length))
|
|
203
|
+
acts.push({...historyAct(record, prepared.effects), before: previous, after});
|
|
204
|
+
}
|
|
205
|
+
if (actId != null && !before) return historyFailure('act_unavailable');
|
|
206
|
+
acts.reverse();
|
|
207
|
+
return {ok: true, from, to, source: prepared.source.slice(from, to), revision: prepared.revision,
|
|
208
|
+
earliestRevision: prepared.earliestRevision, complete: prepared.complete, acts, groups: groupHistoryActs(acts), ...(before ? {before} : {})};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function historyProjection(input, {from = 0, to = input?.source?.length, actId} = {}) {
|
|
212
|
+
const prepared = prepareHistory(input);
|
|
213
|
+
if (!prepared.ok) return prepared;
|
|
214
|
+
return projectHistoryRange(prepared, from, to, actId);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Several ranges share one proved source snapshot for this call.
|
|
218
|
+
export function historyProjections(input, ranges) {
|
|
219
|
+
const prepared = prepareHistory(input);
|
|
220
|
+
if (!prepared.ok) return prepared;
|
|
221
|
+
if (!Array.isArray(ranges)) return historyFailure('range_invalid');
|
|
222
|
+
return {ok: true, projections: ranges.map(({from = 0, to = prepared.source.length, actId} = {}) =>
|
|
223
|
+
structuredClone(projectHistoryRange(prepared, from, to, actId)))};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Project all reading places in one replay. Typing only invalidates this transient view.
|
|
227
|
+
export function historyPlaces(input, blocks) {
|
|
228
|
+
const prepared = prepareHistory(input);
|
|
229
|
+
if (!prepared.ok) return prepared;
|
|
230
|
+
if (!Array.isArray(blocks) || blocks.some(block => !block ||
|
|
231
|
+
!historyBoundary(prepared.source, block.from) || !historyBoundary(prepared.source, block.to) || block.to < block.from))
|
|
232
|
+
return historyFailure('range_invalid');
|
|
233
|
+
const projected = blocks.map(block => ({id: block.id, from: block.from, to: block.to,
|
|
234
|
+
source: prepared.source.slice(block.from, block.to), range: {from: block.from, to: block.to}, acts: []}));
|
|
235
|
+
let text = prepared.source;
|
|
236
|
+
for (let index = prepared.rows.length - 1; index >= 0; index--) {
|
|
237
|
+
const {record, splices} = prepared.rows[index], before = historyPreviousSource(text, splices);
|
|
238
|
+
if (before === null) return historyFailure('history_invalid');
|
|
239
|
+
for (const block of projected) {
|
|
240
|
+
const after = {...block.range, source: text.slice(block.range.from, block.range.to)};
|
|
241
|
+
let touched = false;
|
|
242
|
+
for (let at = splices.length - 1; at >= 0; at--) {
|
|
243
|
+
const mapped = historyRangeBefore(block.range, splices[at]);
|
|
244
|
+
block.range = {from: mapped.from, to: mapped.to}; touched ||= mapped.touched;
|
|
245
|
+
}
|
|
246
|
+
const previous = {...block.range, source: before.slice(block.range.from, block.range.to)};
|
|
247
|
+
if (touched && previous.source !== after.source && !prepared.effects.pictures.has(record.transaction.id)) block.acts.push({...historyAct(record), before: previous, after});
|
|
248
|
+
}
|
|
249
|
+
text = before;
|
|
250
|
+
}
|
|
251
|
+
const acts = prepared.rows.filter(({record}) => !prepared.effects.pictures.has(record.transaction.id)).map(({record}) => historyAct(record));
|
|
252
|
+
const present = new Map(), effects = new Map();
|
|
253
|
+
for (const act of acts) {
|
|
254
|
+
const changes = [];
|
|
255
|
+
const set = (id, value) => {
|
|
256
|
+
const prior = present.get(id) === true;
|
|
257
|
+
present.set(id, value); changes.push({id, before: prior, after: value});
|
|
258
|
+
};
|
|
259
|
+
if (act.reapplies) {
|
|
260
|
+
for (const change of effects.get(act.reapplies) || [{id: act.reapplies, after: true}]) set(change.id, change.after);
|
|
261
|
+
} else {
|
|
262
|
+
const ids = act.sourceTransactionIds || (act.sourceTransactionId ? [act.sourceTransactionId] : act.reverts ? [act.reverts] : []);
|
|
263
|
+
for (const id of ids) {
|
|
264
|
+
for (const change of (effects.get(id) || [{id, before: false, after: true}]).slice().reverse()) {
|
|
265
|
+
if (present.get(change.id) === change.after) set(change.id, change.before);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
set(act.id, true); effects.set(act.id, changes);
|
|
270
|
+
}
|
|
271
|
+
for (const block of projected) { block.acts.reverse(); delete block.range; }
|
|
272
|
+
return {ok: true, source: prepared.source, initialSource: prepared.initialSource, revision: prepared.revision,
|
|
273
|
+
earliestRevision: prepared.earliestRevision, complete: prepared.complete, acts, blocks: projected,
|
|
274
|
+
reversed: acts.filter(act => present.get(act.id) !== true).map(act => act.id)};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Runs retain only exact replacement ancestry. A later replacement owns an ambiguous hunk whole.
|
|
278
|
+
export function selectiveUndo(input, actIds, options = {}) {
|
|
279
|
+
const prepared = prepareHistory(input);
|
|
280
|
+
if (!prepared.ok) return prepared;
|
|
281
|
+
if (!Array.isArray(actIds) || !actIds.length || actIds.some(id => typeof id !== 'string' || !id)) return historyFailure('act_invalid');
|
|
282
|
+
if (!options || typeof options !== 'object' || Array.isArray(options) ||
|
|
283
|
+
(options.keepHunk !== undefined && typeof options.keepHunk !== 'function') ||
|
|
284
|
+
(options.splitSplice !== undefined && typeof options.splitSplice !== 'function')) return historyFailure('act_invalid');
|
|
285
|
+
const requested = new Set(actIds), available = new Set(prepared.rows.map(({record}) => record.transaction.id));
|
|
286
|
+
if ([...requested].some(id => !available.has(id))) return historyFailure('act_unavailable');
|
|
287
|
+
const selected = new Set([...requested].filter(id => prepared.effects.active.has(id)));
|
|
288
|
+
const records = prepared.rows.map(({record, splices}) => ({...record, splices}));
|
|
289
|
+
let inverseId = 'planned-inverse'; while (available.has(inverseId)) inverseId += ':';
|
|
290
|
+
const inverse = {splices: [], transaction: {id: inverseId, sourceTransactionIds: [...selected]}};
|
|
291
|
+
const projectedEffects = selected.size ? _rapierHistoryEffects([...records, inverse]) : prepared.effects;
|
|
292
|
+
const afterMetadata = _rapierMetadataState(prepared.initialMetadata, records, projectedEffects);
|
|
293
|
+
if (!afterMetadata) return historyFailure('history_invalid');
|
|
294
|
+
const metadata = _rapierMetadataDelta(prepared.metadata, afterMetadata), effectChanged = selected.size > 0;
|
|
295
|
+
const authoredEffectChanged = records.some(record => !prepared.effects.references.get(record.transaction.id)?.length &&
|
|
296
|
+
prepared.effects.active.has(record.transaction.id) !== projectedEffects.active.has(record.transaction.id));
|
|
297
|
+
if (!authoredEffectChanged) return {ok: true, splices: [], after: prepared.source, actIds: [...requested],
|
|
298
|
+
sourceTransactionIds: [...selected], metadata, afterMetadata, effectChanged, unchanged: !metadata && !effectChanged,
|
|
299
|
+
superseded: [], overlaps: []};
|
|
300
|
+
try {
|
|
301
|
+
const transported = records.some(row => row.authored) ? authoredHistory(prepared.initialSource, records, selected) : null;
|
|
302
|
+
const rows = transported ? transported.records.map(record => ({record, splices: record.splices,
|
|
303
|
+
sourceIndices: transported.sourceIndices.get(record.transaction.id)})) : prepared.rows;
|
|
304
|
+
const result = projectInverseSource({...prepared, rows}, selected, projectedEffects, options);
|
|
305
|
+
if (!result.ok) return result;
|
|
306
|
+
return {...result, actIds: [...requested], sourceTransactionIds: [...selected], metadata, afterMetadata,
|
|
307
|
+
effectChanged, unchanged: result.after === prepared.source && !metadata && !effectChanged};
|
|
308
|
+
} catch (_) { return historyFailure('history_invalid'); }
|
|
309
|
+
}
|