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.
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // Paired source placement, shared by live editing and canonical-history proof.
3
3
  import {_rapierTransformSplices, _rapierValidAuthored, _rapierHistoryEffects} from './journal-records.mjs';
4
+ import {projectInverseSource} from './inverse-source.mjs';
4
5
  import {sha256} from './hash.mjs';
5
6
  import {canonicalJSON} from './data.mjs';
6
7
 
@@ -169,7 +170,38 @@ function authoredReplay(base, records, append = null) {
169
170
  const origins = new Map(), byId = new Map(), originalOrder = [], cache = new Map(), frontier = new Set();
170
171
  const effects = _rapierHistoryEffects(append ? [...records, append] : records);
171
172
  if (!effects.ok) throw new RangeError('canonical_origin_invalid');
172
- let source = base;
173
+ const referenced = new Set();
174
+ let sharedInverses = false;
175
+ for (const refs of effects.references.values()) for (const id of refs) {
176
+ if (referenced.has(id)) sharedInverses = true;
177
+ referenced.add(id);
178
+ }
179
+ const projectInverse = (text, placed, id) => {
180
+ const refs = effects.references.get(id), before = _rapierHistoryEffects(placed);
181
+ const after = _rapierHistoryEffects([...placed, {transaction: {id, sourceTransactionIds: refs}}]);
182
+ if (!before.ok || !after.ok) throw new RangeError('canonical_origin_invalid');
183
+ const result = projectInverseSource({source: text, initialSource: base,
184
+ rows: placed.map(record => ({record, splices: record.splices})), effects: before}, new Set(refs), after);
185
+ if (!result.ok) throw new RangeError('canonical_origin_invalid');
186
+ return result;
187
+ };
188
+ // Only immutable text digests are reused, within this one replay. Every
189
+ // authored-source comparison and physical placement proof still runs.
190
+ const textHashes = new Map();
191
+ const hashText = text => {
192
+ if (typeof text !== 'string') return sha256(text);
193
+ let digest = textHashes.get(text);
194
+ if (digest === undefined) { digest = sha256(text); textHashes.set(text, digest); }
195
+ return digest;
196
+ };
197
+ const same = (a, b) => canonicalJSON(a) === canonicalJSON(b);
198
+ // Only owner-created ordered views of already admitted string IDs use this.
199
+ const sameIds = (a, b) => {
200
+ if (a.length !== b.length) return false;
201
+ for (let index = 0; index < a.length; index++) if (a[index] !== b[index]) return false;
202
+ return true;
203
+ };
204
+ let source = base, linearPrefix = 0;
173
205
  for (const record of append ? [...records, append] : records) {
174
206
  const id = record.transaction.id;
175
207
  if (byId.has(id)) throw new RangeError('canonical_origin_invalid');
@@ -189,6 +221,11 @@ function authoredReplay(base, records, append = null) {
189
221
  })()))) throw new RangeError('canonical_origin_invalid');
190
222
  const refs = effects.references.get(id) || [];
191
223
  if (refs.some(id => !seen.has(id))) throw new RangeError('canonical_origin_invalid');
224
+ // An explicit origin can prove the same linear placement as an implicit one.
225
+ // Preserve the origin, but do not reconstruct every growing prefix to prove it again.
226
+ if (linearPrefix === originalOrder.length && same(origin.basis, [...frontier].sort()) &&
227
+ same(origin.splices, record.splices) && (!Object.hasOwn(record, 'authored') || origin.source === sha256(source)))
228
+ linearPrefix++;
192
229
  for (const prior of seen) frontier.delete(prior); frontier.add(id);
193
230
  origins.set(id, origin); byId.set(id, record); originalOrder.push(id);
194
231
  if (record !== append) {
@@ -196,7 +233,6 @@ function authoredReplay(base, records, append = null) {
196
233
  if (source === null) throw new RangeError('canonical_origin_invalid');
197
234
  }
198
235
  }
199
- const same = (a, b) => canonicalJSON(a) === canonicalJSON(b);
200
236
  const ordered = (ids, selected = null) => {
201
237
  const order = [], known = new Set(ids), waiting = new Map(), dependents = new Map(), ready = [];
202
238
  const rank = id => selected ? Number(selected.has(id)) : Number(origins.get(id).splices.some(row => row.removed.length));
@@ -229,17 +265,60 @@ function authoredReplay(base, records, append = null) {
229
265
  closure.add(prior); pending.push(...origins.get(prior).basis); }
230
266
  return closure;
231
267
  };
