@msb235/blits 0.10.0 → 0.12.0

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 (126) hide show
  1. package/CHANGELOG.md +248 -0
  2. package/README.md +15 -4
  3. package/dist/arcs.d.ts +40 -0
  4. package/dist/arcs.js +239 -0
  5. package/dist/blend.js +1 -5
  6. package/dist/book.d.ts +2 -0
  7. package/dist/book.js +27 -3
  8. package/dist/chain.d.ts +17 -1
  9. package/dist/chain.js +57 -15
  10. package/dist/channels.d.ts +2 -0
  11. package/dist/channels.js +12 -1
  12. package/dist/clock.d.ts +14 -0
  13. package/dist/clock.js +9 -1
  14. package/dist/clone.js +16 -4
  15. package/dist/columns.d.ts +4 -5
  16. package/dist/columns.js +11 -5
  17. package/dist/crowd.d.ts +1 -1
  18. package/dist/crowd.js +4 -1
  19. package/dist/cue.js +63 -7
  20. package/dist/departed.d.ts +8 -3
  21. package/dist/departed.js +76 -29
  22. package/dist/due.d.ts +4 -0
  23. package/dist/due.js +16 -3
  24. package/dist/easing.d.ts +12 -1
  25. package/dist/easing.js +7 -1
  26. package/dist/everyone.d.ts +14 -1
  27. package/dist/everyone.js +27 -2
  28. package/dist/fade.d.ts +12 -5
  29. package/dist/fade.js +57 -19
  30. package/dist/fill.js +16 -9
  31. package/dist/fit.d.ts +8 -1
  32. package/dist/fit.js +7 -3
  33. package/dist/fold.d.ts +1 -1
  34. package/dist/fold.js +112 -9
  35. package/dist/frames.d.ts +2 -0
  36. package/dist/frames.js +17 -0
  37. package/dist/gather.js +38 -7
  38. package/dist/handle.d.ts +7 -2
  39. package/dist/handle.js +28 -12
  40. package/dist/held.d.ts +13 -2
  41. package/dist/held.js +43 -9
  42. package/dist/history.d.ts +25 -2
  43. package/dist/history.js +163 -35
  44. package/dist/hosts.js +45 -17
  45. package/dist/index.d.ts +5 -2
  46. package/dist/index.js +3 -1
  47. package/dist/keyfill.js +3 -1
  48. package/dist/lane.d.ts +1 -1
  49. package/dist/lane.js +3 -1
  50. package/dist/lanes.d.ts +31 -7
  51. package/dist/lanes.js +57 -6
  52. package/dist/lean.d.ts +15 -0
  53. package/dist/lean.js +23 -0
  54. package/dist/leavings.d.ts +6 -1
  55. package/dist/leavings.js +16 -2
  56. package/dist/marks.d.ts +7 -3
  57. package/dist/marks.js +105 -27
  58. package/dist/meet.js +1 -0
  59. package/dist/mixer.d.ts +35 -6
  60. package/dist/mixer.js +113 -45
  61. package/dist/motion.d.ts +4 -1
  62. package/dist/motion.js +16 -2
  63. package/dist/motions.d.ts +4 -0
  64. package/dist/motions.js +45 -11
  65. package/dist/move.js +35 -12
  66. package/dist/numbers.d.ts +5 -1
  67. package/dist/numbers.js +4 -0
  68. package/dist/owner.d.ts +18 -2
  69. package/dist/owner.js +44 -3
  70. package/dist/pack.d.ts +1 -0
  71. package/dist/pack.js +3 -0
  72. package/dist/place.d.ts +8 -0
  73. package/dist/place.js +16 -8
  74. package/dist/placed.d.ts +19 -0
  75. package/dist/placed.js +54 -0
  76. package/dist/project.d.ts +5 -2
  77. package/dist/project.js +238 -34
  78. package/dist/pull.js +37 -8
  79. package/dist/qualify.d.ts +10 -0
  80. package/dist/qualify.js +60 -3
  81. package/dist/reading.d.ts +8 -1
  82. package/dist/reading.js +8 -1
  83. package/dist/record.d.ts +6 -0
  84. package/dist/record.js +2 -0
  85. package/dist/restored.d.ts +69 -17
  86. package/dist/restored.js +163 -50
  87. package/dist/revive.d.ts +6 -3
  88. package/dist/revive.js +11 -3
  89. package/dist/roster.js +2 -1
  90. package/dist/rows.js +15 -5
  91. package/dist/seek.d.ts +7 -2
  92. package/dist/seek.js +152 -43
  93. package/dist/shared.d.ts +5 -0
  94. package/dist/shared.js +5 -0
  95. package/dist/signals.d.ts +10 -0
  96. package/dist/signals.js +17 -0
  97. package/dist/spans.d.ts +5 -0
  98. package/dist/spans.js +75 -5
  99. package/dist/store.d.ts +13 -0
  100. package/dist/store.js +23 -0
  101. package/dist/strays.js +50 -2
  102. package/dist/tape.d.ts +29 -4
  103. package/dist/tape.js +49 -11
  104. package/dist/touch.js +3 -2
  105. package/dist/transport.d.ts +11 -2
  106. package/dist/transport.js +26 -10
  107. package/dist/types/arc.d.ts +55 -0
  108. package/dist/types/arc.js +1 -0
  109. package/dist/types/mix.d.ts +57 -14
  110. package/dist/types/patch.d.ts +5 -1
  111. package/dist/types/place.d.ts +11 -4
  112. package/dist/types/transport.d.ts +12 -3
  113. package/dist/types/voice.d.ts +72 -5
  114. package/dist/types/watch.d.ts +126 -0
  115. package/dist/types/watch.js +1 -0
  116. package/dist/types.d.ts +2 -0
  117. package/dist/unpage.d.ts +6 -1
  118. package/dist/unpage.js +34 -2
  119. package/dist/voice.d.ts +93 -16
  120. package/dist/voice.js +60 -3
  121. package/dist/watch.d.ts +26 -0
  122. package/dist/watch.js +441 -0
  123. package/dist/weigh.d.ts +13 -2
  124. package/dist/weigh.js +10 -3
  125. package/docs/schema.html +106 -38
  126. package/package.json +10 -4
