@msb235/blits 0.9.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 (95) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/README.md +16 -5
  3. package/dist/blend.js +1 -5
  4. package/dist/book.d.ts +8 -5
  5. package/dist/book.js +47 -19
  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.d.ts +1 -1
  32. package/dist/hosts.js +18 -6
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.js +1 -1
  35. package/dist/keyfill.js +3 -1
  36. package/dist/lane.d.ts +1 -1
  37. package/dist/lane.js +3 -1
  38. package/dist/lanes.d.ts +8 -0
  39. package/dist/lanes.js +21 -4
  40. package/dist/lean.d.ts +15 -0
  41. package/dist/lean.js +23 -0
  42. package/dist/leavings.d.ts +6 -1
  43. package/dist/leavings.js +16 -2
  44. package/dist/marks.d.ts +7 -3
  45. package/dist/marks.js +102 -27
  46. package/dist/meet.js +1 -0
  47. package/dist/mixer.d.ts +23 -7
  48. package/dist/mixer.js +73 -29
  49. package/dist/motion.d.ts +4 -1
  50. package/dist/motion.js +16 -2
  51. package/dist/motions.d.ts +4 -0
  52. package/dist/motions.js +45 -11
  53. package/dist/move.js +28 -10
  54. package/dist/owner.d.ts +18 -2
  55. package/dist/owner.js +38 -2
  56. package/dist/pack.d.ts +1 -0
  57. package/dist/pack.js +3 -0
  58. package/dist/placed.d.ts +19 -0
  59. package/dist/placed.js +54 -0
  60. package/dist/project.d.ts +5 -2
  61. package/dist/project.js +236 -32
  62. package/dist/pull.js +37 -8
  63. package/dist/qualify.js +8 -2
  64. package/dist/reading.d.ts +6 -0
  65. package/dist/reading.js +6 -0
  66. package/dist/record.js +1 -0
  67. package/dist/restored.d.ts +7 -4
  68. package/dist/restored.js +13 -10
  69. package/dist/revive.d.ts +5 -3
  70. package/dist/revive.js +10 -3
  71. package/dist/rows.js +15 -5
  72. package/dist/seek.d.ts +7 -2
  73. package/dist/seek.js +90 -35
  74. package/dist/shared.d.ts +5 -0
  75. package/dist/shared.js +5 -0
  76. package/dist/signals.d.ts +10 -0
  77. package/dist/signals.js +17 -0
  78. package/dist/store.d.ts +11 -0
  79. package/dist/store.js +19 -0
  80. package/dist/strays.js +50 -2
  81. package/dist/tape.d.ts +29 -4
  82. package/dist/tape.js +49 -11
  83. package/dist/transport.d.ts +11 -2
  84. package/dist/transport.js +20 -9
  85. package/dist/types/mix.d.ts +42 -15
  86. package/dist/types/patch.d.ts +5 -1
  87. package/dist/types/place.d.ts +25 -11
  88. package/dist/types/transport.d.ts +12 -3
  89. package/dist/types/voice.d.ts +62 -4
  90. package/dist/unpage.d.ts +6 -1
  91. package/dist/unpage.js +34 -2
  92. package/dist/voice.d.ts +57 -11
  93. package/dist/voice.js +43 -3
  94. package/docs/schema.html +75 -40
  95. package/package.json +7 -2
package/dist/revive.js CHANGED
@@ -107,9 +107,11 @@ function packVoice(voice, keys) {
107
107
  e.seq,
108
108
  packHeld(voice, e.held),
109
109
  e.sync,
110
+ e.ramp,
111
+ e.parted,
110
112
  ]),
111
113
  parts: [...(voice.parts ?? [])].map(([s, r]) => [keys.key(s), r.at, r.over, r.seq]),
112
- parted: [...(voice.parted ?? [])].map(([s, p]) => [keys.key(s), p.at, p.seq]),
114
+ parted: [...(voice.parted ?? [])].map(([s, p]) => p.ramp === undefined ? [keys.key(s), p.at, p.seq] : [keys.key(s), p.at, p.seq, p.ramp]),
113
115
  };
114
116
  }
115
117
  /** A voice a history store paged out, rebuilt from its descriptor and put back as it left. */
@@ -146,17 +148,22 @@ export function reviveVoice(mix, id, d) {
146
148
  }
147
149
  for (const [key, run] of d.runs)
148
150
  voice.motion?.unpack(subjectOf(key), run);
