@msb235/blits 0.11.0 → 0.12.1

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.
Files changed (78) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/arcs.d.ts +40 -0
  3. package/dist/arcs.js +241 -0
  4. package/dist/chain.d.ts +6 -1
  5. package/dist/chain.js +33 -13
  6. package/dist/channels.d.ts +2 -0
  7. package/dist/channels.js +12 -1
  8. package/dist/clone.js +16 -4
  9. package/dist/cue.js +11 -2
  10. package/dist/departed.d.ts +5 -1
  11. package/dist/departed.js +71 -27
  12. package/dist/easing.d.ts +12 -1
  13. package/dist/easing.js +7 -1
  14. package/dist/everyone.d.ts +14 -1
  15. package/dist/everyone.js +27 -2
  16. package/dist/fade.js +3 -3
  17. package/dist/fill.js +16 -9
  18. package/dist/fit.d.ts +8 -1
  19. package/dist/fit.js +7 -3
  20. package/dist/fold.js +69 -13
  21. package/dist/gather.js +38 -7
  22. package/dist/handle.d.ts +2 -0
  23. package/dist/handle.js +8 -0
  24. package/dist/held.d.ts +5 -2
  25. package/dist/held.js +6 -3
  26. package/dist/history.d.ts +19 -2
  27. package/dist/history.js +149 -31
  28. package/dist/hosts.js +27 -11
  29. package/dist/index.d.ts +4 -1
  30. package/dist/index.js +2 -0
  31. package/dist/lanes.d.ts +23 -7
  32. package/dist/lanes.js +36 -2
  33. package/dist/lean.js +2 -2
  34. package/dist/marks.js +3 -0
  35. package/dist/mixer.d.ts +16 -1
  36. package/dist/mixer.js +60 -30
  37. package/dist/move.js +7 -2
  38. package/dist/numbers.d.ts +5 -1
  39. package/dist/numbers.js +4 -0
  40. package/dist/owner.js +6 -1
  41. package/dist/place.d.ts +8 -0
  42. package/dist/place.js +16 -8
  43. package/dist/project.js +3 -3
  44. package/dist/qualify.d.ts +10 -0
  45. package/dist/qualify.js +52 -1
  46. package/dist/reading.d.ts +2 -1
  47. package/dist/reading.js +2 -1
  48. package/dist/record.d.ts +6 -0
  49. package/dist/record.js +1 -0
  50. package/dist/restored.d.ts +67 -18
  51. package/dist/restored.js +159 -49
  52. package/dist/revive.d.ts +1 -0
  53. package/dist/revive.js +1 -0
  54. package/dist/roster.js +2 -1
  55. package/dist/seek.js +72 -18
  56. package/dist/shared.d.ts +1 -1
  57. package/dist/shared.js +1 -1
  58. package/dist/spans.d.ts +5 -0
  59. package/dist/spans.js +75 -5
  60. package/dist/store.d.ts +2 -0
  61. package/dist/store.js +4 -0
  62. package/dist/touch.js +3 -2
  63. package/dist/transport.js +6 -1
  64. package/dist/types/arc.d.ts +55 -0
  65. package/dist/types/arc.js +1 -0
  66. package/dist/types/mix.d.ts +24 -0
  67. package/dist/types/voice.d.ts +10 -1
  68. package/dist/types/watch.d.ts +126 -0
  69. package/dist/types/watch.js +1 -0
  70. package/dist/types.d.ts +2 -0
  71. package/dist/voice.d.ts +37 -6
  72. package/dist/voice.js +17 -0
  73. package/dist/watch.d.ts +28 -0
  74. package/dist/watch.js +467 -0
  75. package/dist/weigh.d.ts +13 -2
  76. package/dist/weigh.js +10 -3
  77. package/docs/schema.html +39 -3
  78. package/package.json +5 -4
package/dist/mixer.js CHANGED
@@ -1,6 +1,7 @@
1
+ import { cueArc } from './arcs.js';
1
2
  import { blended, blendOf, laneRead, shareKept } from './blend.js';
2
- import { aims, chain, linkable, relink, unlink } from './chain.js';
3
- import { lerpInto } from './channels.js';
3
+ import { aims, chain, headOf, linkable, relink, unlink } from './chain.js';
4
+ import { lerpInto, scalarOp } from './channels.js';
4
5
  import { cue } from './cue.js';
5
6
  import { DepartedIndex } from './departed.js';
6
7
  import { beginFade, leaveVoice, parting } from './fade.js';
@@ -27,6 +28,7 @@ import { record, unplayable } from './tape.js';
27
28
  import { touch } from './touch.js';
