@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/seek.js CHANGED
@@ -1,7 +1,9 @@
1
+ import { heldAt } from './chain.js';
1
2
  import { schedule } from './due.js';
2
- import { copyHeld, last, lastWithin } from './history.js';
3
+ import { fadesAt } from './fade.js';
4
+ import { origin } from './held.js';
5
+ import { apart, copyHeld, copyHeldInto, lastIndex, lastWithin, unread } from './history.js';
3
6
  import { laneHost } from './hosts.js';
4
- import { Lanes } from './lanes.js';
5
7
  import { Leavings } from './leavings.js';
6
8
  import { move, nextFrame } from './move.js';
7
9
  import { ownerReading } from './owner.js';
@@ -11,34 +13,112 @@ import { Restored } from './restored.js';
11
13
  import { recount } from './roster.js';
12
14
  import { refitAll } from './spans.js';
13
15
  import { Store } from './store.js';
14
- import { hostAt, replay } from './tape.js';
15
- import { cover, cut } from './unpage.js';
16
+ import { hostAhead, hostAt, replay } from './tape.js';
17
+ import { cover, coverFrame, cut } from './unpage.js';
16
18
  /**
17
19
  * A voice's record of one subject as it stood at mix time `t`, from the latest copy kept by then;
18
20
  * undefined for a stateful record first made after `t`, which is met afresh. A stateless record
19
- * holds nothing that moves with time, so it is copied whenever it was made.
21
+ * holds nothing that moves with time, so it is copied whenever it was made. The weight its last
22
+ * probe by `t` read is not kept, and a lane holds it apart from the record, so the record reads 0
23
+ * until the next probe.
20
24
  */