package/dist/project.js CHANGED
@@ -1,37 +1,52 @@
1
1
  import { ownBlends } from './blend.js';
2
+ import { headOf, heldAt } from './chain.js';
3
+ import { stepOf } from './clock.js';
2
4
  import { open, sighted, weightAt } from './everyone.js';
5
+ import { fadesAt } from './fade.js';
6
+ import { meet, origin } from './held.js';
3
7
  import { copyHeld, last, lastWithin } from './history.js';
8
+ import { markOf, voiceMarks } from './marks.js';
4
9
  import { Mixer } from './mixer.js';
5
10
  import { move } from './move.js';
6
11
  import { heldByInput, ownerReading, relink } from './owner.js';
7
- import { pin } from './place.js';
12
+ import { pin, place } from './place.js';
8
13
  import { reading } from './reading.js';
9
14
  import { record } from './record.js';
10
15
  import { recount } from './roster.js';
11
16
  import { copyStanding, stands } from './standing.js';
12
- import { hostAt } from './tape.js';
17
+ import { hostAhead, hostAt, replay } from './tape.js';
13
18
  import { Transport, within } from './transport.js';
14
- import { cover } from './unpage.js';
19
+ import { cover, coverFrame } from './unpage.js';
15
20
  import { unreached } from './unreached.js';
16
21
  /**
17
22
  * Every mix on a transport read at mix time `t`, through copies of them on a transport of their
18
- * own, moved together so an anchor across mixes answers as it would live.
23
+ * own, moved together so an anchor across mixes answers as it would live. Ahead, they stop at each
24
+ * known event on the way, and a subject asked about is read at each before `t`.
19
25
  */