28
29
  import { announce, Transport } from './transport.js';
29
30
  import { none } from './voice.js';
31
+ import { gave, historyFor, register, unregister } from './watch.js';
30
32
  import { base, capped, fadeOf, horizonFor, ownedBy, ownerBase, prime, weigh, weighOwned, } from './weigh.js';
31
33
  /**
32
34
  * What a mix takes where its options say nothing. Not exported from the package: the suite turns
@@ -142,6 +144,8 @@ export class Mixer {
142
144
  */
143
145
  chains = new Store();
144
146
  version = 0;
147
+ /** What `prime` reads its clock, voice time and pass from: see `Primed`. */
148
+ primed = new Float64Array(3 /* Primed.SIZE */);
145
149
  steps = new Steps();
146
150
  /** Per subject, the voices whose `subjects` name it, in voice order. */
147
151
  named = new Store();
@@ -205,6 +209,8 @@ export class Mixer {
205
209
  bounded;
206
210
  /** By slot, a stock array channel's in-place `lerp`. */
207
211
  lerpsInto;
212
+ /** By slot, a stock scalar channel's arithmetic, which a fold applies without calling the channel. */
213
+ scalars;
208
214
  /** What a fold with a locus in play gathers into, one per depth of folds under way. */
209
215
  locusScratch = [];
210
216
  locusDepth = 0;
@@ -215,6 +221,8 @@ export class Mixer {
215
221
  bookers = null;
216
222
  /** The fit a span being cued takes, which `cue` hands its voice. */
217
223
  fitting = null;
224
+ /** What an arc being cued keeps, which `cue` hands its voice. */
225
+ arcing = null;
218
226
  /** Every owner still in the mix, null until one is cued. */
219
227
  owners = null;
220
228
  /** With a history store, the key each subject is paged under; null without one. */
@@ -231,6 +239,7 @@ export class Mixer {
231
239
  this.channels = this.names.map((k) => kit[k]);
232
240
  this.bounded = this.channels.flatMap((c, i) => (c.bounds ? [i] : []));
233
241
  this.lerpsInto = this.channels.map((c) => lerpInto(c));
242
+ this.scalars = this.channels.map((c) => scalarOp(c));
234
243
  this.names.forEach((k, i) => {
235
244
  this.slotOf.set(k, i);
236
245
  });
@@ -372,6 +381,9 @@ export class Mixer {
372
381
  this.fitting = null;
373
382
  }
374
383
  }
384
+ arc(spec) {
385
+ return cueArc(this, spec);
386
+ }
375
387
  sync(timestamp) {
376
388
  this.own('sync').sync(timestamp);
377
389
  }
@@ -496,6 +508,9 @@ export class Mixer {
496
508
  const motion = voice.motion;
497
509
  return motion !== undefined && voice.elapsedAt(now) >= voice.latest && motion.landed();
498
510
  }
511
+ close() {
512
+ unregister(this);
513
+ }
499
514
  mute(opts) {
500
515
  for (const voice of this.cued) {
501
516
  voice.written = true;
@@ -505,12 +520,14 @@ export class Mixer {
505
520
  }
506
521
  drop(subject) {
507
522
  this.frame = nextFrame();
508
- const head = this.chains.get(subject);
523
+ const head = headOf(this, subject);
509
524
  // Nothing that kept this head, such as `pull`'s remembered list, may take it as current again.
510
525
  if (head !== undefined)
511
526
  unlink(this, head);
512
- if (head !== undefined && head.slot >= 0 && this.lanes !== null)
513
- this.lanes.release(head.slot);
527
+ // A number a seek carried over is released even where its chain is not linked since.
528
+ const slot = this.chains.get(subject)?.slot ?? -1;
529
+ if (slot >= 0 && this.lanes !== null)
530
+ this.lanes.release(slot);
514
531
  // A read ahead's copy reads the live mix's poses, and so hides one where the mix deletes it.
515
532
  if (this.projecting && !(this.pose instanceof Masked))
516
533
  this.pose = new Masked(this.pose);
@@ -521,40 +538,24 @@ export class Mixer {
521
538
  // A record of it is only in a voice over every subject, one naming it, or one gone; a ramp out
522
539
  // of a voice that does not name it is in `parters`, and a patch's own state in `motions`.
523
540
  const kept = releasing(this);
524
- // A motion patch's state is the live mix's alone: a read ahead cannot start a subject afresh in it.
525
- const release = (motion) => {
526
- if (!this.projecting)
527
- motion.release(subject, ...kept);
528
- else if (motion.holds(subject))
529
- unplayable('a drop of a subject a motion patch holds');
530
- };
531
- const forget = (voice) => {
532
- leaveVoice(this, voice, subject);
533
- voice.subjects.delete(subject);
534
- if (voice.motion !== undefined)
535
- release(voice.motion);
536
- voice.parts?.delete(subject);
537
- voice.parted?.delete(subject);
538
- voice.blend?.of.reads.delete(subject);
539
- };
540
541
  for (const voice of this.general)
541
- forget(voice);
542
+ this.forgetIn(voice, subject, kept);
542
543
  const named = this.named.get(subject);
543
544
  if (named !== undefined)
544
545
  for (const voice of [...named])
545
- forget(voice);
546
+ this.forgetIn(voice, subject, kept);
546
547
  const gone = goneIndex(this);
547
548
  for (const voice of gone.general)
548
- forget(voice);
549
+ this.forgetIn(voice, subject, kept);
549
550
  const goneNaming = gone.named.get(subject);
550
551
  if (goneNaming !== undefined)
551
552
  for (const voice of goneNaming)
552
- forget(voice);
553
+ this.forgetIn(voice, subject, kept);
553
554
  if (this.owners !== null)
554
555
  for (const voice of this.owners)
555
- forget(voice);
556
+ this.forgetIn(voice, subject, kept);
556
557
  for (const voice of this.parters) {
557
- forget(voice);
558
+ this.forgetIn(voice, subject, kept);
558
559
  if (voice.parts?.size === 0)
559
560
  voice.parts = null;
560
561
  if (voice.parted?.size === 0)
@@ -565,10 +566,30 @@ export class Mixer {
565
566
  const strays = this.strays.get(subject);
566
567
  if (strays !== undefined)
567
568
  for (const motion of strays)
568
- release(motion);
569
+ this.releaseIn(motion, subject, kept);
569
570
  this.strays.delete(subject);
570
571
  for (const motion of this.strayAll)
571
- release(motion);
572
+ this.releaseIn(motion, subject, kept);
573
+ this.recordDrop(subject);
574
+ }
575
+ /** Lets `voice` go of a dropped subject. Methods rather than closures, so a drop makes neither. */
576
+ forgetIn(voice, subject, kept) {
577
+ leaveVoice(this, voice, subject);
578
+ voice.subjects.delete(subject);
579
+ if (voice.motion !== undefined)
580
+ this.releaseIn(voice.motion, subject, kept);
581
+ voice.parts?.delete(subject);
582
+ voice.parted?.delete(subject);
583
+ voice.blend?.of.reads.delete(subject);
584
+ }
585
+ // A motion patch's state is the live mix's alone: a read ahead cannot start a subject afresh in it.
586
+ releaseIn(motion, subject, kept) {
587
+ if (!this.projecting)
588
+ motion.release(subject, kept[0], kept[1], kept[2]);
589
+ else if (motion.holds(subject))
590
+ unplayable('a drop of a subject a motion patch holds');
591
+ }
592
+ recordDrop(subject) {
572
593
  record(this, 'drop', (stage) => stage.mix(this)?.drop(subject));
573
594
  }
574
595
  touch(subject) {
@@ -680,6 +701,15 @@ function named(transport, name) {
680
701
  if (taken.some((m) => m.name === name))
681
702
  throw new Error(`blits: a mix named ${name} is already on this transport, or a seek could bring it back`);
682
703
  }
704
+ /** Makes a mix the host asked for, with the registry's history where it names none, and lists it there. */
705
+ export function made(kit, opts, lanes) {
706
+ const { history, added } = historyFor(opts);
707
+ const m = new Mixer(kit, added ? { ...opts, history } : opts, lanes);
708
+ if (added)
709
+ gave(m.transport);
710
+ register(m);
711
+ return m;
712
+ }
683
713
  /**
684
714
  * The one that ships: every form on the CPU, probed per subject on demand.
685
715
  *
@@ -689,7 +719,7 @@ export const mixer = {
689
719
  name: 'mixer',
690
720
  runs: new Set(['fn', 'keys', 'motion']),
691
721
  create(kit, opts) {
692
- return new Mixer(kit, opts, Lanes);
722
+ return made(kit, opts, Lanes);
693
723
  },
694
724
  };
695
725
  /**
package/dist/move.js CHANGED
@@ -1,7 +1,8 @@
1
+ import { settleArcs } from './arcs.js';
1
2
  import { changed } from './chain.js';
2
3
  import { elapsedWith } from './clock.js';
3
4
  import { byId, popDue, schedule } from './due.js';
4
- import { detour } from './everyone.js';
5
+ import { detour, outgrow } from './everyone.js';
5
6
  import { beginFade, part, retire } from './fade.js';
6
7
  import { expireGone, keepGone } from './gone.js';
7
8
  import { begin } from './held.js';
@@ -129,8 +130,12 @@ function moveTo(mix, now) {
129
130
  if (!mix.projecting)
130
131
  schedule(mix, voice);
131
132
  }
132
- if (mix.owners !== null)
133
+ if (mix.owners !== null) {
133
134
  lapse(mix, now);
135
+ settleArcs(mix, now);
136
+ }
137
+ if (mix.sharers.length > 0 && !mix.projecting)
138
+ outgrow(mix);
134
139
  mix.stirred = false;
135
140
  if (mix.retired.length > 0)
136
141
  prune(mix);
package/dist/numbers.d.ts CHANGED
@@ -6,7 +6,7 @@ export declare const absent: unique symbol;
6
6
  * them, so numbering one never keeps it alive.
7
7
  */
8
8
  export declare class Numbers<I> {
9
- private readonly owner;
9
+ private owner;
10
10
  private next;
11
11
  /** Numbers released for reuse; null until one is, as most patches never release one. */
12
12
  private free;
@@ -26,6 +26,10 @@ export declare class Numbers<I> {
26
26
  constructor(owner: {
27
27
  forget(slot: number): void;
28
28
  });
29
+ /** Hands the numbers to a new owner, which hears of every release from now on. */
30
+ adopt(owner: {
31
+ forget(slot: number): void;
32
+ }): void;
29
33
  /** One past the highest number handed out, so a loop over 0..size meets every live one. */
30
34
  get size(): number;
31
35
  take(subject: I): number;
package/dist/numbers.js CHANGED
@@ -29,6 +29,10 @@ export class Numbers {
29
29
  constructor(owner) {
30
30
  this.owner = owner;
31
31
  }
32
+ /** Hands the numbers to a new owner, which hears of every release from now on. */
33
+ adopt(owner) {
34
+ this.owner = owner;
35
+ }
32
36
  /** One past the highest number handed out, so a loop over 0..size meets every live one. */
33
37
  get size() {
34
38
  return this.next;
package/dist/owner.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { arcShare } from './arcs.js';
1
2
  import { backWith, clampWeight, elapsedWith, envelope, timeWith } from './clock.js';
2
3
  /** What an owner is cued with in place of a patch: it writes nothing, so no fold ever meets it. */
3
4
  export const ownerPatch = Object.freeze({
@@ -128,8 +129,12 @@ export function ownWeight(owner, now, reduced, base) {
128
129
  /** What every owner above a voice multiplies into its weight. */
129
130
  export function ownersWeight(voice, now, reduced, base) {
130
131
  let w = 1;
131
- for (let o = voice.owner; o !== null; o = o.owner)
132
+ let child = voice;
133
+ for (let o = voice.owner; o !== null; child = o, o = o.owner) {
132
134
  w *= ownWeight(o, now, reduced, base);
135
+ if (o.arc !== null)
136
+ w *= arcShare(o, child, now, reduced);
137
+ }
133
138
  return w;
134
139
  }
135
140
  /**
package/dist/place.d.ts CHANGED
@@ -10,6 +10,8 @@ export declare function mixAt<I, O>(mix: Mixer<I, O>, u: number): number;
10
10
  export declare function pin<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, u: number): void;
11
11
  /** Puts each pending voice's start, given in host time, where the mix's rate now puts it. */
12
12
  export declare function repin<I, O>(mix: Mixer<I, O>): void;
13
+ /** Moves a pending voice's start, its clock with it; false where it is already there. */
14
+ export declare function startAt<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, start: number): boolean;
13
15
  /** Refuses a placement that names a mark twice on one side, or that waits on itself. */
14
16
  export declare function checkPlacement<I, O>(mix: Mixer<I, O>, spec: VoiceSpec<I, O>, anchor: Placement, owner: Voice<I, O> | null): void;
15
17
  /**
@@ -18,3 +20,9 @@ export declare function checkPlacement<I, O>(mix: Mixer<I, O>, spec: VoiceSpec<I
18
20
  * last gave. Repeated so a chain of anchors settles in one sync.
19
21
  */
20
22
  export declare function place<I, O>(mix: Mixer<I, O>): void;
23
+ /**
24
+ * The time an anchor answers on its voice's owner's clock, or undefined while its target has none.
25
+ * A time before the owner starts answers `before`: undefined, so a start waits, or -Infinity for a
26
+ * floor, which is no bound at all.
27
+ */
28
+ export declare function resolve<I, O>(mix: Mixer<I, O>, a: number | NonNullable<Placement['start']>, self: Voice<I, O>, before?: number): number | undefined;
package/dist/place.js CHANGED
@@ -5,6 +5,7 @@ import { noted } from './history.js';
5
5
  import { markOf } from './marks.js';
6
6
  import { mixTime, ownerReading } from './owner.js';
7
7
  import { holders, scored, scoredGone, scoreVersion, unhold } from './scored.js';
8
+ import { refloor } from './spans.js';
8
9
  import { none } from './voice.js';
9
10
  import { warnOnce } from './warn.js';
10
11
  /** What a voice's own owner's clock reads now: the mix clock for a voice no owner holds. */
@@ -35,7 +36,7 @@ export function repin(mix) {
35
36
  }
36
37
  }
37
38
  /** Moves a pending voice's start, its clock with it; false where it is already there. */
38
- function startAt(mix, voice, start) {
39
+ export function startAt(mix, voice, start) {
39
40
  if (start === voice.start)
40
41
  return false;
41
42
  voice.start = start;
@@ -145,18 +146,23 @@ export function place(mix) {
145
146
  }
146
147
  }
147
148
  if (!moved)
148
- return;
149
+ break;
149
150
  }
151
+ refloor(mix);
150
152
  }
151
- /** The time an anchor answers on its voice's owner's clock, or undefined while its target has none. */
152
- function resolve(mix, a, self) {
153
+ /**
154
+ * The time an anchor answers on its voice's owner's clock, or undefined while its target has none.
155
+ * A time before the owner starts answers `before`: undefined, so a start waits, or -Infinity for a
156
+ * floor, which is no bound at all.
157
+ */
158
+ export function resolve(mix, a, self, before) {
153
159
  const o = self.owner;
154
160
  if (typeof a === 'number')
155
161
  return o === null ? mixAt(mix, a - mix.offset) : a;
156
162
  if ('all' in a) {
157
163
  let latest = Number.NEGATIVE_INFINITY;
158
164
  for (const m of a.all) {
159
- const t = member(mix, m, self);
165
+ const t = member(mix, m, self, before);
160
166
  if (t === undefined)
161
167
  return undefined;
162
168
  latest = Math.max(latest, t);
@@ -169,7 +175,7 @@ function resolve(mix, a, self) {
169
175
  let earliest = Number.POSITIVE_INFINITY;
170
176
  let unknown = false;
171
177
  for (const m of a.any) {
172
- const t = member(mix, m, self);
178
+ const t = member(mix, m, self, before);
173
179
  if (t === undefined)
174
180
  unknown = true;
175
181
  else
@@ -205,14 +211,16 @@ function resolve(mix, a, self) {
205
211
  if (o === null)
206
212
  return t + by;
207
213
  const local = ownerReading(o, t);
214
+ if (local === Number.NEGATIVE_INFINITY)
215
+ return before;
208
216
  return Number.isFinite(local) ? local + by : undefined;
209
217
  }
210
218
  /**
211
219
  * What one member of a join answers, or the time it last gave once its target has left, as a lone
212
220
  * anchor keeps its voice where its target last put it.
213
221
  */
214
- function member(mix, a, self) {
215
- const t = resolve(mix, a, self);
222
+ function member(mix, a, self, before) {
223
+ const t = resolve(mix, a, self, before);
216
224
  if (t === undefined)
217
225
  return self.answers?.get(a);
218
226
  if (!mix.projecting) {
package/dist/project.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ownBlends } from './blend.js';
2
- import { heldAt } from './chain.js';
2
+ import { headOf, heldAt } from './chain.js';
3
3
  import { stepOf } from './clock.js';
4
4
  import { open, sighted, weightAt } from './everyone.js';
5
5
  import { fadesAt } from './fade.js';
@@ -362,7 +362,7 @@ function recordOf(mix, voice, subject) {
362
362
  // A voice sharing one record has that for every subject a chain has asked it about, pointed at
363
363
  // the subject for the caller to copy from.
364
364
  if (voice.sharing) {
365
- const slot = mix.chains.get(subject)?.slot ?? -1;
365
+ const slot = headOf(mix, subject)?.slot ?? -1;
366
366
  if (!sighted(voice, slot))
367
367
  return undefined;
368
368
  const all = voice.everyone;
@@ -373,7 +373,7 @@ function recordOf(mix, voice, subject) {
373
373
  const held = voice.subjects.get(subject);
374
374
  if (held !== undefined || voice.unreachedBits === null)
375
375
  return held;
376
- return unreached(voice, mix.chains.get(subject)?.slot ?? -1)
376
+ return unreached(voice, headOf(mix, subject)?.slot ?? -1)
377
377
  ? voice.unreached
378
378
  : undefined;
379
379
  }
package/dist/qualify.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Lanes } from './lanes.js';
2
+ import type { Channel } from './types.js';
2
3
  import type { Voice } from './voice.js';
3
4
  /** What qualifying needs of one voice: whether its patch and spec can run on a lane, and its channels. */
4
5
  export interface Candidate {
@@ -16,6 +17,15 @@ export declare function qualify(numeric: readonly boolean[], voices: readonly Ca
16
17
  voices: Set<number>;
17
18
  };
18
19
  export declare function requalify<I, O>(lanes: Lanes<I, O>, version: number): void;
20
+ /** Whether `keepCallsOff` applies; settable for bench/frame.mjs. */
21
+ export declare let callsOff: boolean;
22
+ export declare function setCallsOff(on: boolean): void;
23
+ /**
24
+ * A voice whose patch a lane would call, as a fn voice's (no tween, keys or motion), writing only
25
+ * arrays: a crowd writing a number reads 17% to 25% faster on lanes (bench/frame.mjs `named` under
26
+ * CALLS_OFF, msb-uai, 2026-10-21).
27
+ */
28
+ export declare function calls<I, O>(v: Voice<I, O>, channels: readonly Channel<unknown>[]): boolean;
19
29
  /**
20
30
  * A voice starts playing on a lane or crowd: its epoch. Every subject meets a voice over all of
21
31
  * them on its next probe; a voice naming its subjects marks only those.
package/dist/qualify.js CHANGED
@@ -53,6 +53,8 @@ export function requalify(lanes, version) {
53
53
  const numeric = host.channels.map((c) => numericOf(c) !== undefined);
54
54
  // A voice writing nothing has no lane to fill, so it stays where its calls are made.
55
55
  const fits = present.map((v) => v.slots.length > 0 && host.fits(v));
56
+ if (callsOff)
57
+ keepCallsOff(present, fits, host.channels);
56
58
  const candidates = () => present.map((v, i) => ({ id: v.id, fits: fits[i], slots: v.slots }));
57
59
  let { channels, voices } = qualify(numeric, candidates());
58
60
  // A locus folds its members together, so they run on lanes all together or not at all.
@@ -193,6 +195,54 @@ export function requalify(lanes, version) {
193
195
  }
194
196
  regroup(lanes, members);
195
197
  }
198
+ /** Whether `keepCallsOff` applies; settable for bench/frame.mjs. */
199
+ export let callsOff = true;
200
+ export function setCallsOff(on) {
201
+ callsOff = on;
202
+ }
203
+ /**
204
+ * A voice whose patch a lane would call, as a fn voice's (no tween, keys or motion), writing only
205
+ * arrays: a crowd writing a number reads 17% to 25% faster on lanes (bench/frame.mjs `named` under
206
+ * CALLS_OFF, msb-uai, 2026-10-21).
207
+ */
208
+ export function calls(v, channels) {
209
+ return (v.built === null &&
210
+ v.motion === undefined &&
211
+ v.slots.every((s) => vector(channels[s])));
212
+ }
213
+ /** A channel folding arrays with a rest, not `last`: what the rule was timed on. */
214
+ function vector(channel) {
215
+ return Array.isArray(channel.rest) && numericOf(channel)?.op !== 'last';
216
+ }
217
+ /**
218
+ * Leaves off lanes each voice calling its patch where no other fitting voice writes any of its
219
+ * channels, but a crowd's other voices: a lane calls such a patch once a subject as the general
220
+ * path does, and its fill walks the subjects a second time (bench/frame.mjs `fns`, 2.58 ms a frame
221
+ * on lanes against 1.07 off, studio, 2026-10-21). Two voices over many subjects on one channel
222
+ * fold faster on lanes (`tweenfn` with three voices).
223
+ */
224
+ function keepCallsOff(present, fits, channels) {
225
+ const others = new Array(channels.length).fill(false);
226
+ const wide = new Array(channels.length).fill(false);
227
+ present.forEach((v, i) => {
228
+ if (!fits[i])
229
+ return;
230
+ const crowd = crowdable(v);
231
+ if (!calls(v, channels))
232
+ for (const s of v.slots)
233
+ others[s] = true;
234
+ else if (!crowd)
235
+ for (const s of v.slots) {
236
+ if (wide[s])
237
+ others[s] = true;
238
+ wide[s] = true;
239
+ }
240
+ });
241
+ present.forEach((v, i) => {
242
+ if (fits[i] && calls(v, channels) && !v.slots.some((s) => others[s]))
243
+ fits[i] = false;
244
+ });
245
+ }
196
246
  /** Whether qualifying laned exactly the channels laned now. */
197
247
  function sameChannels(lanes, channels) {
198
248
  const by = lanes.bySlot;
@@ -273,7 +323,8 @@ export function retouch(lanes) {
273
323
  return qualify();
274
324
  const laned = v.slots.some((slot) => lanes.bySlot[slot] !== undefined);
275
325
  const fits = v.slots.length > 0 && lanes.host.fits(v);
276
- if (!laned && !fits) {
326
+ // As `keepCallsOff`: with none of its channels laned, no fitting voice but its like writes them.
327
+ if (!laned && (!fits || (callsOff && crowdable(v) && calls(v, lanes.host.channels)))) {
277
328
  lanes.whole = false;
278
329
  continue;
279
330
  }
package/dist/reading.d.ts CHANGED
@@ -7,7 +7,8 @@ export declare const reading: {
7
7
  live: boolean;
8
8
  /**
9
9
  * The earliest voice time a read may still ask for; a patch keeping a history may drop what is
10
- * older. Infinity where the mix keeps none, so only what plays now is kept.
10
+ * older. Infinity where the mix keeps none, so only what plays now is kept. Set before a motion
11
+ * voice's read alone, so another patch finds whatever the last one left.
11
12
  */
12
13
  horizon: number;
13
14
  /**
package/dist/reading.js CHANGED
@@ -7,7 +7,8 @@ export const reading = {
7
7
  live: true,
8
8
  /**
9
9
  * The earliest voice time a read may still ask for; a patch keeping a history may drop what is
10
- * older. Infinity where the mix keeps none, so only what plays now is kept.
10
+ * older. Infinity where the mix keeps none, so only what plays now is kept. Set before a motion
11
+ * voice's read alone, so another patch finds whatever the last one left.
11
12
  */
12
13
  horizon: Number.POSITIVE_INFINITY,
13
14
  /**
package/dist/record.d.ts CHANGED
@@ -1,4 +1,9 @@
1
1
  import type { Subject } from './voice.js';
2
+ /** A record with every field written out, optional ones too, as every record in a mix is built. */
3
+ export type WholeRecord = {
4
+ [K in Field]: Subject<unknown>[K];
5
+ };
6
+ type Field = keyof Subject<unknown>;
2
7
  /**
3
8
  * A subject's record for `voice` holding every field one can come to hold, in one order, so every
4
9
  * record in a mix shares one hidden class: a record built elsewhere, or given `base`, `snaps` or
@@ -7,3 +12,4 @@ import type { Subject } from './voice.js';
7
12
  * does not inline would box.
8
13
  */
9
14
  export declare function record(voice: object | null, reaches: boolean, state: unknown): Subject<unknown>;
15
+ export {};
package/dist/record.js CHANGED
@@ -40,5 +40,6 @@ export function record(voice, reaches, state) {
40
40
  slot: -1,
41
41
  run: -1,
42
42
  freed: -1,
43
+ restored: null,
43
44
  };
44
45
  }
@@ -2,43 +2,92 @@ import type { Leavings } from './leavings.js';
2
2
  import { Store } from './store.js';
3
3
  import type { Cut } from './transport.js';
4
4
  import type { Subject } from './voice.js';
5
+ /**
6
+ * The seeks back after the one a record was last put back, written or deleted at, which every
7
+ * record stamped then shares. The next seek fills it; a later seek to as early a moment joins it
8
+ * to its own, so the root it reaches holds the earliest moment sought since. Only records and the
9
+ * stamps joined beneath it hold it, so it is collected with the last record that can read it.
10
+ */
11
+ /** A moment sought, the end of frame `seq`. */
12
+ export interface Moment {
13
+ readonly t: number;
14
+ readonly seq: number;
15
+ }
16
+ export interface Stamp extends Moment {
17
+ /** On a root: the earliest moment sought since, and the frame that ended. */
18
+ t: number;
19
+ seq: number;
20
+ /** The stamp this one was joined to, null on a root. */
21
+ up: Stamp | null;
22
+ /** A bound on how deep the stamps joined beneath it go, so joining the lower keeps paths short. */
23
+ rank: number;
24
+ /** The records subjects left with after this seek's moment, and where it cut; null for none. */
25
+ left: Leavings<unknown> | null;
26
+ cut: Cut | null;
27
+ /** The store that made it; null on `gone`. */
28
+ readonly of: object | null;
29
+ }
5
30
  /**
6
31
  * A voice's records once a seek has gone back: each subject's is put back from the record it had,
7
32
  * the first time it is asked for after a seek, since the subjects a store holds cannot be listed.
8
- * It is one store however many seeks there are: by subject it keeps how many seeks its record has
9
- * been put back for, and a record behind is put back to the earliest moment sought since, as a
10
- * seek back cuts what came after it and a later seek ahead of that moment must not read past it.
33
+ * It is one store however many seeks there are: by subject it keeps the stamp of the seek its
34
+ * record was last put back at, and a record behind is put back to the earliest moment sought
35
+ * since, as a seek back cuts what came after it and a later seek ahead of that moment must not
36
+ * read past it.
11
37
  */
12
38
  export declare class Restored<I> extends Store<I, Subject<unknown>> {
13
39
  private readonly own;
14
40
  private readonly now;
15
41
  private readonly make;
16
- /** Seeks back so far. */
17
- private n;
18
- /** By subject, the `n` its record was last put back, written or deleted at; none reads as 0. */
42
+ /** What a record put back now is stamped with, filled by the next seek. */
43
+ private stamp;
44
+ /** What a record never stamped reads as: the first seek's. */
45
+ private readonly first;
46
+ /**
47
+ * By subject, the stamp its record was last put back, written or deleted at; none reads as
48
+ * `first`. A record that reaches its subject is its own, and holds it as `restored` instead,
49
+ * sparing a write here per subject a seek puts back; one that does not may stand for many.
50
+ */
19
51
  private readonly at;
20
52
  /**
21
- * Each seek back sought to an earlier moment than every one after it, oldest first: the earliest
22
- * moment sought since seek `n` is that of the first here at or after it.
53
+ * The roots, each moment earlier than the next, held weakly: one no record can reach is never
54
+ * read again, and those are cleared out once the list doubles.
55
+ */
56
+ private roots;
57
+ private kept;
58
+ /**
59
+ * Stamps made a batch ahead of their seeks: one made at its seek is young when every old record
60
+ * is written with it, and V8 notes each such write for its next scavenge.
23
61
  */
24
- private readonly least;
25
- /** By seek, the records subjects left with after its moment, and where it cut; null until one has any. */
26
- private futures;
62
+ private ready;
63
+ private aging;
27
64
  /** The latest seek's moment. */
28
- private t;
65
+ top: number;
29
66
  /**
30
67
  * `own` holds the records as they stood before the first seek, and `now` gives the one a subject
31
- * that has not left since has there, as a seek to `t`, the end of frame `seq`, takes it. `make`
32
- * puts a record back as it stood at `t`, undefined where it had none then; `top` is the latest
33
- * seek's moment.
68
+ * that has not left since and is not reached has there, as a seek to `at` takes it. `make` puts
69
+ * a record back as it stood at `at`, undefined where it had none then; `mine` says `live` is the
70
+ * record this store held, which nothing else does.
34
71
  */
35
- constructor(own: Store<I, Subject<unknown>>, now: (key: I, held: Subject<unknown> | undefined, t: number, seq: number) => Subject<unknown> | undefined, make: (live: Subject<unknown>, t: number, top: number) => Subject<unknown> | undefined);
72
+ constructor(own: Store<I, Subject<unknown>>, now: (key: I, held: Subject<unknown> | undefined, at: Moment) => Subject<unknown> | undefined, make: (live: Subject<unknown>, at: Moment, mine: boolean) => Subject<unknown> | undefined);
36
73
  /** A seek back to `t`: `left` has the records subjects left with after `cut`, null for none. */
37
74
  back(t: number, cut: Cut, left: Leavings<I> | null): void;
38
- /** The earliest moment sought by seek `from` or any after it, and the frame it ended. */
39
- private since;
75
+ /** The next seek's stamp, from a batch made a batch of seeks ago, twice as many each time to 64. */
76
+ private fresh;
77
+ /**
78
+ * Whether a reaching record stamped by this store is still its record for the key it was found
79
+ * under: every record let go is stamped `gone`, copies are stamped with nothing, and this turns
80
+ * false if a stamped record is set again, which could put it under a second key.
81
+ */
82
+ private sure;
40
83
  get(key: I): Subject<unknown> | undefined;
84
+ /** A chain's record for a subject, found again without a lookup where this store stamped it. */
85
+ find(key: I, guess: Subject<unknown>): Subject<unknown> | undefined;
86
+ /** `get`, given what `own` holds for `key`. */
87
+ private from;
41
88
  set(key: I, value: Subject<unknown>): void;
42
89
  delete(key: I): void;
90
+ /** Lets go of the record held for `key`, stamping it `gone`. */
91
+ private shed;
43
92
  clear(): void;
44
93
  }