@msb235/blits 0.10.0 → 0.11.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 (94) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +15 -4
  3. package/dist/blend.js +1 -5
  4. package/dist/book.d.ts +2 -0
  5. package/dist/book.js +27 -3
  6. package/dist/chain.d.ts +11 -0
  7. package/dist/chain.js +24 -2
  8. package/dist/clock.d.ts +14 -0
  9. package/dist/clock.js +9 -1
  10. package/dist/columns.d.ts +4 -5
  11. package/dist/columns.js +11 -5
  12. package/dist/crowd.d.ts +1 -1
  13. package/dist/crowd.js +4 -1
  14. package/dist/cue.js +52 -5
  15. package/dist/departed.d.ts +3 -2
  16. package/dist/departed.js +7 -4
  17. package/dist/due.d.ts +4 -0
  18. package/dist/due.js +16 -3
  19. package/dist/fade.d.ts +12 -5
  20. package/dist/fade.js +55 -17
  21. package/dist/fold.d.ts +1 -1
  22. package/dist/fold.js +53 -6
  23. package/dist/frames.d.ts +2 -0
  24. package/dist/frames.js +17 -0
  25. package/dist/handle.d.ts +5 -2
  26. package/dist/handle.js +20 -12
  27. package/dist/held.d.ts +8 -0
  28. package/dist/held.js +37 -6
  29. package/dist/history.d.ts +6 -0
  30. package/dist/history.js +15 -5
  31. package/dist/hosts.js +18 -6
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.js +1 -1
  34. package/dist/keyfill.js +3 -1
  35. package/dist/lane.d.ts +1 -1
  36. package/dist/lane.js +3 -1
  37. package/dist/lanes.d.ts +8 -0
  38. package/dist/lanes.js +21 -4
  39. package/dist/lean.d.ts +15 -0
  40. package/dist/lean.js +23 -0
  41. package/dist/leavings.d.ts +6 -1
  42. package/dist/leavings.js +16 -2
  43. package/dist/marks.d.ts +7 -3
  44. package/dist/marks.js +102 -27
  45. package/dist/meet.js +1 -0
  46. package/dist/mixer.d.ts +21 -6
  47. package/dist/mixer.js +70 -29
  48. package/dist/motion.d.ts +4 -1
  49. package/dist/motion.js +16 -2
  50. package/dist/motions.d.ts +4 -0
  51. package/dist/motions.js +45 -11
  52. package/dist/move.js +28 -10
  53. package/dist/owner.d.ts +18 -2
  54. package/dist/owner.js +38 -2
  55. package/dist/pack.d.ts +1 -0
  56. package/dist/pack.js +3 -0
  57. package/dist/placed.d.ts +19 -0
  58. package/dist/placed.js +54 -0
  59. package/dist/project.d.ts +5 -2
  60. package/dist/project.js +236 -32
  61. package/dist/pull.js +37 -8
  62. package/dist/qualify.js +8 -2
  63. package/dist/reading.d.ts +6 -0
  64. package/dist/reading.js +6 -0
  65. package/dist/record.js +1 -0
  66. package/dist/restored.d.ts +7 -4
  67. package/dist/restored.js +13 -10
  68. package/dist/revive.d.ts +5 -3
  69. package/dist/revive.js +10 -3
  70. package/dist/rows.js +15 -5
  71. package/dist/seek.d.ts +7 -2
  72. package/dist/seek.js +90 -35
  73. package/dist/shared.d.ts +5 -0
  74. package/dist/shared.js +5 -0
  75. package/dist/signals.d.ts +10 -0
  76. package/dist/signals.js +17 -0
  77. package/dist/store.d.ts +11 -0
  78. package/dist/store.js +19 -0
  79. package/dist/strays.js +50 -2
  80. package/dist/tape.d.ts +29 -4
  81. package/dist/tape.js +49 -11
  82. package/dist/transport.d.ts +11 -2
  83. package/dist/transport.js +20 -9
  84. package/dist/types/mix.d.ts +39 -14
  85. package/dist/types/patch.d.ts +5 -1
  86. package/dist/types/place.d.ts +11 -4
  87. package/dist/types/transport.d.ts +12 -3
  88. package/dist/types/voice.d.ts +62 -4
  89. package/dist/unpage.d.ts +6 -1
  90. package/dist/unpage.js +34 -2
  91. package/dist/voice.d.ts +57 -11
  92. package/dist/voice.js +43 -3
  93. package/docs/schema.html +68 -36
  94. package/package.json +7 -2
package/dist/placed.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * By subject number, the crowd voices whose rows are placed on it, so letting a number go reaches
3
+ * its rows without walking every crowd's. Kept by voice id, which a row keeps as a crowd closes up
4
+ * or is rebuilt around it.
5
+ */
6
+ export class Placed {
7
+ /** By subject number, one voice placed on it; 0, which is no voice's id, for none. */
8
+ one = new Int32Array(0);
9
+ /** The others, for a subject with several; null until some subject has two. */
10
+ rest = null;
11
+ /** Makes room for subject numbers below `cap`. */
12
+ grow(cap) {
13
+ const one = new Int32Array(cap);
14
+ one.set(this.one);
15
+ this.one = one;
16
+ }
17
+ /** Some voice placed on the subject at `slot`, 0 for none. */
18
+ any(slot) {
19
+ return this.one[slot] | 0;
20
+ }
21
+ /** Every voice placed on the subject at `slot`, in no order: for a test to read. */
22
+ all(slot) {
23
+ const one = this.any(slot);
24
+ return one === 0 ? [] : [one, ...(this.rest?.get(slot) ?? [])];
25
+ }
26
+ add(slot, id) {
27
+ if (this.one[slot] === 0) {
28
+ this.one[slot] = id;
29
+ return;
30
+ }
31
+ this.rest ??= new Map();
32
+ const more = this.rest.get(slot);
33
+ if (more === undefined)
34
+ this.rest.set(slot, [id]);
35
+ else
36
+ more.push(id);
37
+ }
38
+ remove(slot, id) {
39
+ const more = this.rest?.get(slot);
40
+ if (this.one[slot] === id)
41
+ this.one[slot] = more === undefined ? 0 : more.pop();
42
+ else if (more !== undefined) {
43
+ const i = more.indexOf(id);
44
+ if (i < 0)
45
+ return;
46
+ more[i] = more[more.length - 1];
47
+ more.pop();
48
+ }
49
+ else
50
+ return;
51
+ if (more !== undefined && more.length === 0)
52
+ this.rest.delete(slot);
53
+ }
54
+ }
package/dist/project.d.ts CHANGED
@@ -4,9 +4,12 @@ import type { TransportProjection } from './types.js';
4
4
  import type { Subject, Voice } from './voice.js';