20
- export function projectAll(transport, t) {
21
- const ahead = Number.isNaN(transport.now) || t >= transport.now;
22
- const tape = transport.tape;
23
- const stamp = ahead && tape !== undefined ? tape.timestampAt(tape.undoDepth()) : undefined;
26
+ export function projectAll(transport, at) {
27
+ const kept = transport.history !== undefined;
28
+ let t;
29
+ let numbered;
30
+ if (typeof at === 'number')
31
+ t = at;
32
+ else {
33
+ if (!kept)
34
+ throw new Error('blits: reading a frame back needs a mix made with history');
35
+ numbered = coverFrame(transport, at.frame);
36
+ t = numbered.at;
37
+ }
38
+ // The frame being played reads as the mix's own time does; any other is behind, whatever its time.
39
+ const ahead = numbered !== undefined
40
+ ? numbered.seq === transport.seq
41
+ : Number.isNaN(transport.now) || t >= transport.now;
24
42
  const pace = transport.pace;
25
- const next = stamp === undefined || pace === null ? stamp : pace.reading(stamp);
26
- if (next !== undefined && next <= t)
27
- throw new Error(`blits: the tape holds calls at ${next} that a read ahead to ${t} cannot play; seek there to see them`);
28
43
  // Behind, the last frame at or before `t`, read as it showed when `t` is that frame's own time.
29
44
  let cut = { seq: transport.seq, strict: false };
30
- let u = hostAt(transport, t);
31
- const kept = transport.history !== undefined;
45
+ let u = ahead ? hostAhead(transport, t) : hostAt(transport, t);
32
46
  if (!ahead && kept) {
33
- cover(transport, t);
34
- const frame = transport.frames.at(t);
47
+ if (numbered === undefined)
48
+ cover(transport, t);
49
+ const frame = numbered ?? transport.frames.at(t);
35
50
  if (frame !== undefined) {
36
51
  cut = { seq: frame.seq, strict: frame.at === t };
37
52
  if (frame.at === t)
@@ -57,7 +72,7 @@ export function projectAll(transport, t) {
57
72
  history: undefined,
58
73
  lanes: false,
59
74
  transport: c,
60
- });
75
+ }, null);
61
76
  copy.slot = mix.slot;
62
77
  copy.projecting = true;
63
78
  copies.set(mix, copy);
@@ -70,28 +85,50 @@ export function projectAll(transport, t) {
70
85
  }
71
86
  // A read at the mix's own time lands nothing, as a probe does not: what the host has done since
72
87
  // the last sync shows at the next one.
73
- if (ahead && t !== transport.now)
74
- for (const copy of copies.values())
75
- move(copy, t);
88
+ let stood = c;
89
+ let earlier = null;
90
+ if (ahead && t !== transport.now) {
91
+ earlier = new Map();
92
+ stood = playAhead(transport, c, copies, t, earlier);
93
+ }
76
94
  const read = (f) => {
77
95
  reading.live = false;
96
+ reading.ahead = stood.ahead.size > 0 ? stood.ahead : null;
78
97
  try {
79
98
  return f();
80
99
  }
81
100
  finally {
82
101
  reading.live = true;
102
+ reading.ahead = null;
83
103
  }
84
104
  };
85
105
  return {
86
106
  timestamp: t,
87
107
  of(mix) {
88
- const copy = copies.get(mix);
108
+ const key = mix;
109
+ const copy = copies.get(key);
89
110
  if (copy === undefined)
90
111
  throw new Error('blits: that mix is not on this transport');
112
+ const stages = earlier?.get(key);
113
+ // A subject asked about is read at every stop first, in order, so the state it carries
114
+ // crosses each event as a frame there would carry it.
115
+ const warm = stages === undefined
116
+ ? null
117
+ : (subject) => {
118
+ if (stages.met.has(subject))
119
+ return;
120
+ stages.met.add(subject);
121
+ for (const stage of stages.at)
122
+ stage.fold(subject);
123
+ };
91
124
  return {
92
125
  timestamp: t,
93
- probe: (subject, out) => read(() => copy.fold(subject, out)),
126
+ probe: (subject, out) => read(() => {
127
+ warm?.(subject);
128
+ return copy.fold(subject, out);
129
+ }),
94
130
  assess: (subject) => read(() => {
131
+ warm?.(subject);
95
132
  copy.fold(subject);
96
133
  return doubts(copy, subject);
97
134
  }),
@@ -99,7 +136,150 @@ export function projectAll(transport, t) {
99
136
  },
100
137
  };
101
138
  }
102
- /** A mix's copy for a read ahead: its voices as they stand, each subject carried from the live one. */
139
+ /**
140
+ * Moves a read ahead's copies to mix time `t`, making the calls the tape holds on the way as a seek
141
+ * there makes them on the mixes. While a voice could carry state across it, it stops at each known
142
+ * event: a call, and every mark of a voice or announced. At each stop the copies are kept for
143
+ * subjects to be read there and copied on, so `copies` ends holding those at `t`. Answers the
144
+ * transport they stand on, and puts the tape back where it stood.
145
+ */
146
+ function playAhead(transport, c, copies, t, earlier) {
147
+ const tape = transport.tape;
148
+ const depth = tape?.undoDepth() ?? 0;
149
+ let played = false;
150
+ try {
151
+ for (;;) {
152
+ const e = nextEvent(tape, c, t);
153
+ if (!(e < t))
154
+ break;
155
+ played = playTo(transport, c, copies, e) || played;
156
+ if (!carries(c))
157
+ continue;
158
+ // Placed now, as the next move would place them, while a target that has just left is here.
159
+ for (const m of c.members)
160
+ if (m.anchored > 0)
161
+ place(m);
162
+ const next = restage(c);
163
+ for (const [mix, copy] of copies) {
164
+ const on = new Mixer(copy.kit, { ...copy.opts, transport: next }, null);
165
+ on.slot = copy.slot;
166
+ on.projecting = true;
167
+ copyAhead(copy, on);
168
+ const stages = earlier.get(mix);
169
+ if (stages === undefined)
170
+ earlier.set(mix, { at: [copy], met: new Set() });
171
+ else
172
+ stages.at.push(copy);
173
+ copies.set(mix, on);
174
+ }
175
+ c = next;
176
+ }
177
+ played = playTo(transport, c, copies, t) || played;
178
+ }
179
+ finally {
180
+ if (played)
181
+ tape?.goto(depth);
182
+ }
183
+ return c;
184
+ }
185
+ /**
186
+ * Whether a voice on a read ahead's copies could carry state from one known event to the next: a
187
+ * patch that steps, or a weight given by a signal, which may keep state. Where none could, a read
188
+ * crosses the events in one go, as fast as one with none.
189
+ */
190
+ function carries(c) {
191
+ for (const m of c.members)
192
+ for (const v of m.cued)
193
+ if (v.state !== 'done' && (v.patch.step !== undefined || typeof v.spec.weight === 'function'))
194
+ return true;
195
+ return false;
196
+ }
197
+ /**
198
+ * The earliest known event a read ahead's copies have yet to reach before `t`, mix time: the next
199
+ * call the tape holds, and while a voice could carry state across one, a mark of a voice or one
200
+ * announced. Infinity where there is none.
201
+ */
202
+ function nextEvent(tape, c, t) {
203
+ const now = c.now;
204
+ let e = Number.POSITIVE_INFINITY;
205
+ const call = tape?.timestampAt(tape.undoDepth());
206
+ if (call !== undefined)
207
+ e = c.pace === null ? call : c.pace.reading(call);
208
+ if (!carries(c))
209
+ return e < t ? e : Number.POSITIVE_INFINITY;
210
+ for (const m of c.members)
211
+ for (const v of m.cued) {
212
+ if (v.state === 'done')
213
+ continue;
214
+ for (const mark of voiceMarks) {
215
+ const at = markOf(m, v, mark);
216
+ if (at !== undefined && at > now && at < e)
217
+ e = at;
218
+ }
219
+ const inner = v.spec.marks;
220
+ if (inner !== undefined)
221
+ for (const mark of Object.keys(inner)) {
222
+ const at = markOf(m, v, mark);
223
+ if (at !== undefined && at > now && at < e)
224
+ e = at;
225
+ }
226
+ }
227
+ for (const a of c.announced) {
228
+ const at = c.pace === null ? a.at : c.pace.at(a.at, c.u);
229
+ if (at > now && at < e)
230
+ e = at;
231
+ }
232
+ return e < t ? e : Number.POSITIVE_INFINITY;
233
+ }
234
+ /**
235
+ * Makes the calls the tape holds up to mix time `to` on a read ahead's copies and moves them
236
+ * there. Answers whether it moved the tape.
237
+ */
238
+ function playTo(transport, c, copies, to) {
239
+ const tape = transport.tape;
240
+ const due = (at) => (c.pace === null ? at : c.pace.reading(at)) <= to;
241
+ const next = tape?.timestampAt(tape.undoDepth());
242
+ c.u = hostAhead(c, to);
243
+ let played = false;
244
+ if (next !== undefined && due(next)) {
245
+ // A call may write what a copy shares with the voice it was copied from.
246
+ for (const copy of c.members)
247
+ for (const v of copy.cued) {
248
+ if (v.parts !== null)
249
+ v.parts = new Map(v.parts);
250
+ if (v.parted !== null)
251
+ v.parted = new Map(v.parted);
252
+ }
253
+ played = true;
254
+ const last = replay(transport, due, c, {
255
+ transport: () => c,
256
+ mix: (of) => copies.get(of),
257
+ });
258
+ c.u = Math.max(last, hostAhead(c, to));
259
+ }
260
+ // A call played at `to` itself moved the copies there before it, as a sync does.
261
+ if (!(c.now >= to))
262
+ for (const copy of c.members)
263
+ move(copy, to);
264
+ return played;
265
+ }
266
+ /** A transport for a read ahead's copies to go on from where those on `c` stand. */
267
+ function restage(c) {
268
+ const next = new Transport(undefined, true);
269
+ next.offset = c.offset;
270
+ next.u = c.u;
271
+ next.nextId = c.nextId;
272
+ next.now = c.now;
273
+ next.pace = c.pace === null ? null : c.pace.until(Number.POSITIVE_INFINITY);
274
+ next.announced = c.announced.map((a) => ({ ...a }));
275
+ // The retargets and pushes the tape made carry their times, so every stop shares one list.
276
+ next.ahead = c.ahead;
277
+ return next;
278
+ }
279
+ /**
280
+ * A mix's copy for a read ahead: its voices as they stand, each subject carried from the live one,
281
+ * or from the copy at the known event before.
282
+ */
103
283
  function copyAhead(mix, c) {
104
284
  mirror(mix, c);
105
285
  c.cued = mix.cued
@@ -112,7 +292,8 @@ function copyAhead(mix, c) {
112
292
  return copy;
113
293
  });
114
294
  ownBlends(c.cued);
115
- if (mix.owners !== null)
295
+ // A copy keeps no list of owners, so the one before says only by its voices.
296
+ if (mix.owners !== null || (mix.projecting && c.cued.some((v) => v.holding !== null)))
116
297
  relink(c.cued);
117
298
  recount(c);
118
299
  }
@@ -136,11 +317,26 @@ function copyBack(mix, c, t, cut) {
136
317
  const copy = v.copy((subject) => recall(mix, v, subject, t, cut, c, copy), lastWithin(log, cut) ?? log[0]);
137
318
  if (then !== undefined)
138
319
  copy.setting.host = then;
320
+ // Its own, which a fade the copy plays out writes to.
321
+ if (v.parts !== null)
322
+ copy.parts = new Map(v.parts);
323
+ if (v.parted !== null)
324
+ copy.parted = new Map(v.parted);
325
+ if (v.left !== null)
326
+ fadesAt(copy, v.left.after(cut), cut.seq);
327
+ if (copy.parted !== null)
328
+ for (const [subject, p] of copy.parted) {
329
+ // A ramp that has ended since was still running at `t`.
330
+ if (p.ramp === undefined || !(t < p.at))
331
+ continue;
332
+ copy.parts ??= new Map();
333
+ copy.parts.set(subject, p.ramp);
334
+ }
139
335
  return copy;
140
336
  });
