rapier-html 1.2.42 → 1.2.43

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 CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "package": "rapier-html",
3
- "version": "1.2.42",
3
+ "version": "1.2.43",
4
4
  "path": "dist/rapier.html",
5
- "bytes": 5828319,
6
- "sha256": "12f1cc548ff4e0d95a105b0170ce2f40aed8989cc58a4937f43eacb05581833c",
7
- "builtAt": "2026-10-24T21:57:08.348Z",
5
+ "bytes": 5832352,
6
+ "sha256": "233e582745310b79d57c2fc26435ed02c00d657f6c1f7d8b676b2d678522e3cd",
7
+ "builtAt": "2026-10-25T00:05:03.516Z",
8
8
  "node": "v22.23.4",
9
9
  "canonical": true,
10
10
  "packing": "zopfli",
package/README.md CHANGED
@@ -7,8 +7,8 @@ Turn Markdown into one offline HTML file that contains the full Rapier editor.
7
7
  <img src="https://raw.githubusercontent.com/jackskip22/rapier-plugins/main/assets/write-390-light.png" alt="The Rapier editor on a phone: a launch plan with an agent's change marked" width="320">
8
8
  </picture>
9
9
 
10
- Try it: `npx rapier-html@1.2.42 notes.md`, then open `notes.rapier.html` in any browser.
11
- Install: `npm install rapier-html@1.2.42`.
10
+ Try it: `npx rapier-html@1.2.43 notes.md`, then open `notes.rapier.html` in any browser.
11
+ Install: `npm install rapier-html@1.2.43`.
12
12
 
13
13
  It opens on a phone or computer, with no account. People read, edit, draw and paint; agents hand over documents, show
14
14
  what changed and get the edited copy back.
@@ -26,14 +26,14 @@ The file needs no hosted workspace.
26
26
  Needs Node 22 or newer.
27
27
 
28
28
  ```sh
29
- npx -- rapier-html@1.2.42 notes.md # the editor, notes.md inside it, written beside the file
30
- npx -- rapier-html@1.2.42 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
31
- npx -- rapier-html@1.2.42 notes.md --include draw,math # carries Draw and Math (default: what the document needs)
32
- npx -- rapier-html@1.2.42 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
33
- npx -- rapier-html@1.2.42 revised.md --compare original.md # opens showing what changed
34
- npx -- rapier-html@1.2.42 notes.md --return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT" # Send back
35
- npx -- rapier-html@1.2.42 notes.md out.html # a named output
36
- npm install rapier-html@1.2.42 # as a library: writePage, unwrap
29
+ npx -- rapier-html@1.2.43 notes.md # the editor, notes.md inside it, written beside the file
30
+ npx -- rapier-html@1.2.43 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
31
+ npx -- rapier-html@1.2.43 notes.md --include draw,math # carries Draw and Math (default: what the document needs)
32
+ npx -- rapier-html@1.2.43 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
33
+ npx -- rapier-html@1.2.43 revised.md --compare original.md # opens showing what changed
34
+ npx -- rapier-html@1.2.43 notes.md --return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT" # Send back
35
+ npx -- rapier-html@1.2.43 notes.md out.html # a named output
36
+ npm install rapier-html@1.2.43 # as a library: writePage, unwrap
37
37
  ```
38
38
 
39
39
  The first `--` is for npm, so `--help` reaches the command. It never overwrites a file.
@@ -110,11 +110,15 @@ function keyedRow(row, depth) {
110
110
  }
111
111
  return json;
112
112
  }
113
+ const plainRowArrays = new WeakSet();
113
114
  export function* canonicalRowsParts(rows, depth = 0) {
114
- const {array, keys} = fields(rows, depth, new Set());
115
- if (!array) { yield* canonicalParts(rows, depth); return; }
115
+ if (!(depth <= 64 && plainRowArrays.has(rows))) {
116
+ const {array} = fields(rows, depth, new Set());
117
+ if (!array) { yield* canonicalParts(rows, depth); return; }
118
+ if (Object.isFrozen(rows) && depth <= 64) plainRowArrays.add(rows);
119
+ }
116
120
  yield '[';
117
- for (let i = 0; i < keys.length; i++) { if (i) yield ','; yield canonicalRow(rows[keys[i]], depth + 1); }
121
+ for (let i = 0; i < rows.length; i++) { if (i) yield ','; yield canonicalRow(rows[i], depth + 1); }
118
122
  yield ']';
119
123
  }