232
- const canonicalOrder = ordered(originalOrder), firstExplicit = (append ? [...records, append] : records).findIndex(record => Object.hasOwn(record, 'authored'));
233
- const implicitPrefix = firstExplicit < 0 ? records.length : firstExplicit;
234
- const materialize = order => {
235
- const key = canonicalJSON(order);
268
+ const canonicalOrder = ordered(originalOrder);
269
+ // Only the three immediate fresh-spread call sites use this entry. Nothing
270
+ // retains allocation trust after an array has crossed an observable method.
271
+ const literalKey = order => {
272
+ // An input iterator can supply arbitrary values. Preserve the whole-array
273
+ // depth/refusal path before any per-string encoding in that case.
274
+ for (let index = 0; index < order.length; index++)
275
+ if (typeof order[index] !== 'string') return canonicalJSON(order);
276
+ // Keep each canonical primitive encoder call and its serializer behavior.
277
+ return '[' + Array.from({length: order.length}, (_, index) => canonicalJSON(order[index])).join(',') + ']';
278
+ };
279
+ const materialize = (order, rawSubmitted = false) => materializeKey(order, canonicalJSON(order), rawSubmitted);
280
+ const materializeLiteral = order => materializeKey(order, literalKey(order), false);
281
+ const materializeKey = (order, key, rawSubmitted) => {
236
282
  if (cache.has(key)) return cache.get(key);
237
- const placed = [], ids = new Set(), sourceIndices = new Map(); let text = base;
283
+ let prefixEnds = null;
284
+ const prefixKey = length => {
285
+ // The original submitted array keeps its existing slice/species/data gate.
286
+ // Only this module's own order allocations use the validated key string.
287
+ if (rawSubmitted) return canonicalJSON(order.slice(0, length));
288
+ if (!prefixEnds) {
289
+ prefixEnds = [1];
290
+ let quoted = false, depth = 0;
291
+ // Scan canonical JSON only: an escaped code unit cannot end a string,
292
+ // and commas inside strings or nested values cannot split this array.
293
+ for (let at = 1; at < key.length - 1; at++) {
294
+ const code = key.charCodeAt(at);
295
+ if (quoted) {
296
+ if (code === 92) at++;
297
+ else if (code === 34) quoted = false;
298
+ } else if (code === 34) quoted = true;
299
+ else if (code === 91 || code === 123) depth++;
300
+ else if (code === 93 || code === 125) depth--;
301
+ else if (code === 44 && depth === 0) prefixEnds.push(at);
302
+ }
303
+ if (key.length > 2) prefixEnds.push(key.length - 1);
304
+ }
305
+ return length === prefixEnds.length - 1 ? key : key.slice(0, prefixEnds[length]) + ']';
306
+ };
307
+ let placed = [], ids = new Set(), sourceIndices = new Map(), text = base;
238
308
  if (!order.length) { const empty = {text, records: [], sourceIndices}; cache.set(key, empty); return empty; }
239
- // An implicit linear prefix already is its authored view. Replay it once;
309
+ // A proved linear prefix already is its authored view. Replay it once;
240
310
  // reconstructing every growing prefix adds no evidence.
241
311
  let prefix = 0;
242
- while (prefix < order.length && prefix < implicitPrefix && order[prefix] === originalOrder[prefix]) {
312
+ // Resume only an exact ordered prefix already proved in this replay. Copy its
313
+ // mutable containers before extending it; transported rows retain their placement.
314
+ for (let length = order.length - 1; length > 0; length--) {
315
+ const prior = cache.get(prefixKey(length));
316
+ if (!prior) continue;
317
+ placed = prior.records.slice(); ids = new Set(order.slice(0, length));
318
+ sourceIndices = new Map(prior.sourceIndices); text = prior.text; prefix = length;
319
+ break;
320
+ }
321
+ while (prefix < order.length && prefix < linearPrefix && order[prefix] === originalOrder[prefix]) {
243
322
  const record = byId.get(order[prefix]); text = _rapierTransformSplices(text, record.splices);
244
323
  if (text === null) throw new RangeError('canonical_origin_invalid');
245
324
  placed.push(record); ids.add(order[prefix++]);
@@ -249,28 +328,53 @@ function authoredReplay(base, records, append = null) {
249
328
  const original = byId.get(id), origin = origins.get(id);
250
329
  if (!original || ids.has(id) || origin.basis.some(prior => !ids.has(prior)))
251
330
  throw new RangeError('canonical_origin_invalid');
252
- const closure = closureOf(origin.basis), basisOrder = canonicalOrder.filter(prior => closure.has(prior)), basis = materialize(basisOrder);
253
- if (sha256(basis.text) !== origin.source) throw new RangeError('canonical_origin_invalid');
331
+ const closure = closureOf(origin.basis);
332
+ if (closure.size === ids.size && [...closure].every(prior => ids.has(prior))) {
333
+ // The current view already is the complete authored context. There are no
334
+ // independent rows to cross, even when earlier acts were concurrent.
335
+ if (hashText(text) !== origin.source) throw new RangeError('canonical_origin_invalid');
336
+ const next = _rapierTransformSplices(text, origin.splices);
337
+ if (next === null) throw new RangeError('canonical_origin_invalid');
338
+ placed.push({...original, splices: origin.splices}); text = next; ids.add(id);
339
+ if (original.derivedCommentIndex != null) sourceIndices.set(id, origin.splices.map((_, index) => index));
340
+ cache.set(prefixKey(placed.length), {text, records: placed.slice(), sourceIndices: new Map(sourceIndices)});
341
+ continue;
342
+ }
343
+ const basisOrder = canonicalOrder.filter(prior => closure.has(prior)), basis = materialize(basisOrder);
344
+ if (hashText(basis.text) !== origin.source) throw new RangeError('canonical_origin_invalid');
254
345
  // Reconstruct causal context before crossing it. Physical gaps may already
255
346
  // have collapsed under a deletion; they cannot choose concurrent ordering.
256
- const contextOrder = canonicalOrder.filter(prior => ids.has(prior)), context = same(contextOrder, [...ids]) ? {text, records: placed} : materialize(contextOrder);
347
+ const contextOrder = canonicalOrder.filter(prior => ids.has(prior)), context = sameIds(contextOrder, [...ids]) ? {text, records: placed} : materialize(contextOrder);
257
348
  if (context.text !== text) throw new RangeError('canonical_origin_invalid');
258
349
  const alignedOrder = [...basisOrder, ...contextOrder.filter(prior => !closure.has(prior))];
259
- const aligned = same(alignedOrder, [...ids]) ? {text, records: placed} : materialize(alignedOrder);
350
+ const aligned = sameIds(alignedOrder, [...ids]) ? {text, records: placed} : materializeLiteral(alignedOrder);
260
351
  if (aligned.text !== text) throw new RangeError('canonical_origin_invalid');
261
352
  const crossed = aligned.records.slice(basisOrder.length);
262
- const moved = transposeRecord(basis.text, origin.splices, id, crossed);
353
+ let moved;
354
+ if (sharedInverses && effects.references.get(id)?.length) {
355
+ // Each inverse retains its own targets. First prove the exact intention
356
+ // in its causal source, then place that choice in the accepted effect
357
+ // graph. Another inverse may already suppress the same original bytes.
358
+ const authored = projectInverse(basis.text, basis.records, id);
359
+ if (!same(origin.splices, authored.splices) ||
360
+ _rapierTransformSplices(basis.text, origin.splices) !== authored.after)
361
+ throw new RangeError('canonical_origin_invalid');
362
+ const accepted = projectInverse(text, aligned.records, id);
363
+ moved = {text: accepted.after, splices: accepted.splices};
364
+ } else moved = transposeRecord(basis.text, origin.splices, id, crossed);
263
365
  if (_rapierTransformSplices(text, moved.splices) !== moved.text) throw new RangeError('canonical_origin_invalid');
264
366
  placed.push({...original, splices: moved.splices}); text = moved.text; ids.add(id);
265
367
  if (original.derivedCommentIndex != null) sourceIndices.set(id, moved.sourceIndices);
266
- cache.set(canonicalJSON(order.slice(0, placed.length)), {text, records: placed.slice(), sourceIndices: new Map(sourceIndices)});
368
+ cache.set(prefixKey(placed.length), {text, records: placed.slice(), sourceIndices: new Map(sourceIndices)});
267
369
  }
268
370
  const value = {text, records: placed, sourceIndices}; cache.set(key, value); return value;
269
371
  };
270
372
  const physicalOrder = append ? originalOrder.slice(0, -1) : originalOrder, physical = materialize(physicalOrder);
271
373
  if (physical.text !== source || physical.records.some((row, index) => !same(row.splices, records[index].splices)))
272
374
  throw new RangeError('canonical_origin_invalid');
273
- return {source, origins, originalOrder, materialize, ordered};
375
+ // Raw submission is selected here by a private call site, never an input flag.
376
+ return {source, origins, originalOrder, materialize, materializeLiteral,
377
+ materializeSubmitted: order => materialize(order, true), ordered};
274
378
  }
275
379
 
276
380
  export function authoredHistory(base, records, selected = null) {
@@ -306,13 +410,13 @@ export function authoredSourceReader(base, records) {
306
410
  export function transposeAuthored(base, records, incoming, submitted) {
307
411
  if (!incoming.authored || !Array.isArray(submitted)) throw new RangeError('canonical_origin_invalid');
308
412
  const replay = authoredReplay(base, records, incoming), id = incoming.transaction.id;
309
- const authored = replay.materialize([...submitted, id]);
413
+ const authored = replay.materializeLiteral([...submitted, id]);
310
414
  if (canonicalJSON(authored.records.at(-1).splices) !== canonicalJSON(incoming.splices))
311
415
  throw new RangeError('canonical_origin_invalid');
312
416
  const accepted = replay.materialize(replay.originalOrder), known = new Set([...submitted, id]);
313
- const reciprocal = replay.materialize([...submitted, id, ...replay.ordered(replay.originalOrder).filter(prior => !known.has(prior))]);
417
+ const reciprocal = replay.materializeLiteral([...submitted, id, ...replay.ordered(replay.originalOrder).filter(prior => !known.has(prior))]);
314
418
  if (reciprocal.text !== accepted.text) throw new RangeError('canonical_origin_invalid');
315
419
  return {text: accepted.text, splices: accepted.records.at(-1).splices,
316
- submittedBefore: replay.materialize(submitted).text, submitted: authored.text,
420
+ submittedBefore: replay.materializeSubmitted(submitted).text, submitted: authored.text,
317
421
  against: reciprocal.records.slice(submitted.length + 1)};
318
422
  }