149
- voice.left = Leavings.from(d.left.map(([key, at, seq, p, sync]) => ({
151
+ voice.left = Leavings.from(d.left.map(([key, at, seq, p, sync, ramp = null, parted = null]) => ({
150
152
  subject: subjectOf(key),
151
153
  at,
152
154
  seq,
153
155
  sync,
154
156
  held: unpackHeld(voice, p),
157
+ ramp,
158
+ parted,
155
159
  })));
156
160
  if (d.parts.length > 0)
157
161
  voice.parts = new Map(d.parts.map(([key, at, over, seq]) => [subjectOf(key), { at, over, seq }]));
158
162
  if (d.parted.length > 0)
159
- voice.parted = new Map(d.parted.map(([key, at, seq]) => [subjectOf(key), { at, seq }]));
163
+ voice.parted = new Map(d.parted.map(([key, at, seq, ramp]) => [
164
+ subjectOf(key),
165
+ { at, seq, ramp: ramp ?? undefined },
166
+ ]));
160
167
  const h = mix.outs?.take(id);
161
168
  if (h === undefined)
162
169
  voice.handle = handle(mix, voice);
package/dist/rows.js CHANGED
@@ -16,8 +16,11 @@ function same(a, b) {
16
16
  /** Takes a crowd row off its subject, to be placed again if a probe meets it again. */
17
17
  export function unplace(lanes, c, p) {
18
18
  const slot = c.list[p];
19
- if (c.idle && slot >= 0)
20
- reach(lanes, slot, -1);
19
+ if (slot >= 0) {
20
+ if (c.idle)
21
+ reach(lanes, slot, -1);
22
+ lanes.placed.remove(slot, c.voices[p].id);
23
+ }
21
24
  c.list[p] = -1;
22
25
  c.records[p] = undefined;
23
26
  c.deltas[p] = null;
@@ -167,15 +170,20 @@ export function bury(lanes, c, p) {
167
170
  lanes.crowdOf.delete(v.id);
168
171
  c.dead++;
169
172
  }
173
+ /** Copies `n` numbers of a row; a view of them to hand `set` was three objects a row moved. */
174
+ function carry(from, a, to, b, n) {
175
+ for (let i = 0; i < n; i++)
176
+ to[b + i] = from[a + i];
177
+ }
170
178
  function copyRow(from, p, to, q, keys = from.keys) {
171
179
  from.fix(p);
172
180
  to.list[q] = from.list[p];
173
- to.data.set(from.data.subarray(p * 8 /* Row.STRIDE */, (p + 1) * 8 /* Row.STRIDE */), q * 8 /* Row.STRIDE */);
174
- to.hot.set(from.hot.subarray(p * from.stride, (p + 1) * from.stride), q * to.stride);
181
+ carry(from.data, p * 8 /* Row.STRIDE */, to.data, q * 8 /* Row.STRIDE */, 8 /* Row.STRIDE */);
182
+ carry(from.hot, p * from.stride, to.hot, q * to.stride, from.stride);
175
183
  copyKeys(keys, to, q);
176
184
  if ((from.hot[p * from.stride + 0 /* Hot.FLAGS */] & 4 /* Flag.STALE */) !== 0)
177
185
  to.stale = true;
178
- to.samples.set(from.samples.subarray(p * from.axes, (p + 1) * from.axes), q * to.axes);
186
+ carry(from.samples, p * from.axes, to.samples, q * to.axes, from.axes);
179
187
  to.deltas[q] = from.deltas[p] ?? null;
180
188
  to.records[q] = from.records[p];
181
189
  to.voices[q] = from.voices[p];
@@ -257,6 +265,8 @@ function leaveCrowd(lanes, c, p) {
257
265
  const slot = c.list[p];
258
266
  const rec = c.records[p];
259
267
  if (slot >= 0) {
268
+ // Its row is emptied or left behind with its crowd, so the subject has it placed no longer.
269
+ lanes.placed.remove(slot, c.voices[p].id);
260
270
  if (c.idle)
261
271
  reach(lanes, slot, -1);
262
272
  const w = reportedRow(lanes, c, p, slot);
package/dist/seek.d.ts CHANGED
@@ -1,3 +1,8 @@
1
1
  import type { Transport } from './transport.js';
2
- /** Moves every mix on the transport to mix time `t`, as it stood at the end of that frame. */
3
- export declare function seek(transport: Transport, t: number): void;
2
+ /**
3
+ * Moves every mix on the transport to mix time `to`, as it stood at the end of that frame, or to
4
+ * the end of the frame numbered `to.frame`.
5
+ */
6
+ export declare function seek(transport: Transport, to: number | {
7
+ readonly frame: number;
8
+ }): void;
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, last, lastWithin } 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,14 +13,16 @@ 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, t, every) {
22
26
  const inputs = live.inputs?.filter((e) => e.at <= t);
23
27
  const snaps = live.snaps;
24
28
  if (snaps === undefined) {
@@ -28,15 +32,41 @@ function restore(voice, live, t) {
28
32
  h.stepped = t;
29
33
  // Whether it had rested by `t` is not kept; the next probe finds it again.
30
34
  h.rested = false;
35
+ h.weight = 0;
31
36
  h.inputs = inputs;
37
+ if (!(h.since <= t))
38
+ origin(mix, voice, h);
32
39
  return h;
33
40
  }
34
41
  const snap = last(snaps, t, true);
35
42
  if (snap === undefined)
36
43
  return undefined;
37
44
  const h = copyHeld(voice, snap.held);
45
+ h.weight = 0;
46
+ if (snap.at < t)
47
+ h.copied = true;
38
48
  h.snaps = snaps.slice(0, snaps.indexOf(snap) + 1);
39
49
  h.inputs = inputs;
50
+ // A start still ahead when the copy was kept may have moved before `t`, with no copy between.
51
+ if (!(h.since <= snap.at))
52
+ origin(mix, voice, h);
53
+ if (voice.patch.step !== undefined && (snap.ahead !== undefined || t - snap.at >= every)) {
54
+ const ahead = snap.ahead;
55
+ // Steps counted from a start that has moved since are not this record's.
56
+ if (ahead === undefined || ahead.since !== h.since)
57
+ snap.ahead = {
58
+ put: h,
59
+ sought: t,
60
+ since: h.since,
61
+ at: snap.at,
62
+ ticks: h.ticks,
63
+ state: undefined,
64
+ };
65
+ else {
66
+ ahead.put = h;
67
+ ahead.sought = t;
68
+ }
69
+ }
40
70
  return h;
41
71
  }
42
72
  /** Takes a voice cued after the moment sought out of the mix, until the tape cues it again. */
@@ -71,9 +101,15 @@ function restoreVoice(mix, voice, t, seq) {
71
101
  voice.parts = null;
72
102
  }
73
103
  if (voice.parted !== null) {
74
- for (const [subject, p] of voice.parted)
75
- if (p.seq > seq)
76
- voice.parted.delete(subject);
104
+ for (const [subject, p] of voice.parted) {
105
+ if (p.seq <= seq)
106
+ continue;
107
+ voice.parted.delete(subject);
108
+ if (p.ramp === undefined || p.ramp.seq > seq)
109
+ continue;
110
+ voice.parts ??= new Map();
111
+ voice.parts.set(subject, p.ramp);
112
+ }
77
113
  if (voice.parted.size === 0)
78
114
  voice.parted = null;
79
115
  }
@@ -87,25 +123,22 @@ function restoreVoice(mix, voice, t, seq) {
87
123
  voice.unreachedBits = null;
88
124
  voice.seeks++;
89
125
  const left = voice.left?.all() ?? [];
126
+ if (voice.left !== null)
127
+ fadesAt(voice, voice.left.after(cut), seq);
90
128
  voice.left = Leavings.from(left.filter((e) => e.seq <= seq));
91
129
  const future = Leavings.from(left.filter((e) => e.seq > seq));
92
130
  let records = voice.subjects;
93
131
  if (!(records instanceof Restored)) {
94
- records = new Restored(records, (live, least, top) => {
95
- const h = restore(voice, live, least);
132
+ const every = apart(mix.opts.history);
133
+ const now = (subject, held, least, seq) => heldAt(voice, subject, held, least, seq);
134
+ records = new Restored(records, now, (live, least, top) => {
135
+ const h = restore(mix, voice, live, least, every);
96
136
  if (h === undefined)
97
137
  return h;
98
138
  if (h.reaches)
99
139
  voice.seen++;
100
140
  if (h.rested)
101
141
  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
142
  if (!(h.since <= top)) {
110
143
  voice.early ??= [];
111
144
  voice.early.push(h);
@@ -127,8 +160,11 @@ function stateAt(voice, t) {
127
160
  return 'frozen';
128
161
  return 'live';
129
162
  }
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) {
163
+ /**
164
+ * Moves every mix on the transport to mix time `to`, as it stood at the end of that frame, or to
165
+ * the end of the frame numbered `to.frame`.
166
+ */
167
+ export function seek(transport, to) {
132
168
  if (transport.projecting)
133
169
  throw new Error('blits: a projection does not seek');
134
170
  const history = transport.history;
@@ -139,25 +175,42 @@ export function seek(transport, t) {
139
175
  throw new Error('blits: seeking needs history.tape, which keeps the calls a seek plays again');
140
176
  if (Number.isNaN(transport.now))
141
177
  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) {
178
+ let t;
179
+ let frame;
180
+ if (typeof to === 'number') {
181
+ t = to;
182
+ if (t === Number.POSITIVE_INFINITY)
183
+ throw new RangeError('blits: a mix seeks to a finite time');
184
+ if (!(t >= transport.born))
185
+ throw new Error(`blits: ${t} is older than this mix's history reaches`);
186
+ cover(transport, t);
187
+ if (t < transport.now) {
188
+ frame = transport.frames.at(t);
189
+ if (frame === undefined)
190
+ throw new Error(`blits: ${t} is older than this mix's history reaches`);
191
+ }
192
+ }
193
+ else {
194
+ const f = coverFrame(transport, to.frame);
195
+ t = f.at;
196
+ // The frame being played is where the mix stands; any other is behind it, whatever its time.
197
+ if (f.seq !== transport.seq)
198
+ frame = f;
199
+ }
200
+ if (frame === undefined) {
148
201
  // Ahead, under the rate the recorded calls up to `t` set, past any frames rate 0 held at `t`.
149
202
  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));
203
+ const at = hostAhead(transport, t);
204
+ const u = Number.isNaN(last) ? at : Math.max(last, at);
151
205
  if (!Number.isFinite(u))
152
206
  throw new RangeError(`blits: the mix's rate never reaches ${t}`);
153
207
  transport.u = u;
154
- for (const m of transport.members)
155
- move(m, t);
208
+ // A call played at `t` itself moved the mixes there before it, as a sync does.
209
+ if (Number.isNaN(last) || transport.now !== t)
210
+ for (const m of transport.members)
211
+ move(m, t);
156
212
  }
157
213
  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
214
  // On the frame itself, its own host time: rate 0 may have held the clock there for several.
162
215
  const u = frame.at === t ? frame.u : hostAt(transport, t);
163
216
  const seq = frame.seq;
@@ -219,12 +272,14 @@ function back(mix, t, seq, on) {
219
272
  mix.retired = [];
220
273
  // Owners hold what is back; a voice that went live again counts its place in its owner anew.
221
274
  for (const voice of kept)
222
- if (voice.holding !== null)
275
+ if (voice.holding !== null) {
223
276
  voice.holding.children = kept.filter((v) => v.owner === voice);
277
+ voice.holding.back(seq);
278
+ }
224
279
  // Rebuilt from the voices that are back: the lanes, the chains and every count the fold reads.
225
280
  const oldLanes = mix.lanes;
226
- if (oldLanes !== null)
227
- mix.lanes = new Lanes(laneHost(mix));
281
+ if (oldLanes !== null && mix.lanesOf !== null)
282
+ mix.lanes = new mix.lanesOf(laneHost(mix));
228
283
  mix.chains = new Store();
229
284
  mix.owners = kept.some((v) => v.holding !== null) ? kept.filter((v) => v.holding !== null) : null;
230
285
  recount(mix);
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.11.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.11.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/store.d.ts CHANGED
@@ -20,3 +20,14 @@ export declare class Store<K, V> {
20
20
  delete(key: K): void;
21
21
  clear(): void;
22
22
  }
23
+ /**
24
+ * A store over another, for a copy that must not write the one it reads: what is set or deleted
25
+ * here stays here, and a key deleted here is no longer read from the one under it.
26
+ */
27
+ export declare class Masked<K, V> extends Store<K, V> {
28
+ private readonly under;
29
+ private readonly hidden;
30
+ constructor(under: Store<K, V>);
31
+ get(key: K): V | undefined;
32
+ delete(key: K): void;
33
+ }
package/dist/store.js CHANGED
@@ -62,6 +62,25 @@ export class Store {
62
62
  this.strong.clear();
63
63
  }
64
64
  }
65
+ /**
66
+ * A store over another, for a copy that must not write the one it reads: what is set or deleted
67
+ * here stays here, and a key deleted here is no longer read from the one under it.
68
+ */
69
+ export class Masked extends Store {
70
+ under;
71
+ hidden = new Store();
72
+ constructor(under) {
73
+ super();
74
+ this.under = under;
75
+ }
76
+ get(key) {
77
+ return super.get(key) ?? (this.hidden.get(key) ? undefined : this.under.get(key));
78
+ }
79
+ delete(key) {
80
+ super.delete(key);
81
+ this.hidden.set(key, true);
82
+ }
83
+ }
65
84
  /** No key held inline. */
66
85
  const none = Symbol('none');
67
86
  /** 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;
package/dist/tape.js CHANGED
@@ -1,4 +1,13 @@
1
1
  import { move } from './move.js';
2
+ const live = { transport: (of) => of, mix: (of) => of };
3
+ /** The stage the tape is playing calls on; the live one outside a read ahead's replay. */
4
+ let stage = live;
5
+ /** What a call threw on a read ahead's stage, for `replay` to throw once the tape has the entry back. */
6
+ let failed;
7
+ /** Refuses a recorded call a read ahead's copies cannot take. */
8
+ export function unplayable(what) {
9
+ throw new Error(`blits: the tape holds ${what}, which a read ahead cannot play; seek there to see it`);
10
+ }
2
11
  /** What a recorded call's `invert` gives: a mix goes back by restoring, never by undoing a call. */
3
12
  const undone = { apply() { }, invert: () => undone };
4
13
  /**
@@ -15,7 +24,10 @@ export function tapeOf(transport) {
15
24
  coalesceWindowMs: 0,
16
25
  });
17
26
  }
18
- /** Records a host call, which `again` makes once more when the mix plays past it after a seek. */
27
+ /**
28
+ * Records a host call, which `again` makes once more when the mix plays past it after a seek, or
29
+ * when a read ahead does, on the stage it is given.
30
+ */
19
31
  export function record(on, label, again) {
20
32
  const tape = on.tape;
21
33
  if (tape === undefined || on.replaying || on.projecting)
@@ -23,7 +35,16 @@ export function record(on, label, again) {
23
35
  const op = {
24
36
  label,
25
37
  apply() {
26
- again();
38
+ if (stage === live)
39
+ again(stage);
40
+ // Thrown inside the tape's `redo`, it would leave the tape without the entry.
41
+ else
42
+ try {
43
+ again(stage);
44
+ }
45
+ catch (e) {
46
+ failed ??= { error: e };
47
+ }
27
48
  },
28
49
  invert: () => undone,
29
50
  };
@@ -33,16 +54,26 @@ export function record(on, label, again) {
33
54
  export function hostAt(clock, t) {
34
55
  return clock.pace === null ? t : clock.pace.timeOf(t);
35
56
  }
57
+ /**
58
+ * The host time a move ahead to mix time `t` lands at: never behind the clock's own, since rate 0
59
+ * reads `t` at every host time from the earliest `hostAt` gives, and a call stamped earlier than
60
+ * one already on the tape would cut it out of order.
61
+ */
62
+ export function hostAhead(clock, t) {
63
+ const at = hostAt(clock, t);
64
+ return clock.u > at ? clock.u : at;
65
+ }
36
66
  /**
37
67
  * Makes the recorded calls `due` by the host time each was made at again, each host time's in a
38
68
  * frame of its own with every mix on the transport moved to where it stood then, as the host made
39
- * them. Answers the host time of the last frame it played, NaN for none.
69
+ * them. Answers the host time of the last frame it played, NaN for none. Given `onto` and the
70
+ * stage of its copies, it makes them on those, and the caller puts the tape back.
40
71
  */
41
- export function replay(transport, due) {
72
+ export function replay(transport, due, onto = transport, on = live) {
42
73
  const tape = transport.tape;
43
74
  if (tape === undefined)
44
75
  return Number.NaN;
45
- const u = transport.u;
76
+ const u = onto.u;
46
77
  let last = Number.NaN;
47
78
  try {
48
79
  for (;;) {
@@ -51,23 +82,30 @@ export function replay(transport, due) {
51
82
  return last;
52
83
  if (at !== last) {
53
84
  last = at;
54
- transport.u = at;
55
- const now = transport.pace === null ? at : transport.pace.reading(at);
56
- if (now > transport.now)
57
- for (const m of transport.members)
85
+ onto.u = at;
86
+ const now = onto.pace === null ? at : onto.pace.reading(at);
87
+ if (now > onto.now)
88
+ for (const m of onto.members)
58
89
  move(m, now);
59
- transport.tick(transport.now);
90
+ onto.tick(onto.now);
60
91
  }
61
92
  transport.replaying = true;
93
+ stage = on;
62
94
  try {
63
95
  tape.redo();
64
96
  }
65
97
  finally {
66
98
  transport.replaying = false;
99
+ stage = live;
100
+ }
101
+ if (failed !== undefined) {
102
+ const { error } = failed;
103
+ failed = undefined;
104
+ throw error;
67
105
  }
68
106
  }
69
107
  }
70
108
  finally {
71
- transport.u = u;
109
+ onto.u = u;
72
110
  }
73
111
  }