120
124
  export function exactKeys(value, keys) {
@@ -10,10 +10,10 @@ import {minimalHistorySplice} from './inverse-source.mjs';
10
10
  import {evaluateWill} from './will.mjs';
11
11
  export {textRoot, rootAfter};
12
12
  const rootShape = value => typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value);
13
- function* checkedRecords(ledger, prepared) {
13
+ function* checkedRecords(ledger, prepared, from = null) {
14
14
  if (!prepared.ok || ledger.records.length !== prepared.records.length) throw fault('record chain differs');
15
- let root = ledger.start.root, revision = ledger.start.revision;
16
- for (let index = 0; index < prepared.records.length; index++) {
15
+ let root = from ? from.root : ledger.start.root, revision = from ? from.revision : ledger.start.revision;
16
+ for (let index = from ? from.index : 0; index < prepared.records.length; index++) {
17
17
  const record = ledger.records[index];
18
18
  if (!Array.isArray(record.splices) || !prepared.validRecord(index,
19
19
  ledger.documentAuthority, 64 * 1024 * 1024, utf8Length, !ledger.complete) ||
@@ -170,6 +170,7 @@ const expandedRows = new WeakMap();
170
170
  const sameSplices = (a, b) => a === b || a.length === b.length &&
171
171
  a.every((row, i) => row.pos === b[i].pos && row.removed === b[i].removed && row.inserted === b[i].inserted);
172
172
  const sameEffect = (a, b) => a === b || !!a && !!b && canonicalJSON(a) === canonicalJSON(b);
173
+ let recentLedgers = [];
173
174
  function* produceLedger({text, metadata, records, documentAuthority, revision, root, complete = false, earlier = records} = {}, native = false) {
174
175
  if (!_rapierValidMetadata(metadata)) throw fault('document metadata required');
175
176
  metadata = {...metadata};
@@ -191,16 +192,30 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
191
192
  yield;
192
193
  }
193
194
  const headBytes = bytes;
195
+ let base = recentLedgers.find(row => row.rows.length <= expanded.length && row.rows.every((value, at) => value === expanded[at]));
194
196
  let startSource = new HistorySource(text), startMetadata = {...metadata};
195
- for (let i = expanded.length - 1; i >= 0; i--) {
197
+ for (let i = expanded.length - 1; i >= (base ? base.rows.length : 0); i--) {
196
198
  startSource = startSource.apply(expanded[i].splices, true);
197
- startMetadata = _rapierTransformMetadata(startMetadata, expanded[i].metadata, true);
199
+ if (expanded[i].metadata) startMetadata = _rapierTransformMetadata(startMetadata, expanded[i].metadata, true);
198
200
  if (startSource === null || !startMetadata) throw fault('history does not reverse from this document');
199
201
  for (const row of expanded[i].splices) bytes += utf8Length(row.removed) - utf8Length(row.inserted);
200
202
  if (bytes > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
201
203
  yield;
202
204
  }
203
- const startText = startSource.slice();
205
+ if (base && (bytes !== base.headBytes || startSource.length !== base.headText.length || !startSource.matches(0, base.headText) ||
206
+ canonicalJSON(startMetadata) !== base.headMetadata)) {
207
+ base = null; bytes = headBytes; startSource = new HistorySource(text); startMetadata = {...metadata};
208
+ for (let i = expanded.length - 1; i >= 0; i--) {
209
+ startSource = startSource.apply(expanded[i].splices, true);
210
+ if (expanded[i].metadata) startMetadata = _rapierTransformMetadata(startMetadata, expanded[i].metadata, true);
211
+ if (startSource === null || !startMetadata) throw fault('history does not reverse from this document');
212
+ for (const row of expanded[i].splices) bytes += utf8Length(row.removed) - utf8Length(row.inserted);
213
+ if (bytes > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
214
+ yield;
215
+ }
216
+ }
217
+ if (base) { startMetadata = {...base.startMetadata}; bytes = base.startBytes; }
218
+ const startText = base ? base.startText : startSource.slice();
204
219
  const unchangedText = expanded.every(row => !row.splices.length);
205
220
  const contentRoot = headFacts.contentRoot;
206
221
  revision ??= expanded.at(-1)?.transaction.revision ?? 0;
@@ -209,19 +224,24 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
209
224
  const startRevision = expanded[0]?.transaction.baseRevision ?? revision, startRoot = expanded[0]?.beforeHash ?? root;
210
225
  if (typeof documentAuthority !== 'string' || !documentAuthority || documentAuthority.length > 256) throw fault('unsupported or invalid envelope');
211
226
  if (!nonnegative(startRevision) || !rootShape(startRoot)) throw fault('invalid start');
212
- const startFacts = unchangedText ? headFacts : yield* sourceFacts(startText, native);
227
+ const startFacts = unchangedText ? headFacts : base ? base.startFacts : yield* sourceFacts(startText, native);
213
228
  if (startRevision === 0 && startRoot !== startFacts.contentRoot) throw fault('invalid start');
214
229
  const headSHA = headFacts.sha256, startSHA = startFacts.sha256;
215
230
  const payload = {format: 'rapier-ledger/2', documentAuthority,
216
231
  start: {text: startText, revision: startRevision, root: startRoot, sha256: startSHA, metadata: startMetadata},
217
232
  records: expanded, head: {revision, root, sha256: headSHA, metadata: {...metadata}}, complete: complete === true && startRevision === 0 && expanded.every((row, i) => !i || row.transaction.baseRevision === expanded[i - 1].transaction.revision)};
218
- for (const _record of checkedRecords(payload, prepared)) yield;
233
+ const from = base && base.documentAuthority === documentAuthority && base.complete === payload.complete &&
234
+ base.startRoot === startRoot && base.startRevision === startRevision ? {index: base.rows.length, root: base.chainRoot, revision: base.chainRevision} : null;
235
+ for (const _record of checkedRecords(payload, prepared, from)) yield;
219
236
  if (expanded.some(row => Object.hasOwn(row, 'authored'))) {
220
237
  try { authoredHistory(startText, expanded); } catch (_) { throw fault('authored origin differs'); }
221
238
  }
222
239
  const replayedMetadata = prepared.replayMetadata(startMetadata);
223
240
  if (!replayedMetadata || canonicalJSON(replayedMetadata) !== canonicalJSON(metadata)) throw fault('metadata does not describe this document');
224
241
  const ledger = {...payload, sha256: yield* checksum(envelopeParts(payload), native)};
242
+ recentLedgers = [{rows: expanded, headText: text, headMetadata: canonicalJSON(metadata), headBytes, startText, startMetadata: {...startMetadata},
243
+ startFacts, startBytes: bytes, documentAuthority, complete: payload.complete, startRoot, startRevision, chainRoot: payload.head.root,
244
+ chainRevision: payload.head.revision}, ...recentLedgers.filter(row => row.rows[0] !== expanded[0] && row.documentAuthority === documentAuthority)].slice(0, 4);
225
245
  ownLedger(ledger);
226
246
  proofs.set(ledger, {shell: Array.from(envelopeParts({...ledger, start: {...ledger.start, text: ''}})).join(''), startText,
227
247
  text, metadata: {...metadata}, root, revision, prepared: _rapierPrepareHistoryRecords(ledger.records),
@@ -273,7 +293,7 @@ function historyNavigation(ledger, text, metadata, prepared, source) {
273
293
  let traversed = source, traversedMetadata = {...metadata};
274
294
  for (let i = cursor - 1; i >= 0; i--) {
275
295
  traversed = step(traversed, branch[i], false);
276
- traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata, true);
296
+ if (byId.get(branch[i]).metadata) traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata, true);
277
297
  if (traversed === null || !traversedMetadata) throw fault('undo branch does not reverse');
278
298
  }
279
299
  if (ledger.complete) {
@@ -284,7 +304,7 @@ function historyNavigation(ledger, text, metadata, prepared, source) {
284
304
  traversed = source; traversedMetadata = {...metadata};
285
305
  for (let i = cursor; i < branch.length; i++) {
286
306
  traversed = step(traversed, branch[i], true);
287
- traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata);
307
+ if (byId.get(branch[i]).metadata) traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata);
288
308
  if (traversed === null || !traversedMetadata) throw fault('redo branch does not replay');
289
309
  }
290
310
  if (ledger.complete && pictures.size) {
@@ -110,7 +110,8 @@ function _rapierRecordIndex(records, shared = false) {
110
110
  }
111
111
  return value;
112
112
  };
113
- return {byId, splices: record => resolve(record, false), metadata: record => resolve(record, true)};
113
+ const extend = more => { for (const record of more) { const id = record?.transaction?.id; if (!byId.has(id)) byId.set(id, []); byId.get(id).push(record); } };
114
+ return {byId, extend, splices: record => resolve(record, false), metadata: record => resolve(record, true)};
114
115
  }
115
116
 
116
117
  const _rapierLedgerIndexes = new WeakMap();
@@ -233,87 +234,89 @@ function _rapierPictureFinish(record, original, earlier, lookup) {
233
234
  });
234
235
  }
235
236
 
236
- function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null) {
237
- try {
238
- if (!Array.isArray(records)) return {ok: false};
239
- const nodes = new Map(), references = new Map(), pictures = new Map(), byId = new Map(), finished = new Set(), earlier = [];
240
- const update = (target, delta) => {
241
- const pending = [[target, delta]];
242
- while (pending.length) {
243
- const [id, change] = pending.pop(), node = nodes.get(id), was = node.active;
244
- node.blockers += change;
245
- if (node.blockers < 0) throw Error('effect');
246
- node.active = node.blockers === 0;
247
- if (was !== node.active) for (const ref of node.references) pending.push([ref, node.active ? 1 : -1]);
248
- }
249
- };
250
- const activity = new Map();
251
- const active = id => {
252
- if (visit) return nodes.get(id).active;
253
- const pending = [[id, 0]];
254
- while (pending.length) {
255
- const frame = pending.at(-1), target = frame[0];
256
- if (activity.has(target)) { pending.pop(); continue; }
257
- const dependents = nodes.get(target).dependents;
258
- if (frame[1] === dependents.length) { activity.set(target, true); pending.pop(); continue; }
259
- const next = dependents[frame[1]];
260
- if (!activity.has(next)) pending.push([next, 0]);
261
- else if (activity.get(next)) { activity.set(target, false); pending.pop(); }
262
- else frame[1]++;
263
- }
264
- return activity.get(id);
265
- };
266
- const lastUndo = new Map(), contexts = new Map(), order = [];
267
- const contextOf = origin => {
268
- if (!origin) return null;
269
- if (!_rapierValidAuthored(origin)) throw Error('origin');
270
- const known = new Set(), pending = origin.basis.slice();
271
- while (pending.length) {
272
- const id = pending.pop(), context = contexts.get(id);
273
- if (!context) throw Error('origin');
274
- if (known.has(id)) continue;
275
- known.add(id);
276
- if (context.basis) pending.push(...context.basis);
277
- else for (let index = 0; index < context.index; index++) known.add(order[index]);
278
- }
279
- const inactive = new Set();
280
- for (let index = order.length - 1; index >= 0; index--) {
281
- const id = order[index];
282
- if (!known.has(id) || inactive.has(id)) continue;
283
- for (const ref of references.get(id)) {
284
- if (!known.has(ref)) throw Error('origin');
285
- inactive.add(ref);
286
- }
237
+ function _rapierHistoryWalker(records, lookup, visit = null, observe = null) {
238
+ let nodes = new Map(), references = new Map(), pictures = new Map();
239
+ const byId = new Map(), finished = new Set(), earlier = [];
240
+ const update = (target, delta) => {
241
+ const pending = [[target, delta]];
242
+ while (pending.length) {
243
+ const [id, change] = pending.pop(), node = nodes.get(id), was = node.active;
244
+ node.blockers += change;
245
+ if (node.blockers < 0) throw Error('effect');
246
+ node.active = node.blockers === 0;
247
+ if (was !== node.active) for (const ref of node.references) pending.push([ref, node.active ? 1 : -1]);
248
+ }
249
+ };
250
+ const activity = new Map();
251
+ const active = id => {
252
+ if (visit) return nodes.get(id).active;
253
+ const pending = [[id, 0]];
254
+ while (pending.length) {
255
+ const frame = pending.at(-1), target = frame[0];
256
+ if (activity.has(target)) { pending.pop(); continue; }
257
+ const dependents = nodes.get(target).dependents;
258
+ if (frame[1] === dependents.length) { activity.set(target, true); pending.pop(); continue; }
259
+ const next = dependents[frame[1]];
260
+ if (!activity.has(next)) pending.push([next, 0]);
261
+ else if (activity.get(next)) { activity.set(target, false); pending.pop(); }
262
+ else frame[1]++;
263
+ }
264
+ return activity.get(id);
265
+ };
266
+ const lastUndo = new Map(), contexts = new Map(), order = [];
267
+ const contextOf = origin => {
268
+ if (!origin) return null;
269
+ if (!_rapierValidAuthored(origin)) throw Error('origin');
270
+ const known = new Set(), pending = origin.basis.slice();
271
+ while (pending.length) {
272
+ const id = pending.pop(), context = contexts.get(id);
273
+ if (!context) throw Error('origin');
274
+ if (known.has(id)) continue;
275
+ known.add(id);
276
+ if (context.basis) pending.push(...context.basis);
277
+ else for (let index = 0; index < context.index; index++) known.add(order[index]);
278
+ }
279
+ const inactive = new Set();
280
+ for (let index = order.length - 1; index >= 0; index--) {
281
+ const id = order[index];
282
+ if (!known.has(id) || inactive.has(id)) continue;
283
+ for (const ref of references.get(id)) {
284
+ if (!known.has(ref)) throw Error('origin');
285
+ inactive.add(ref);
287
286
  }
288
- return {known, inactive};
289
- };
290
- for (const record of records) {
287
+ }
288
+ return {known, inactive};
289
+ };
290
+ const walker = {observe, failed: false};
291
+ walker.add = record => {
292
+ if (walker.failed) return false;
293
+ try {
291
294
  const tx = record?.transaction;
292
- if (!tx || typeof tx.id !== 'string' || !tx.id || nodes.has(tx.id) || (tx.reverts && tx.reapplies)) return {ok: false};
295
+ if (!tx || typeof tx.id !== 'string' || !tx.id || nodes.has(tx.id) || (tx.reverts && tx.reapplies)) { walker.failed = true; return false; }
293
296
  const supplied = tx.sourceTransactionIds || (tx.sourceTransactionId ? [tx.sourceTransactionId] : []);
294
297
  if (!Array.isArray(supplied) || new Set(supplied).size !== supplied.length ||
295
- (tx.sourceTransactionIds && tx.sourceTransactionId && (supplied.length !== 1 || supplied[0] !== tx.sourceTransactionId))) return {ok: false};
298
+ (tx.sourceTransactionIds && tx.sourceTransactionId && (supplied.length !== 1 || supplied[0] !== tx.sourceTransactionId))) { walker.failed = true; return false; }
296
299
  let refs = supplied;
297
300
  if (tx.operation === 'document.finish-picture' && tx.sourceTransactionId) {
298
301
  const owner = tx.sourceTransactionId;
299
302
  if (!nodes.has(owner) || !active(owner) || finished.has(owner) ||
300
- !_rapierPictureFinish(record, byId.get(owner), earlier, lookup ||= _rapierRecordIndex(records))) return {ok: false};
303
+ !_rapierPictureFinish(record, byId.get(owner), earlier, lookup ||= _rapierRecordIndex(records))) { walker.failed = true; return false; }
301
304
  pictures.set(tx.id, owner); finished.add(owner); refs = [];
302
305
  }
303
306
  if (tx.reverts || tx.reapplies) {
304
307
  const target = tx.reverts || tx.reapplies;
305
- if (!nodes.has(target) || (supplied.length && (supplied.length !== 1 || supplied[0] !== target))) return {ok: false};
308
+ if (!nodes.has(target) || (supplied.length && (supplied.length !== 1 || supplied[0] !== target))) { walker.failed = true; return false; }
306
309
  if (tx.reverts) refs = [target];
307
310
  else {
308
311
  const context = contextOf(record.authored || tx.authored);
309
312
  const undo = (lastUndo.get(target) || []).findLast(id => context
310
313
  ? context.known.has(id) && !context.inactive.has(id) : active(id));
311
- if (!undo) return {ok: false};
314
+ if (!undo) { walker.failed = true; return false; }
312
315
  refs = [undo];
313
316
  }
314
317
  }
315
- if (refs.some(id => typeof id !== 'string' || !nodes.has(id) || pictures.has(id))) return {ok: false};
316
- if (tx.actor?.kind === 'agent' && refs.some(id => byId.get(id).transaction.actor?.kind !== 'agent')) return {ok: false};
318
+ if (refs.some(id => typeof id !== 'string' || !nodes.has(id) || pictures.has(id))) { walker.failed = true; return false; }
319
+ if (tx.actor?.kind === 'agent' && refs.some(id => byId.get(id).transaction.actor?.kind !== 'agent')) { walker.failed = true; return false; }
317
320
  nodes.set(tx.id, {active: true, blockers: 0, references: refs, dependents: []}); references.set(tx.id, refs.slice());
318
321
  byId.set(tx.id, record); earlier.push(record);
319
322
  for (const ref of refs) {
@@ -328,23 +331,45 @@ function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null
328
331
  lastUndo.get(tx.reverts).push(tx.id);
329
332
  }
330
333
  if (visit) visit(record, nodes, refs);
331
- if (observe) observe(record, refs, active);
332
- }
333
- if (!visit) for (let i = order.length - 1; i >= 0; i--) {
334
- const node = nodes.get(order[i]); node.active = node.blockers === 0;
335
- if (node.active) for (const ref of node.references) nodes.get(ref).blockers++;
334
+ if (walker.observe) walker.observe(record, refs, active);
335
+ return true;
336
+ } catch (_) { walker.failed = true; return false; }
337
+ };
338
+ walker.finish = () => {
339
+ if (walker.failed) return {ok: false};
340
+ const blockers = new Map(), effectiveActIds = [];
341
+ for (let i = order.length - 1; i >= 0; i--) {
342
+ const id = order[i], node = nodes.get(id), on = visit ? node.active : !(blockers.get(id) > 0);
343
+ if (!visit && on) for (const ref of node.references) blockers.set(ref, (blockers.get(ref) || 0) + 1);
344
+ if (on && !pictures.has(id)) effectiveActIds.push(id);
336
345
  }
337
- const effectiveActIds = [...nodes].filter(([id, node]) => node.active && !pictures.has(id)).map(([id]) => id);
338
- return {ok: true, effectiveActIds, active: new Set(effectiveActIds), references, pictures};
339
- } catch (_) { return {ok: false}; }
346
+ effectiveActIds.reverse();
347
+ const result = {ok: true, effectiveActIds, active: new Set(effectiveActIds), references, pictures};
348
+ walker.shared = true;
349
+ return result;
350
+ };
351
+ const add = walker.add;
352
+ walker.add = record => {
353
+ if (walker.shared) { references = new Map(references); pictures = new Map(pictures); walker.shared = false; }
354
+ return add(record);
355
+ };
356
+ return walker;
357
+ }
358
+
359
+ function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null) {
360
+ if (!Array.isArray(records)) return {ok: false};
361
+ const walker = _rapierHistoryWalker(records, lookup, visit, observe);
362
+ for (const record of records) if (!walker.add(record)) return {ok: false};
363
+ return walker.finish();
340
364
  }
341
365
 
342
366
  function _rapierHistoryEffects(records, visit = null) {
343
367
  return _rapierWalkHistoryEffects(records, null, visit);
344
368
  }
345
369
 
346
- function _rapierMetadataProof(lookup) {
347
- const values = {}, required = {}, assignments = {filename: [], docKind: []};
370
+ function _rapierMetadataProof(lookup, from = null) {
371
+ const values = {...from?.values}, required = {...from?.required},
372
+ assignments = {filename: [...(from?.assignments.filename || [])], docKind: [...(from?.assignments.docKind || [])]};
348
373
  const requireInitial = (key, value) => {
349
374
  if (Object.hasOwn(required, key) && required[key] !== value) throw Error('metadata_effect');
350
375
  required[key] = value;
@@ -371,7 +396,7 @@ function _rapierMetadataProof(lookup) {
371
396
  if (!_rapierValidMetadata(initial) || Object.keys(required).some(key => initial[key] !== required[key])) return null;
372
397
  return {...initial, ...values};
373
398
  };
374
- return {observe, replay};
399
+ return {observe, replay, fork: other => _rapierMetadataProof(other || lookup, {values, required, assignments})};
375
400
  }
376
401
 
377
402
  function _rapierMetadataState(initial, records, effects = _rapierHistoryEffects(records)) {
@@ -478,16 +503,79 @@ const _rapierPrepareHistoryRecords = (() => {
478
503
  frame.descriptors = descriptors; frame.fields = fields;
479
504
  frame.leaving = true; active.add(value); pending.push(frame);
480
505
  for (let at = fields.length - 1; at >= 0; at--) {
481
- const key = fields[at]; pending.push({value: descriptors[key].value, parent: frame, key, depth: frame.depth + 1});
506
+ const key = fields[at], child = descriptors[key].value;
507
+ const prior = array && child && typeof child === 'object' ? copies.get(child) : undefined;
508
+ if (prior?.immutable && !active.has(child) && frame.depth + 1 + prior.height <= 64) {
509
+ finish({parent: frame, key}, prior.copy, true, prior.height); continue;
510
+ }
511
+ pending.push({value: child, parent: frame, key, depth: frame.depth + 1});
482
512
  }
483
513
  }
484
514
  return result.value;
485
515
  };
516
+ let resumable = [];
517
+ const rowOf = (lookup, record) => {
518
+ const splices = lookup.splices(record), metadata = lookup.metadata(record);
519
+ if (splices && !Object.isFrozen(splices)) {
520
+ for (const row of splices) Object.freeze(row);
521
+ Object.freeze(splices);
522
+ }
523
+ if (metadata && !Object.isFrozen(metadata)) {
524
+ for (const pair of Object.values(metadata)) Object.freeze(pair);
525
+ Object.freeze(metadata);
526
+ }
527
+ return Object.freeze({record, splices, metadata});
528
+ };
529
+ const observe = (records, basis, byId, rows, effects, proof) => {
530
+ for (const refs of effects.references.values()) Object.freeze(refs);
531
+ const effectView = Object.freeze({ok: true, effectiveActIds: Object.freeze(effects.effectiveActIds),
532
+ active: view(effects.active), references: view(effects.references), pictures: view(effects.pictures)});
533
+ const observation = Object.freeze({ok: effects.ok, records, rows: Object.freeze(rows), effects: effectView, byId: view(byId),
534
+ replayMetadata: initial => effects.ok ? proof.replay(initial) : null,
535
+ validRecord: (index, documentAuthority, maxDocumentBytes, utf8Length, allowGap = false) => {
536
+ if (!effects.ok || !Number.isSafeInteger(index) || index < 0 || index >= records.length) return false;
537
+ const row = rows[index], tx = row.record.transaction;
538
+ return _rapierCheckLedgerRecord(row.record, records[index - 1] || null, documentAuthority, maxDocumentBytes, utf8Length, allowGap,
539
+ {...row, duplicate: false, semantic: !!(tx.reverts || tx.reapplies || tx.sourceTransactionId || tx.sourceTransactionIds?.length)});
540
+ }});
541
+ prepared = new WeakMap([[records, {basis, observation}]]);
542
+ return observation;
543
+ };
544
+ const authorityOf = state => state.records[0]?.transaction?.documentAuthority;
545
+ const keep = state => { resumable = [state, ...resumable.filter(row => row !== state && row.records[0] !== state.records[0] &&
546
+ authorityOf(row) === authorityOf(state))].slice(0, 4); };
547
+ const extend = records => {
548
+ const base = resumable.find(row => row.records.length < records.length && row.records[0] === records[0] &&
549
+ row.records.every((record, at) => record === records[at]));
550
+ if (!base) return null;
551
+ resumable = resumable.filter(row => row !== base);
552
+ const more = records.slice(base.records.length), byId = new Map(base.byId);
553
+ for (const record of more) {
554
+ const id = record?.transaction?.id;
555
+ if (typeof id !== 'string' || !id || byId.has(id)) return null;
556
+ byId.set(id, record);
557
+ }
558
+ const {lookup, walker} = base;
559
+ lookup.extend(more);
560
+ const rows = base.rows.concat(more.map(record => rowOf(lookup, record)));
561
+ const proof = base.proof.fork(lookup);
562
+ walker.observe = proof.observe;
563
+ for (const record of more) if (!walker.add(record)) return null;
564
+ const effects = walker.finish();
565
+ if (!effects.ok) return null;
566
+ if (rows.every(row => row.splices && row.metadata !== undefined)) keep({records, lookup, walker, proof, byId, rows});
567
+ return observe(records, records, byId, rows, effects, proof);
568
+ };
486
569
  return (input, earlier = input) => {
487
570
  try {
488
571
  if (!Array.isArray(input) || !Array.isArray(earlier)) return {ok: false};
489
572
  const records = capture(input), basis = input === earlier ? records : capture(earlier), previous = prepared.get(records);
490
573
  if (previous?.basis === basis) return previous.observation;
574
+ if (basis === records) {
575
+ let grown = null;
576
+ try { grown = extend(records); } catch (_) { grown = null; }
577
+ if (grown) return grown;
578
+ }
491
579
  const byId = new Map();
492
580
  for (const record of records) {
493
581
  const id = record?.transaction?.id;
@@ -495,32 +583,14 @@ const _rapierPrepareHistoryRecords = (() => {
495
583
  byId.set(id, record);
496
584
  }
497
585
  const lookup = _rapierRecordIndex(basis), proof = _rapierMetadataProof(lookup);
498
- const rows = records.map(record => {
499
- const splices = lookup.splices(record), metadata = lookup.metadata(record);
500
- if (splices && !Object.isFrozen(splices)) {
501
- for (const row of splices) Object.freeze(row);
502
- Object.freeze(splices);
503
- }
504
- if (metadata && !Object.isFrozen(metadata)) {
505
- for (const pair of Object.values(metadata)) Object.freeze(pair);
506
- Object.freeze(metadata);
507
- }
508
- return Object.freeze({record, splices, metadata});
509
- });
510
- const effects = _rapierWalkHistoryEffects(records, lookup, null, proof.observe);
586
+ const rows = records.map(record => rowOf(lookup, record));
587
+ const walker = _rapierHistoryWalker(records, lookup, null, proof.observe);
588
+ for (const record of records) if (!walker.add(record)) return {ok: false};
589
+ const effects = walker.finish();
511
590
  if (!effects.ok) return {ok: false};
512
- for (const refs of effects.references.values()) Object.freeze(refs);
513
- const effectView = Object.freeze({ok: true, effectiveActIds: Object.freeze(effects.effectiveActIds),
514
- active: view(effects.active), references: view(effects.references), pictures: view(effects.pictures)});
515
- const observation = Object.freeze({ok: effects.ok, records, rows: Object.freeze(rows), effects: effectView, byId: view(byId),
516
- replayMetadata: initial => effects.ok ? proof.replay(initial) : null,
517
- validRecord: (index, documentAuthority, maxDocumentBytes, utf8Length, allowGap = false) => {
518
- if (!effects.ok || !Number.isSafeInteger(index) || index < 0 || index >= records.length) return false;
519
- const row = rows[index], tx = row.record.transaction;
520
- return _rapierCheckLedgerRecord(row.record, records[index - 1] || null, documentAuthority, maxDocumentBytes, utf8Length, allowGap,
521
- {...row, duplicate: false, semantic: !!(tx.reverts || tx.reapplies || tx.sourceTransactionId || tx.sourceTransactionIds?.length)});
522
- }});
523
- prepared = new WeakMap([[records, {basis, observation}]]); return observation;
591
+ if (basis === records && rows.every(row => row.splices && row.metadata !== undefined))
592
+ keep({records, lookup, walker, proof, byId, rows});
593
+ return observe(records, basis, byId, rows, effects, proof);
524
594
  } catch (_) { return {ok: false}; }
525
595
  };
526
596
  })();
@@ -100,7 +100,7 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
100
100
  return end < body.length ? sourceOffset(end + 1) : undefined;
101
101
  }});
102
102
  let lines = 0;
103
- for (let at = 0; at < body.length; at++) if (body.charCodeAt(at) === 10) lines++;
103
+ for (let at = body.indexOf('\n'); at !== -1; at = body.indexOf('\n', at + 1)) lines++;
104
104
  if (lines <= BLOCK_WINDOW) {
105
105
  state = new parser.block.State(body, parser, env, tokens);
106
106
  state.rapierSourceSlice = (from, to) => source.slice(sourceOffset(from), sourceOffset(to));
@@ -179,8 +179,7 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
179
179
  }
180
180
  }
181
181
 
182
- const PERCENT_PAYLOAD = Array.from({length: 128}, (_, code) => code >= 48 && code <= 57 || code >= 65 && code <= 90 ||
183
- code >= 97 && code <= 122 || '-._~:/?@+$,;='.includes(String.fromCharCode(code)));
182
+ const INVALID_PERCENT_PAYLOAD = /[^A-Za-z0-9._~:\/?@+$,;=%-]/;
184
183
  const isHexUnit = code => code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102;
185
184
  let lastScan = null;
186
185
  export function dataImage(url) {
@@ -197,13 +196,11 @@ function scanDataImage(url) {
197
196
  ...(prepared.encoding ? {encoding: prepared.encoding} : {})};
198
197
  if (/^data:image\/svg\+xml,/i.test(url)) {
199
198
  const payloadStart = 19;
200
- let byteLength = 0;
201
- for (let at = payloadStart; at < url.length; at++, byteLength++) {
202
- const code = url.charCodeAt(at);
203
- if (code === 37) {
204
- if (!isHexUnit(url.charCodeAt(at + 1)) || !isHexUnit(url.charCodeAt(at + 2))) return null;
205
- at += 2;
206
- } else if (!(code < 128 && PERCENT_PAYLOAD[code])) return null;
199
+ if (INVALID_PERCENT_PAYLOAD.test(url)) return null;
200
+ let byteLength = url.length - payloadStart;
201
+ for (let at = url.indexOf('%', payloadStart); at !== -1; at = url.indexOf('%', at + 3)) {
202
+ if (!isHexUnit(url.charCodeAt(at + 1)) || !isHexUnit(url.charCodeAt(at + 2))) return null;
203
+ byteLength -= 2;
207
204
  }
208
205
  return byteLength && byteLength <= IMAGE_LIMITS.bytes
209
206
  ? {codec: 'image/svg+xml', byteLength, payloadStart, encoding: 'percent'} : null;