141
337
  ownBlends(c.cued);
142
338
  if (mix.owners !== null)
143
- relink(c.cued);
339
+ relink(c.cued, cut.seq);
144
340
  stateAt(c, t);
145
341
  recount(c);
146
342
  }
@@ -166,7 +362,7 @@ function recordOf(mix, voice, subject) {
166
362
  // A voice sharing one record has that for every subject a chain has asked it about, pointed at
167
363
  // the subject for the caller to copy from.
168
364
  if (voice.sharing) {
169
- const slot = mix.chains.get(subject)?.slot ?? -1;
365
+ const slot = headOf(mix, subject)?.slot ?? -1;
170
366
  if (!sighted(voice, slot))
171
367
  return undefined;
172
368
  const all = voice.everyone;
@@ -177,7 +373,7 @@ function recordOf(mix, voice, subject) {
177
373
  const held = voice.subjects.get(subject);
178
374
  if (held !== undefined || voice.unreachedBits === null)
179
375
  return held;
180
- return unreached(voice, mix.chains.get(subject)?.slot ?? -1)
376
+ return unreached(voice, headOf(mix, subject)?.slot ?? -1)
181
377
  ? voice.unreached
182
378
  : undefined;
183
379
  }
@@ -202,12 +398,16 @@ function carry(mix, voice, subject, c, copy) {
202
398
  * voice's start with fresh state, which is exact for a voice stepped at a fixed interval.
203
399
  */
204
400
  export function recall(mix, voice, subject, t, cut, c, copy) {
205
- const live = voice.left?.at(subject, cut) ?? recordOf(mix, voice, subject);
401
+ const live = voice.left?.at(subject, cut) ??
402
+ heldAt(voice, subject, recordOf(mix, voice, subject), t, cut.seq);
206
403
  if (live === undefined)
207
404
  return undefined;
208
405
  const snap = live.snaps && last(live.snaps, t, true);
209
406
  if (snap) {
210
407
  const h = copyHeld(voice, snap.held);
408
+ // A start still ahead when the copy was kept may have moved before `t`, with no copy between.
409
+ if (c !== undefined && copy !== undefined && !(h.since <= snap.at))
410
+ origin(c, copy, h);
211
411
  h.from = h.stepped;
212
412
  h.replay = live.inputs;
213
413
  return h;
@@ -215,17 +415,16 @@ export function recall(mix, voice, subject, t, cut, c, copy) {
215
415
  // Its origin by the controls the voice had then: a seek or an anchor has moved a waiting
216
416
  // voice's since.
217
417
  const since = c === undefined || copy === undefined ? live.since : c.sinceOf(copy, live.delay);
218
- const stepped = since < t ? since : t;
219
418
  const h = record(voice, live.reaches, live.reaches && voice.patch.state
220
419
  ? voice.patch.state(subject)
221
420
  : undefined);
222
421
  h.delay = live.delay;
223
422
  h.since = since;
224
423
  h.shown = c === undefined || copy === undefined ? live.shown : c.shownOf(copy, since);
225
- h.stepped = stepped;
424
+ const fresh = meet(mix, voice, h, t);
226
425
  h.rebuilt = voice.rebuilds;
227
- h.from = stepped;
228
- h.unknown = voice.spec.from === 'current';
426
+ h.from = h.stepped;
427
+ h.unknown = voice.spec.from === 'current' || fresh;
229
428
  h.replay = live.inputs;
230
429
  return h;
231
430
  }
@@ -286,10 +485,15 @@ function doubtOf(mix, voice, held) {
286
485
  return 'held';
287
486
  }
288
487
  if (mix.now > (held.from ?? mix.now)) {
289
- const tick = mix.opts.stepMs;
290
- const fixed = tick !== undefined && tick > 0 && !mix.reducedNow;
291
- if ((voice.patch.step && !fixed) || (held.kept !== null && held.kept.size > 0 && !replayed))
488
+ const fixed = stepOf(mix, voice) !== undefined;
489
+ // A read ahead reads a signal's state at every known event, and one step across a stretch
490
+ // with none in it is as good as any number; a read back steps it from a copy in one go.
491
+ const kept = held.kept !== null && held.kept.size > 0 && !replayed && mix.backward;
492
+ if ((voice.patch.step && !fixed) || kept)
292
493
  return 'stepped';
293
494
  }
495
+ // A seek stepped it from a copy, and what it keeps now goes on from there.
496
+ if (held.copied && held.kept !== null && held.kept.size > 0 && !replayed)
497
+ return 'stepped';
294
498
  return 'exact';
295
499
  }
package/dist/pull.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { axesOf } from './channels.js';
2
2
  import { copy } from './clone.js';
3
3
  import { clampRun, flush, pullRun, writeLater } from './columns.js';
4
+ /** What `pull` remembers at a position it has read no subject at. */
5
+ const UNREAD = Symbol('unread');
4
6
  /** Writes a folded pose's value into subject `n`'s places in a column. */
5
7
  function writeValue(c, v, n) {
6
8
  const at = n * c.axes;
@@ -76,19 +78,33 @@ export function pull(mix, subjects, into) {
76
78
  // all of them at once while nothing has relinked since the last pull, else one by one.
77
79
  const was = mix.pulled;
78
80
  const heads = mix.pulledHeads;
79
- was.length = list.length;
80
- heads.length = list.length;
81
81
  if (mix.pulledSlots.length < list.length) {
82
- const slots = new Int32Array(Math.max(64, list.length));
82
+ const slots = new Int32Array(Math.max(64, list.length)).fill(-1);
83
83
  slots.set(mix.pulledSlots);
84
84
  mix.pulledSlots = slots;
85
85
  }
86
+ // A position the list no longer has, or has anew, holds no head: its slot says so.
87
+ if (list.length < was.length)
88
+ mix.pulledSlots.fill(-1, list.length, was.length);
89
+ // A new position holds a mark no subject equals: a hole would read as `undefined`, a subject.
90
+ for (let n = was.length; n < list.length; n++)
91
+ was.push(UNREAD);
92
+ was.length = list.length;
93
+ heads.length = list.length;
86
94
  const version = mix.version;
87
95
  const relinks = mix.relinks;
88
96
  const live = !Number.isNaN(now);
89
97
  const current = live && mix.pulledVersion === version && mix.pulledRelinks === relinks;
98
+ // Which remembered heads `pullRun` takes as current: all while nothing has changed; those
99
+ // `unlink` has not stamped since `pulledRelinks` while the version they were linked at stands;
100
+ // none otherwise.
101
+ const since = current
102
+ ? Number.POSITIVE_INFINITY
103
+ : live && mix.pulledVersion === version
104
+ ? mix.pulledRelinks
105
+ : -1;
90
106
  try {
91
- readAll(mix, list, columns, live, current, whole);
107
+ readAll(mix, list, columns, live, current, since, whole);
92
108
  }
93
109
  finally {
94
110
  if (lanes !== null)
@@ -104,7 +120,7 @@ export function pull(mix, subjects, into) {
104
120
  * compile the loop on the stack does not leave `pull`'s own code compiled before its tail had run:
105
121
  * under churn that code deoptimized at the tail on every frame.
106
122
  */
107
- function readAll(mix, list, columns, live, current, whole) {
123
+ function readAll(mix, list, columns, live, current, since, whole) {
108
124
  const lanes = mix.lanes;
109
125
  const now = mix.now;
110
126
  const scratch = mix.scratch;
@@ -116,7 +132,7 @@ function readAll(mix, list, columns, live, current, whole) {
116
132
  const runs = live && whole;
117
133
  for (let n = 0; n < list.length; n++) {
118
134
  if (runs) {
119
- n = pullRun(lanes, slots, list, was, current ? null : heads, n, columns, now, mix.version);
135
+ n = pullRun(lanes, slots, list, was, since, n, columns, now, mix.version);
120
136
  if (n === list.length)
121
137
  break;
122
138
  }
@@ -149,13 +165,24 @@ function readAll(mix, list, columns, live, current, whole) {
149
165
  writeValue(c, pose[c.key], n);
150
166
  }
151
167
  }
152
- /** The kit slot, width, rest and bounds of every channel `pull` was handed an array for. */
168
+ /**
169
+ * The kit slot, width, rest and bounds of every channel `pull` was handed an array for: one record
170
+ * a channel for the mix's life, pointed at the array this call was handed.
171
+ */
153
172
  function columnsOf(mix, into) {
154
173
  const columns = [];
174
+ mix.pullColumns ??= new Map();
175
+ const known = mix.pullColumns;
155
176
  for (const key of Object.keys(into)) {
156
177
  const out = into[key];
157
178
  if (out === undefined)
158
179
  continue;
180
+ const had = known.get(key);
181
+ if (had !== undefined) {
182
+ had.out = out;
183
+ columns.push(had);
184
+ continue;
185
+ }
159
186
  const slot = mix.slotOf.get(key);
160
187
  if (slot === undefined)
161
188
  throw new Error(`blits: pull was handed ${key}, which the kit lacks`);
@@ -170,7 +197,9 @@ function columnsOf(mix, into) {
170
197
  at.set(rest);
171
198
  if (bounds !== undefined)
172
199
  clampRun(at, 0, axes, bounds);
173
- columns.push({ key, slot, axes, out, rest: at, bounds });
200
+ const column = { key, slot, axes, out, rest: at, bounds };
201
+ known.set(key, column);
202
+ columns.push(column);
174
203
  }
175
204
  return columns;
176
205
  }
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
@@ -1,6 +1,7 @@
1
1
  import { numericOf } from './channels.js';
2
2
  import { flush } from './columns.js';
3
3
  import { crowdable } from './crowd.js';
4
+ import { byId } from './due.js';
4
5
  import { weighAt } from './everyone.js';
5
6
  import { wake } from './fill.js';
6
7
  import { Lane } from './lane.js';
@@ -52,6 +53,8 @@ export function requalify(lanes, version) {
52
53
  const numeric = host.channels.map((c) => numericOf(c) !== undefined);
53
54
  // A voice writing nothing has no lane to fill, so it stays where its calls are made.
54
55
  const fits = present.map((v) => v.slots.length > 0 && host.fits(v));
56
+ if (callsOff)
57
+ keepCallsOff(present, fits, host.channels);
55
58
  const candidates = () => present.map((v, i) => ({ id: v.id, fits: fits[i], slots: v.slots }));
56
59
  let { channels, voices } = qualify(numeric, candidates());
57
60
  // A locus folds its members together, so they run on lanes all together or not at all.
@@ -192,6 +195,54 @@ export function requalify(lanes, version) {
192
195
  }
193
196
  regroup(lanes, members);
194
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
+ }
195
246
  /** Whether qualifying laned exactly the channels laned now. */
196
247
  function sameChannels(lanes, channels) {
197
248
  const by = lanes.bySlot;
@@ -231,8 +282,13 @@ export function open(lanes, v) {
231
282
  * A crowd a quarter empty rows is compacted.
232
283
  */
233
284
  export function retouch(lanes) {
234
- const touched = [...new Set(lanes.touched)].sort((a, b) => a.id - b.id);
235
- lanes.touched.length = 0;
285
+ const touched = byId(lanes.touched);
286
+ lanes.touched = [];
287
+ let kept = 0;
288
+ for (let i = 0; i < touched.length; i++)
289
+ if (i === 0 || touched[i] !== touched[i - 1])
290
+ touched[kept++] = touched[i];
291
+ touched.length = kept;
236
292
  // The qualify a false answer calls takes up every touched voice, crowd rows starting included.
237
293
  const qualify = () => {
238
294
  lanes.touched = touched;
@@ -267,7 +323,8 @@ export function retouch(lanes) {
267
323
  return qualify();
268
324
  const laned = v.slots.some((slot) => lanes.bySlot[slot] !== undefined);
269
325
  const fits = v.slots.length > 0 && lanes.host.fits(v);
270
- 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)))) {
271
328
  lanes.whole = false;
272
329
  continue;
273
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
  /**
@@ -25,4 +26,10 @@ export declare const reading: {
25
26
  * since it read it. One count for every mix, as `moved` is.
26
27
  */
27
28
  inputs: number;
29
+ /**
30
+ * While a read ahead that played its tape's calls reads: the retargets and pushes those calls
31
+ * made, by motion patch and then subject, in time order. A patch applies them with its own and
32
+ * commits none. Null for every other read.
33
+ */
34
+ ahead: Map<object, Map<unknown, unknown[]>> | null;
28
35
  };
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
  /**
@@ -25,4 +26,10 @@ export const reading = {
25
26
  * since it read it. One count for every mix, as `moved` is.
26
27
  */
27
28
  inputs: 0,
29
+ /**
30
+ * While a read ahead that played its tape's calls reads: the retargets and pushes those calls
31
+ * made, by motion patch and then subject, in time order. A patch applies them with its own and
32
+ * commits none. Null for every other read.
33
+ */
34
+ ahead: null,
28
35
  };
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 {};