21
- function restore(voice, live, t) {
25
+ function restore(mix, voice, live, at, every, mine) {
26
+ // Read here, not passed: a double passed to a call is boxed, once per record put back.
27
+ const t = at.t;
22
28
  const inputs = live.inputs?.filter((e) => e.at <= t);
23
29
  const snaps = live.snaps;
24
30
  if (snaps === undefined) {
25
- // Nothing a step or `setting.keep` holds: what it keeps does not move with time.
26
- const h = copyHeld(voice, live);
31
+ // Nothing a step or `setting.keep` holds: what it keeps does not move with time. One record
32
+ // stands for every subject a voice does not reach, so that one is copied.
33
+ const h = mine && live.reaches ? unread(live) : copyHeld(voice, live);
27
34
  if (h.stepped > t)
28
35
  h.stepped = t;
29
36
  // Whether it had rested by `t` is not kept; the next probe finds it again.
30
37
  h.rested = false;
38
+ h.weight = 0;
31
39
  h.inputs = inputs;
40
+ if (!(h.since <= t))
41
+ origin(mix, voice, h);
32
42
  return h;
33
43
  }
34
- const snap = last(snaps, t, true);
35
- if (snap === undefined)
44
+ // A seek back to just before now finds its copy among the newest, looked at before searching.
45
+ const n = snaps.length;
46
+ const i = n > 0 && snaps[n - 1].at <= t
47
+ ? n - 1
48
+ : n > 1 && snaps[n - 2].at <= t
49
+ ? n - 2
50
+ : lastIndex(snaps, t, true);
51
+ if (i < 0)
36
52
  return undefined;
37
- const h = copyHeld(voice, snap.held);
38
- h.snaps = snaps.slice(0, snaps.indexOf(snap) + 1);
53
+ const snap = snaps[i];
54
+ // What the record replaced held, which a record put back over it would lose.
55
+ const { ticks, since, rebuilt, state, slot, restored } = live;
56
+ let h;
57
+ if (mine && live.reaches) {
58
+ // Its chain may still read its number from it, and its store has just stamped it.
59
+ h = copyHeldInto(voice, snap.held, live, true);
60
+ h.slot = slot;
61
+ h.restored = restored;
62
+ }
63
+ else
64
+ h = copyHeld(voice, snap.held, true);
65
+ h.weight = 0;
66
+ if (snap.at < t)
67
+ h.copied = true;
68
+ const kept = i + 1;
69
+ // The record replaced is let go, so its list is cut where it stands.
70
+ if (mine)
71
+ trim(voice, snaps, kept);
72
+ else
73
+ for (let i = 0; i < kept; i++)
74
+ snaps[i].shared = true;
75
+ h.snaps = mine ? snaps : snaps.slice(0, kept);
39
76
  h.inputs = inputs;
77
+ // A start still ahead when the copy was kept may have moved before `t`, with no copy between.
78
+ if (!(h.since <= snap.at))
79
+ origin(mix, voice, h);
80
+ // The record a seek replaces, stepped on from this copy with nothing since rebuilding or moving
81
+ // its start, holds the copy's state at its own `ticks`, which a probe there need not step to.
82
+ const was = mine && ticks > h.ticks && since === h.since && rebuilt === h.rebuilt;
83
+ if (voice.patch.step !== undefined && (snap.ahead !== undefined || t - snap.at >= every || was)) {
84
+ let ahead = snap.ahead;
85
+ // Steps counted from a start that has moved since are not this record's.
86
+ if (ahead === undefined || ahead.since !== h.since) {
87
+ ahead = {
88
+ put: h,
89
+ sought: t,
90
+ since: h.since,
91
+ at: snap.at,
92
+ ticks: h.ticks,
93
+ state: undefined,
94
+ leftTicks: 0,
95
+ left: undefined,
96
+ };
97
+ snap.ahead = ahead;
98
+ }
99
+ else {
100
+ ahead.put = h;
101
+ ahead.sought = t;
102
+ }
103
+ if (was) {
104
+ // Nothing else holds the record replaced, so its state is taken, not copied.
105
+ ahead.leftTicks = ticks;
106
+ ahead.left = state;
107
+ }
108
+ }
40
109
  return h;
41
110
  }
111
+ /** Cuts a record's list of copies to its first `kept`, keeping those no other list holds as spares. */
112
+ function trim(voice, snaps, kept) {
113
+ while (snaps.length > kept) {
114
+ const snap = snaps.pop();
115
+ if (snap.shared)
116
+ continue;
117
+ snap.ahead = undefined;
118
+ voice.spares ??= [];
119
+ voice.spares.push(snap);
120
+ }
121
+ }
42
122
  /** Takes a voice cued after the moment sought out of the mix, until the tape cues it again. */
43
123
  function park(mix, voice) {
44
124
  const owner = voice.owner;
@@ -71,9 +151,15 @@ function restoreVoice(mix, voice, t, seq) {
71
151
  voice.parts = null;
72
152
  }
73
153
  if (voice.parted !== null) {
74
- for (const [subject, p] of voice.parted)
75
- if (p.seq > seq)
76
- voice.parted.delete(subject);
154
+ for (const [subject, p] of voice.parted) {
155
+ if (p.seq <= seq)
156
+ continue;
157
+ voice.parted.delete(subject);
158
+ if (p.ramp === undefined || p.ramp.seq > seq)
159
+ continue;
160
+ voice.parts ??= new Map();
161
+ voice.parts.set(subject, p.ramp);
162
+ }
77
163
  if (voice.parted.size === 0)
78
164
  voice.parted = null;
79
165
  }
@@ -86,32 +172,31 @@ function restoreVoice(mix, voice, t, seq) {
86
172
  voice.unreached = null;
87
173
  voice.unreachedBits = null;
88
174
  voice.seeks++;
175
+ voice.spares = null;
89
176
  const left = voice.left?.all() ?? [];
177
+ if (voice.left !== null)
178
+ fadesAt(voice, voice.left.after(cut), seq);
90
179
  voice.left = Leavings.from(left.filter((e) => e.seq <= seq));
91
180
  const future = Leavings.from(left.filter((e) => e.seq > seq));
92
181
  let records = voice.subjects;
93
182
  if (!(records instanceof Restored)) {
94
- records = new Restored(records, (live, least, top) => {
95
- const h = restore(voice, live, least);
183
+ const every = apart(mix.opts.history);
184
+ const now = (subject, held, at) => heldAt(voice, subject, held, at.t, at.seq);
185
+ const restored = new Restored(records, now, (live, at, mine) => {
186
+ const h = restore(mix, voice, live, at, every, mine);
96
187
  if (h === undefined)
97
188
  return h;
98
189
  if (h.reaches)
99
190
  voice.seen++;
100
191
  if (h.rested)
101
192
  voice.restedCount++;
102
- if (!(h.since <= least)) {
103
- // Kept while its origin was still ahead: where the controls put back place it now.
104
- h.since = mix.sinceOf(voice, h.delay);
105
- h.shown = mix.shownOf(voice, h.since);
106
- if (h.ticks === 0 && h.stepped > h.since)
107
- h.stepped = h.since;
108
- }
109
- if (!(h.since <= top)) {
193
+ if (!(h.since <= restored.top)) {
110
194
  voice.early ??= [];
111
195
  voice.early.push(h);
112
196
  }
113
197
  return h;
114
198
  });
199
+ records = restored;
115
200
  voice.subjects = records;
116
201
  }
117
202
  records.back(t, cut, future);
@@ -127,8 +212,11 @@ function stateAt(voice, t) {
127
212
  return 'frozen';
128
213
  return 'live';
129
214
  }
130
- /** Moves every mix on the transport to mix time `t`, as it stood at the end of that frame. */
131
- export function seek(transport, t) {
215
+ /**
216
+ * Moves every mix on the transport to mix time `to`, as it stood at the end of that frame, or to
217
+ * the end of the frame numbered `to.frame`.
218
+ */
219
+ export function seek(transport, to) {
132
220
  if (transport.projecting)
133
221
  throw new Error('blits: a projection does not seek');
134
222
  const history = transport.history;
@@ -139,25 +227,42 @@ export function seek(transport, t) {
139
227
  throw new Error('blits: seeking needs history.tape, which keeps the calls a seek plays again');
140
228
  if (Number.isNaN(transport.now))
141
229
  throw new Error('blits: a mix that has never synced has nothing to seek');
142
- if (t === Number.POSITIVE_INFINITY)
143
- throw new RangeError('blits: a mix seeks to a finite time');
144
- if (!(t >= transport.born))
145
- throw new Error(`blits: ${t} is older than this mix's history reaches`);
146
- cover(transport, t);
147
- if (t >= transport.now) {
230
+ let t;
231
+ let frame;
232
+ if (typeof to === 'number') {
233
+ t = to;
234
+ if (t === Number.POSITIVE_INFINITY)
235
+ throw new RangeError('blits: a mix seeks to a finite time');
236
+ if (!(t >= transport.born))
237
+ throw new Error(`blits: ${t} is older than this mix's history reaches`);
238
+ cover(transport, t);
239
+ if (t < transport.now) {
240
+ frame = transport.frames.at(t);
241
+ if (frame === undefined)
242
+ throw new Error(`blits: ${t} is older than this mix's history reaches`);
243
+ }
244
+ }
245
+ else {
246
+ const f = coverFrame(transport, to.frame);
247
+ t = f.at;
248
+ // The frame being played is where the mix stands; any other is behind it, whatever its time.
249
+ if (f.seq !== transport.seq)
250
+ frame = f;
251
+ }
252
+ if (frame === undefined) {
148
253
  // Ahead, under the rate the recorded calls up to `t` set, past any frames rate 0 held at `t`.
149
254
  const last = replay(transport, (at) => (transport.pace === null ? at : transport.pace.reading(at)) <= t);
150
- const u = Number.isNaN(last) ? hostAt(transport, t) : Math.max(last, hostAt(transport, t));
255
+ const at = hostAhead(transport, t);
256
+ const u = Number.isNaN(last) ? at : Math.max(last, at);
151
257
  if (!Number.isFinite(u))
152
258
  throw new RangeError(`blits: the mix's rate never reaches ${t}`);
153
259
  transport.u = u;
154
- for (const m of transport.members)
155
- move(m, t);
260
+ // A call played at `t` itself moved the mixes there before it, as a sync does.
261
+ if (Number.isNaN(last) || transport.now !== t)
262
+ for (const m of transport.members)
263
+ move(m, t);
156
264
  }
157
265
  else {
158
- const frame = transport.frames.at(t);
159
- if (frame === undefined)
160
- throw new Error(`blits: ${t} is older than this mix's history reaches`);
161
266
  // On the frame itself, its own host time: rate 0 may have held the clock there for several.
162
267
  const u = frame.at === t ? frame.u : hostAt(transport, t);
163
268
  const seq = frame.seq;
@@ -219,13 +324,17 @@ function back(mix, t, seq, on) {
219
324
  mix.retired = [];
220
325
  // Owners hold what is back; a voice that went live again counts its place in its owner anew.
221
326
  for (const voice of kept)
222
- if (voice.holding !== null)
327
+ if (voice.holding !== null) {
223
328
  voice.holding.children = kept.filter((v) => v.owner === voice);
329
+ voice.holding.back(seq);
330
+ }
224
331
  // Rebuilt from the voices that are back: the lanes, the chains and every count the fold reads.
332
+ // Subject numbers carry over, and with them the chains that hold them, each linked afresh.
225
333
  const oldLanes = mix.lanes;
226
- if (oldLanes !== null)
227
- mix.lanes = new Lanes(laneHost(mix));
228
- mix.chains = new Store();
334
+ if (oldLanes !== null && mix.lanesOf !== null)
335
+ mix.lanes = new mix.lanesOf(laneHost(mix), oldLanes);
336
+ else
337
+ mix.chains = new Store();
229
338
  mix.owners = kept.some((v) => v.holding !== null) ? kept.filter((v) => v.holding !== null) : null;
230
339
  recount(mix);
231
340
  mix.parters.clear();
package/dist/shared.d.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * The package version, for a fact whose shape follows the code too closely to version by hand:
3
+ * copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
4
+ */
5
+ export declare const release = "0.12.0";
1
6
  /**
2
7
  * A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
3
8
  * patch or channel it made is read by another copy's mix. The name carries a version, bumped when
package/dist/shared.js CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * The package version, for a fact whose shape follows the code too closely to version by hand:
3
+ * copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
4
+ */
5
+ export const release = '0.12.0';
1
6
  /**
2
7
  * A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
3
8
  * patch or channel it made is read by another copy's mix. The name carries a version, bumped when
package/dist/signals.d.ts CHANGED
@@ -7,6 +7,16 @@ export declare const heard: (signal: object) => boolean;
7
7
  * @category signal
8
8
  */
9
9
  export declare function peak<I, H = unknown>(...signals: readonly Signal<I, H>[]): Signal<I, H>;
10
+ /** A blend member's share of its signal's read `k`: the members' stops sit evenly along 0..1. */
11
+ export declare function shareOf(k: number, i: number, stops: number): number;
12
+ /**
13
+ * Member `i` of `n`'s share of `by`, as `mix.blend` weighs it: the hat of `by`'s read at the
14
+ * member's stop, the stops spaced evenly from 0 to 1. Weights a voice outside a blend as one of
15
+ * its members, such as one member played alone.
16
+ *
17
+ * @category signal
18
+ */
19
+ export declare function share<I, H = unknown>(by: Signal<I, H>, i: number, n: number): Signal<I, H>;
10
20
  /**
11
21
  * Marks `read` as a signal driven from outside the clock, which a host's own signal has to be for
12
22
  * `assess` and a history's `inputs` to treat it as one. Two uses:
package/dist/signals.js CHANGED
@@ -27,6 +27,23 @@ export function peak(...signals) {
27
27
  return out;
28
28
  }, signals);
29
29
  }
30
+ /** A blend member's share of its signal's read `k`: the members' stops sit evenly along 0..1. */
31
+ export function shareOf(k, i, stops) {
32
+ const d = stops === 0 ? 0 : Math.abs(k * stops - i);
33
+ return d >= 1 ? 0 : 1 - d;
34
+ }
35
+ /**
36
+ * Member `i` of `n`'s share of `by`, as `mix.blend` weighs it: the hat of `by`'s read at the
37
+ * member's stop, the stops spaced evenly from 0 to 1. Weights a voice outside a blend as one of
38
+ * its members, such as one member played alone.
39
+ *
40
+ * @category signal
41
+ */
42
+ export function share(by, i, n) {
43
+ if (!Number.isInteger(n) || n < 1 || !Number.isInteger(i) || i < 0 || i >= n)
44
+ throw new Error(`blits: share wants a member 0 to ${n - 1}, not ${i}`);
45
+ return marked((subject, setting) => shareOf(by(subject, setting), i, n - 1), [by]);
46
+ }
30
47
  const touch = () => {
31
48
  reading.inputs++;
32
49
  };
package/dist/spans.d.ts CHANGED
@@ -22,6 +22,11 @@ export declare function checkChild<I, O>(spec: VoiceSpec<I, O>, child: Voice<I,
22
22
  * after.
23
23
  */
24
24
  export declare function refit<I, O>(mix: Mixer<I, O>, span: Voice<I, O>, from?: number): void;
25
+ /**
26
+ * Fits again each span with a child still to start whose anchored `earliest` answers differently
27
+ * than at its last fit: an anchor that has come to have an answer, or moved.
28
+ */
29
+ export declare function refloor<I, O>(mix: Mixer<I, O>): void;
25
30
  /** Whether a span whose last child just left at mix time `at` stays, its budget not yet out. */
26
31
  export declare function lingers<I, O>(span: Voice<I, O>, at: number): boolean;
27
32
  /** Lets the spans that are empty and out of budget leave, at the moment their budget ran out. */
package/dist/spans.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { arcEnd } from './arcs.js';
1
2
  import { elapsedWith, retime } from './clock.js';
2
3
  import { retire } from './fade.js';
3
4
  import { defaultFit, layout, settle } from './fit.js';
@@ -6,6 +7,7 @@ import { noted } from './history.js';
6
7
  import { nextFrame } from './move.js';
7
8
  import { leave } from './origin.js';
8
9
  import { ownerReading } from './owner.js';
10
+ import { resolve } from './place.js';
9
11
  /** What a span keeps beside its owner: its claim, its fit, and how the last fit came out. */
10
12
  export class Fitting {
11
13
  budget;
@@ -39,9 +41,13 @@ export class Fitting {
39
41
  export function checkChild(spec, child) {
40
42
  if (spec.start !== undefined || spec.anchor?.start !== undefined || spec.anchor?.in !== undefined)
41
43
  throw new Error('blits: a span places the voices it holds, so one takes no start of its own');
42
- if (child.holding !== null) {
44
+ if (child.arc !== null) {
45
+ if (!Number.isFinite(arcEnd(child)))
46
+ throw new Error('blits: a span cannot fit an arc held open, which never ends');
47
+ }
48
+ else if (child.holding !== null) {
43
49
  if (child.fitting === null)
44
- throw new Error('blits: a span holds voices and spans, not owners');
50
+ throw new Error('blits: a span holds voices, spans and arcs, not owners');
45
51
  }
46
52
  else if (!Number.isFinite(child.span))
47
53
  throw new Error('blits: a span cannot fit a voice that never ends');
@@ -59,7 +65,7 @@ function elapsedIn(child, local) {
59
65
  }
60
66
  /** How much of a child is left, voice ms at its own clock: what its span still has to fit. */
61
67
  function leftOf(child, local) {
62
- const length = child.fitting !== null ? child.fitting.end : child.span;
68
+ const length = child.fitting !== null ? child.fitting.end : child.arc !== null ? arcEnd(child) : child.span;
63
69
  return Math.max(0, length - elapsedIn(child, local));
64
70
  }
65
71
  /**
@@ -73,10 +79,11 @@ export function refit(mix, span, from = mix.now) {
73
79
  if (mix.projecting || span.state === 'done')
74
80
  return;
75
81
  const local = clockOf(span, from);
76
- const kids = (span.holding?.children ?? []).filter((c) => c.state !== 'done' && c.state !== 'frozen' && c.state !== 'fading' && leftOf(c, local) > 0);
77
- const claims = kids.map((c) => {
82
+ const { kids, floors, waiting } = lineUp(mix, span, local);
83
+ const claims = kids.map((c, i) => {
78
84
  const s = c.spec;
79
85
  return {
86
+ floor: floors[i],
80
87
  natural: leftOf(c, local) / (s.rate ?? 1),
81
88
  state: c.state === 'pending' ? 'pending' : 'playing',
82
89
  faster: Math.max(1, s.faster ?? 1),
@@ -127,6 +134,17 @@ export function refit(mix, span, from = mix.now) {
127
134
  noted(mix, c);
128
135
  }
129
136
  });
137
+ for (const c of waiting) {
138
+ if (c.start === Number.POSITIVE_INFINITY)
139
+ continue;
140
+ c.start = Number.POSITIVE_INFINITY;
141
+ c.anchorNow = Number.POSITIVE_INFINITY;
142
+ c.anchorElapsed = 0;
143
+ c.clocks = null;
144
+ mix.frame = nextFrame();
145
+ reorigin(mix, c, mix.now);
146
+ noted(mix, c);
147
+ }
130
148
  if (kids.length > 0)
131
149
  mix.lanes?.refill();
132
150
  const reach = local + length;
@@ -147,6 +165,58 @@ export function refit(mix, span, from = mix.now) {
147
165
  refit(mix, span.owner, from);
148
166
  }
149
167
  }
168
+ /**
169
+ * A span's children still to fit, in the order it lays them out: those playing first, as cued, then
170
+ * those still to start by rank. Each pending one's floor is where its `earliest` answers, span ms
171
+ * from `local`, and before `local` only in the frame it was cued. One whose anchor has no answer yet waits unplaced, and under `queue` or `stagger`
172
+ * so does every child after it, since each is laid out from the one before.
173
+ */
174
+ function lineUp(mix, span, local) {
175
+ const live = (span.holding?.children ?? []).filter((c) => c.state !== 'done' && c.state !== 'frozen' && c.state !== 'fading' && leftOf(c, local) > 0);
176
+ const kids = live.filter((c) => c.state !== 'pending');
177
+ const floors = kids.map(() => undefined);
178
+ const waiting = [];
179
+ const pending = live
180
+ .filter((c) => c.state === 'pending')
181
+ .sort((a, b) => (a.spec.rank ?? 0) - (b.spec.rank ?? 0));
182
+ const chained = span.fitting.claim.order !== 'together';
183
+ for (const c of pending) {
184
+ const e = c.spec.earliest;
185
+ const t = e === undefined ? undefined : resolve(mix, e, c, Number.NEGATIVE_INFINITY);
186
+ c.floorSeen = t ?? Number.NaN;
187
+ if ((e !== undefined && t === undefined) || (chained && waiting.length > 0)) {
188
+ waiting.push(c);
189
+ continue;
190
+ }
191
+ kids.push(c);
192
+ // An anchor that answered before the span began has no time on its clock: no bound.
193
+ const floor = t === undefined || t === Number.NEGATIVE_INFINITY ? undefined : t - local;
194
+ // A past floor places a child only as it is cued: one waiting since waits on those before it.
195
+ floors.push(floor === undefined || c.cuedSeq === mix.transport.seq ? floor : Math.max(0, floor));
196
+ }
197
+ return { kids, floors, waiting };
198
+ }
199
+ /**
200
+ * Fits again each span with a child still to start whose anchored `earliest` answers differently
201
+ * than at its last fit: an anchor that has come to have an answer, or moved.
202
+ */
203
+ export function refloor(mix) {
204
+ if (mix.projecting)
205
+ return;
206
+ for (const span of mix.owners ?? []) {
207
+ if (span.fitting === null || span.state === 'done')
208
+ continue;
209
+ const moved = (span.holding?.children ?? []).some((c) => {
210
+ const e = c.spec.earliest;
211
+ if (c.state !== 'pending' || e === undefined || typeof e === 'number')
212
+ return false;
213
+ const t = resolve(mix, e, c, Number.NEGATIVE_INFINITY) ?? Number.NaN;
214
+ return !Object.is(t, c.floorSeen);
215
+ });
216
+ if (moved)
217
+ refit(mix, span);
218
+ }
219
+ }
150
220
  /** Whether a span whose last child just left at mix time `at` stays, its budget not yet out. */
151
221
  export function lingers(span, at) {
152
222
  const f = span.fitting;
package/dist/store.d.ts CHANGED
@@ -16,7 +16,20 @@ export declare class Store<K, V> {
16
16
  private oneValue;
17
17
  get(key: K): V | undefined;
18
18
  set(key: K, value: V): void;
19
+ /** As `get`, where `guess` may be the value for `key`, which a store that can tell spares a lookup. */
20
+ find(key: K, _guess: V): V | undefined;
19
21
  has(key: K): boolean;
20
22
  delete(key: K): void;
21
23
  clear(): void;
22
24
  }
25
+ /**
26
+ * A store over another, for a copy that must not write the one it reads: what is set or deleted
27
+ * here stays here, and a key deleted here is no longer read from the one under it.
28
+ */
29
+ export declare class Masked<K, V> extends Store<K, V> {
30
+ private readonly under;
31
+ private readonly hidden;
32
+ constructor(under: Store<K, V>);
33
+ get(key: K): V | undefined;
34
+ delete(key: K): void;
35
+ }
package/dist/store.js CHANGED
@@ -43,6 +43,10 @@ export class Store {
43
43
  }
44
44
  this.strong.set(key, value);
45
45
  }
46
+ /** As `get`, where `guess` may be the value for `key`, which a store that can tell spares a lookup. */
47
+ find(key, _guess) {
48
+ return this.get(key);
49
+ }
46
50
  has(key) {
47
51
  return this.get(key) !== undefined;
48
52
  }
@@ -62,6 +66,25 @@ export class Store {
62
66
  this.strong.clear();
63
67
  }
64
68
  }
69
+ /**
70
+ * A store over another, for a copy that must not write the one it reads: what is set or deleted
71
+ * here stays here, and a key deleted here is no longer read from the one under it.
72
+ */
73
+ export class Masked extends Store {
74
+ under;
75
+ hidden = new Store();
76
+ constructor(under) {
77
+ super();
78
+ this.under = under;
79
+ }
80
+ get(key) {
81
+ return super.get(key) ?? (this.hidden.get(key) ? undefined : this.under.get(key));
82
+ }
83
+ delete(key) {
84
+ super.delete(key);
85
+ this.hidden.set(key, true);
86
+ }
87
+ }
65
88
  /** No key held inline. */
66
89
  const none = Symbol('none');
67
90
  /** A Map's key equality, SameValueZero: NaN finds NaN. */
package/dist/strays.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { unpart } from './fade.js';
2
2
  import { pageOut } from './paging.js';
3
- import { record } from './tape.js';
3
+ import { record, unplayable } from './tape.js';
4
4
  /** Notes a motion patch that may hold state for a subject no voice playing it reaches. */
5
5
  export function stray(mix, motion, subject) {
6
6
  const list = mix.strays.get(subject);
@@ -49,7 +49,12 @@ export function motionOwner(mix) {
49
49
  const m = mix.deref();
50
50
  if (m === undefined)
51
51
  return;
52
- record(m, 'motion', () => {
52
+ record(m, 'motion', (stage) => {
53
+ const to = stage.mix(m);
54
+ if (to === undefined)
55
+ return;
56
+ if (to !== m)
57
+ return changeAhead(to, id, motion, subject, change);
53
58
  const v = cuedById(m, id) ?? m.gone.find((g) => g.id === id);
54
59
  (v?.motion ?? motion).change(subject, { ...change });
55
60
  });
@@ -79,6 +84,49 @@ export function motionOwner(mix) {
79
84
  },
80
85
  };
81
86
  }
87
+ /**
88
+ * A retarget or push the tape holds, made for a read ahead: kept on its transport for the patch
89
+ * to apply as it reads, since the patch's own state is the live mix's.
90
+ */
91
+ function changeAhead(to, id, motion, given, change) {
92
+ const subject = given;
93
+ const v = cuedById(to, id);
94
+ let at = change.at;
95
+ if (v !== undefined) {
96
+ // As the patch asks its mix live: a subject faded out of the voice is back, and a change with
97
+ // no time takes the voice's at this frame, or the subject's own where it is yet to be met.
98
+ const parted = v.parted?.has(subject) === true;
99
+ if (parted)
100
+ unpart(to, v, subject);
101
+ if (at === undefined) {
102
+ const frame = frameOf(to, v, subject);
103
+ const back = parted ? timeIn(to, v, v.spec.stagger ? v.spec.stagger(subject) : 0) : frame;
104
+ if (!Number.isNaN(frame))
105
+ at = frame;
106
+ else if (!Number.isNaN(back))
107
+ at = back;
108
+ }
109
+ }
110
+ if (at === undefined)
111
+ unplayable('a change to a motion made before its voice met the subject');
112
+ const stamped = at;
113
+ const made = to.transport.ahead;
114
+ let bySubject = made.get(motion);
115
+ if (bySubject === undefined) {
116
+ bySubject = new Map();
117
+ made.set(motion, bySubject);
118
+ }
119
+ const list = bySubject.get(subject);
120
+ const c = { at, to: change.to, v: change.v };
121
+ if (list === undefined)
122
+ bySubject.set(subject, [c]);
123
+ else {
124
+ let i = list.length;
125
+ while (i > 0 && list[i - 1].at > stamped)
126
+ i--;
127
+ list.splice(i, 0, c);
128
+ }
129
+ }
82
130
  /** The voice in the list with this id, which is in id order. */
83
131
  export function cuedById(mix, id) {
84
132
  const cued = mix.cued;
package/dist/tape.d.ts CHANGED
@@ -1,24 +1,49 @@
1
+ import type { Mixer } from './mixer.js';
1
2
  import type { Pace } from './pace.js';
2
3
  import type { Transport } from './transport.js';
3
4
  import type { Tape } from './types.js';
5
+ /**
6
+ * Where a recorded call is made again: on the mixes the host made it on, or, for a read ahead, on
7
+ * its copies of them.
8
+ */
9
+ export interface Stage {
10
+ transport(of: Transport): Transport;
11
+ /** The mix standing for `of`; undefined where a read ahead has no copy of it. */
12
+ mix<I, O>(of: Mixer<I, O>): Mixer<I, O> | undefined;
13
+ }
14
+ /** Refuses a recorded call a read ahead's copies cannot take. */
15
+ export declare function unplayable(what: string): never;
4
16
  /**
5
17
  * The tape `history.tape` makes for a transport, stamping each call with the host time it was made
6
18
  * at: rate 0 holds mix time still across frames, and host time tells them apart.
7
19
  */
8
20
  export declare function tapeOf(transport: Transport): Tape | undefined;
9
- /** Records a host call, which `again` makes once more when the mix plays past it after a seek. */
21
+ /**
22
+ * Records a host call, which `again` makes once more when the mix plays past it after a seek, or
23
+ * when a read ahead does, on the stage it is given.
24
+ */
10
25
  export declare function record(on: {
11
26
  readonly tape: Tape | undefined;
12
27
  readonly replaying: boolean;
13
28
  readonly projecting: boolean;
14
- }, label: string, again: () => void): void;
29
+ }, label: string, again: (stage: Stage) => void): void;
15
30
  /** The host time at which the mix clock reads `t`, under the rate it has now. */
16
31
  export declare function hostAt(clock: {
17
32
  readonly pace: Pace | null;
18
33
  }, t: number): number;
34
+ /**
35
+ * The host time a move ahead to mix time `t` lands at: never behind the clock's own, since rate 0
36
+ * reads `t` at every host time from the earliest `hostAt` gives, and a call stamped earlier than
37
+ * one already on the tape would cut it out of order.
38
+ */
39
+ export declare function hostAhead(clock: {
40
+ readonly pace: Pace | null;
41
+ readonly u: number;
42
+ }, t: number): number;
19
43
  /**
20
44
  * Makes the recorded calls `due` by the host time each was made at again, each host time's in a
21
45
  * frame of its own with every mix on the transport moved to where it stood then, as the host made
22
- * them. Answers the host time of the last frame it played, NaN for none.
46
+ * them. Answers the host time of the last frame it played, NaN for none. Given `onto` and the
47
+ * stage of its copies, it makes them on those, and the caller puts the tape back.
23
48
  */
24
- export declare function replay(transport: Transport, due: (u: number) => boolean): number;
49
+ export declare function replay(transport: Transport, due: (u: number) => boolean, onto?: Transport, on?: Stage): number;