5
5
  /**
6
6
  * Every mix on a transport read at mix time `t`, through copies of them on a transport of their
7
- * own, moved together so an anchor across mixes answers as it would live.
7
+ * own, moved together so an anchor across mixes answers as it would live. Ahead, they stop at each
8
+ * known event on the way, and a subject asked about is read at each before `t`.
8
9
  */
9
- export declare function projectAll(transport: Transport, t: number): TransportProjection;
10
+ export declare function projectAll(transport: Transport, at: number | {
11
+ readonly frame: number;
12
+ }): TransportProjection;
10
13
  /** Sets each copied voice's state to what its clock and fade make it at `t`. */
11
14
  export declare function stateAt<I, O>(c: Mixer<I, O>, t: number): void;
12
15
  /** Gives a projection's copy what it reads of the live mix as it is, before its voices are copied. */
package/dist/project.js CHANGED
@@ -1,37 +1,52 @@
1
1
  import { ownBlends } from './blend.js';
2
+ import { 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
  }
@@ -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.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';
@@ -231,8 +232,13 @@ export function open(lanes, v) {
231
232
  * A crowd a quarter empty rows is compacted.
232
233
  */
233
234
  export function retouch(lanes) {
234
- const touched = [...new Set(lanes.touched)].sort((a, b) => a.id - b.id);
235
- lanes.touched.length = 0;
235
+ const touched = byId(lanes.touched);
236
+ lanes.touched = [];
237
+ let kept = 0;
238
+ for (let i = 0; i < touched.length; i++)
239
+ if (i === 0 || touched[i] !== touched[i - 1])
240
+ touched[kept++] = touched[i];
241
+ touched.length = kept;
236
242
  // The qualify a false answer calls takes up every touched voice, crowd rows starting included.
237
243
  const qualify = () => {
238
244
  lanes.touched = touched;
package/dist/reading.d.ts CHANGED
@@ -25,4 +25,10 @@ export declare const reading: {
25
25
  * since it read it. One count for every mix, as `moved` is.
26
26
  */
27
27
  inputs: number;
28
+ /**
29
+ * While a read ahead that played its tape's calls reads: the retargets and pushes those calls
30
+ * made, by motion patch and then subject, in time order. A patch applies them with its own and
31
+ * commits none. Null for every other read.
32
+ */
33
+ ahead: Map<object, Map<unknown, unknown[]>> | null;
28
34
  };
package/dist/reading.js CHANGED
@@ -25,4 +25,10 @@ export const reading = {
25
25
  * since it read it. One count for every mix, as `moved` is.
26
26
  */
27
27
  inputs: 0,
28
+ /**
29
+ * While a read ahead that played its tape's calls reads: the retargets and pushes those calls
30
+ * made, by motion patch and then subject, in time order. A patch applies them with its own and
31
+ * commits none. Null for every other read.
32
+ */
33
+ ahead: null,
28
34
  };
package/dist/record.js CHANGED
@@ -23,6 +23,7 @@ export function record(voice, reaches, state) {
23
23
  phase: 0,
24
24
  seeks: 0,
25
25
  rebuilt: 0,
26
+ copied: false,
26
27
  base: undefined,
27
28
  slope: undefined,
28
29
  kept: null,
@@ -11,6 +11,7 @@ import type { Subject } from './voice.js';
11
11
  */
12
12
  export declare class Restored<I> extends Store<I, Subject<unknown>> {
13
13
  private readonly own;
14
+ private readonly now;
14
15
  private readonly make;
15
16
  /** Seeks back so far. */
16
17
  private n;
@@ -26,13 +27,15 @@ export declare class Restored<I> extends Store<I, Subject<unknown>> {
26
27
  /** The latest seek's moment. */
27
28
  private t;
28
29
  /**
29
- * `own` holds the records as they stood before the first seek. `make` puts a record back as it
30
- * stood at `t`, undefined where it had none then; `top` is the latest seek's moment.
30
+ * `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.
31
34
  */
32
- constructor(own: Store<I, Subject<unknown>>, make: (live: Subject<unknown>, t: number, top: number) => Subject<unknown> | undefined);
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);
33
36
  /** A seek back to `t`: `left` has the records subjects left with after `cut`, null for none. */
34
37
  back(t: number, cut: Cut, left: Leavings<I> | null): void;
35
- /** The earliest moment sought by seek `from` or any after it. */
38
+ /** The earliest moment sought by seek `from` or any after it, and the frame it ended. */
36
39
  private since;
37
40
  get(key: I): Subject<unknown> | undefined;
38
41
  set(key: I, value: Subject<unknown>): void;