@msb235/blits 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/README.md +31 -7
  3. package/dist/bare.js +7 -1
  4. package/dist/book.d.ts +7 -0
  5. package/dist/book.js +35 -4
  6. package/dist/chain.d.ts +2 -1
  7. package/dist/chain.js +7 -3
  8. package/dist/channels.d.ts +12 -19
  9. package/dist/channels.js +18 -118
  10. package/dist/clock.d.ts +9 -2
  11. package/dist/clock.js +10 -2
  12. package/dist/closed.d.ts +15 -0
  13. package/dist/closed.js +144 -0
  14. package/dist/color.d.ts +61 -0
  15. package/dist/color.js +229 -0
  16. package/dist/columns.js +2 -1
  17. package/dist/crowd.d.ts +1 -1
  18. package/dist/crowd.js +4 -1
  19. package/dist/cue.d.ts +2 -0
  20. package/dist/cue.js +80 -27
  21. package/dist/due.js +3 -0
  22. package/dist/fade.d.ts +13 -0
  23. package/dist/fade.js +57 -4
  24. package/dist/fill.d.ts +1 -1
  25. package/dist/fill.js +15 -8
  26. package/dist/fit.d.ts +144 -0
  27. package/dist/fit.js +181 -0
  28. package/dist/fold.d.ts +3 -3
  29. package/dist/fold.js +18 -11
  30. package/dist/gather.d.ts +6 -0
  31. package/dist/gather.js +18 -0
  32. package/dist/gone.d.ts +32 -0
  33. package/dist/gone.js +134 -0
  34. package/dist/handle.d.ts +27 -2
  35. package/dist/handle.js +60 -11
  36. package/dist/held.js +4 -0
  37. package/dist/history.d.ts +15 -2
  38. package/dist/history.js +56 -9
  39. package/dist/hosts.d.ts +2 -2
  40. package/dist/hosts.js +20 -6
  41. package/dist/index.d.ts +8 -2
  42. package/dist/index.js +5 -1
  43. package/dist/lane.d.ts +1 -1
  44. package/dist/lanes.d.ts +32 -9
  45. package/dist/lanes.js +53 -24
  46. package/dist/locus.d.ts +3 -3
  47. package/dist/locus.js +1 -1
  48. package/dist/marks.d.ts +3 -0
  49. package/dist/marks.js +80 -12
  50. package/dist/meet.d.ts +3 -2
  51. package/dist/meet.js +15 -3
  52. package/dist/mixer.d.ts +56 -36
  53. package/dist/mixer.js +158 -87
  54. package/dist/motion.d.ts +2 -199
  55. package/dist/motion.js +2 -652
  56. package/dist/motions.d.ts +266 -0
  57. package/dist/motions.js +724 -0
  58. package/dist/move.d.ts +1 -1
  59. package/dist/move.js +28 -8
  60. package/dist/owner.d.ts +1 -1
  61. package/dist/owner.js +3 -3
  62. package/dist/pace.d.ts +2 -0
  63. package/dist/pace.js +8 -0
  64. package/dist/pack.d.ts +31 -0
  65. package/dist/pack.js +65 -0
  66. package/dist/paging.d.ts +63 -0
  67. package/dist/paging.js +110 -0
  68. package/dist/patch.d.ts +3 -0
  69. package/dist/patch.js +2 -0
  70. package/dist/place.js +235 -43
  71. package/dist/project.d.ts +7 -4
  72. package/dist/project.js +117 -78
  73. package/dist/pull.js +4 -3
  74. package/dist/qualify.js +4 -2
  75. package/dist/revive.d.ts +58 -0
  76. package/dist/revive.js +158 -0
  77. package/dist/sample.d.ts +1 -1
  78. package/dist/sample.js +6 -1
  79. package/dist/scored.d.ts +57 -0
  80. package/dist/scored.js +196 -0
  81. package/dist/seek.d.ts +3 -0
  82. package/dist/seek.js +311 -0
  83. package/dist/spans.d.ts +29 -0
  84. package/dist/spans.js +165 -0
  85. package/dist/strays.d.ts +1 -1
  86. package/dist/strays.js +21 -0
  87. package/dist/tape.d.ts +21 -0
  88. package/dist/tape.js +65 -0
  89. package/dist/ticker.d.ts +28 -3
  90. package/dist/ticker.js +3 -0
  91. package/dist/transport.d.ts +125 -0
  92. package/dist/transport.js +283 -0
  93. package/dist/types.d.ts +404 -38
  94. package/dist/unpage.d.ts +8 -0
  95. package/dist/unpage.js +165 -0
  96. package/dist/voice.d.ts +75 -7
  97. package/dist/voice.js +119 -4
  98. package/dist/weigh.js +2 -2
  99. package/docs/schema.html +120 -31
  100. package/package.json +2 -1
package/dist/place.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { noted } from './history.js';
2
2
  import { markOf } from './marks.js';
3
3
  import { mixTime, ownerReading } from './owner.js';
4
+ import { holders, scored, scoredGone, scoreVersion, unhold } from './scored.js';
4
5
  import { none } from './voice.js';
5
6
  /** What a voice's own owner's clock reads now: the mix clock for a voice no owner holds. */
6
7
  export function localNow(mix, voice) {
@@ -17,6 +18,7 @@ export function mixAt(mix, u) {
17
18
  export function pin(mix, voice, u) {
18
19
  mix.pins ??= new Map();
19
20
  mix.pins.set(voice, u);
21
+ voice.pinned = u;
20
22
  }
21
23
  /** Puts each pending voice's start, given in host time, where the mix's rate now puts it. */
22
24
  export function repin(mix) {
@@ -49,20 +51,23 @@ export function checkPlacement(mix, spec, anchor, owner) {
49
51
  const name = spec.name;
50
52
  if (name === undefined)
51
53
  return;
52
- // A voice is known by its owner, its score and its name; a bare name in an anchor is in the
53
- // asker's score under the asker's owner.
54
- const key = (o, score, n) => `${o?.id ?? 0}\u0000${score ?? ''}\u0000${n}`;
55
- const names = (p, score, o) => [p.start, p.in, p.out, p.end].flatMap((a) => {
56
- if (a === undefined || typeof a === 'number')
57
- return [];
54
+ // A voice is known by its owner, its score and its name, and by its mix where it names no score;
55
+ // a bare name in an anchor is in the asker's score under the asker's owner.
56
+ const key = (o, score, n, slot) => `${score === undefined ? slot : ''}\u0000${o?.id ?? 0}\u0000${score ?? ''}\u0000${n}`;
57
+ const named = (a, score, o, slot) => {
58
+ if ('all' in a)
59
+ return a.all.flatMap((m) => named(m, score, o, slot));
60
+ if ('any' in a)
61
+ return a.any.flatMap((m) => named(m, score, o, slot));
58
62
  const q = 'of' in a ? a.of : 'after' in a ? a.after : 'with' in a ? a.with : a.before;
59
63
  if (typeof q === 'string')
60
- return [key(o, score, q)];
61
- return q.name === undefined ? [] : [key(o, q.score ?? score, q.name)];
62
- });
63
- const self = key(owner, spec.score, name);
64
+ return [key(o, score, q, slot)];
65
+ return q.name === undefined ? [] : [key(o, q.score ?? score, q.name, slot)];
66
+ };
67
+ const names = (p, score, o, slot) => [p.start, p.in, p.out, p.end].flatMap((a) => a === undefined || typeof a === 'number' ? [] : named(a, score, o, slot));
68
+ const self = key(owner, spec.score, name, mix.slot);
64
69
  const seen = new Set();
65
- const waits = names(anchor, spec.score, owner);
70
+ const waits = names(anchor, spec.score, owner, mix.slot);
66
71
  while (waits.length > 0) {
67
72
  const n = waits.pop();
68
73
  if (n === self)
@@ -70,11 +75,12 @@ export function checkPlacement(mix, spec, anchor, owner) {
70
75
  if (seen.has(n))
71
76
  continue;
72
77
  seen.add(n);
73
- for (const v of mix.cued)
74
- if (v.spec.name !== undefined &&
75
- key(v.owner, v.spec.score, v.spec.name) === n &&
76
- v.spec.anchor)
77
- waits.push(...names(v.spec.anchor, v.spec.score, v.owner));
78
+ for (const m of mix.transport.members)
79
+ for (const v of m.cued)
80
+ if (v.spec.name !== undefined &&
81
+ key(v.owner, v.spec.score, v.spec.name, m.slot) === n &&
82
+ v.spec.anchor)
83
+ waits.push(...names(v.spec.anchor, v.spec.score, v.owner, m.slot));
78
84
  }
79
85
  }
80
86
  /**
@@ -104,7 +110,7 @@ export function place(mix) {
104
110
  moved = true;
105
111
  }
106
112
  }
107
- if (voice.state !== 'fading') {
113
+ if (voice.state !== 'fading' && !voice.outSet) {
108
114
  const by = anchor.out ?? anchor.end;
109
115
  const t = by === undefined ? undefined : resolve(mix, by, voice);
110
116
  if (t !== undefined) {
@@ -127,6 +133,32 @@ function resolve(mix, a, self) {
127
133
  const o = self.owner;
128
134
  if (typeof a === 'number')
129
135
  return o === null ? mixAt(mix, a - mix.offset) : a;
136
+ if ('all' in a) {
137
+ let latest = Number.NEGATIVE_INFINITY;
138
+ for (const m of a.all) {
139
+ const t = member(mix, m, self);
140
+ if (t === undefined)
141
+ return undefined;
142
+ latest = Math.max(latest, t);
143
+ }
144
+ return a.all.length === 0 ? undefined : latest;
145
+ }
146
+ if ('any' in a) {
147
+ // A member still unknown may yet answer earlier than every known one, until one has passed.
148
+ const now = Number.isNaN(mix.now) ? Number.NEGATIVE_INFINITY : localNow(mix, self);
149
+ let earliest = Number.POSITIVE_INFINITY;
150
+ let unknown = false;
151
+ for (const m of a.any) {
152
+ const t = member(mix, m, self);
153
+ if (t === undefined)
154
+ unknown = true;
155
+ else
156
+ earliest = Math.min(earliest, t);
157
+ }
158
+ if (earliest === Number.POSITIVE_INFINITY)
159
+ return undefined;
160
+ return unknown && earliest > now ? undefined : earliest;
161
+ }
130
162
  let query;
131
163
  let mark;
132
164
  let by = a.by ?? 0;
@@ -147,7 +179,7 @@ function resolve(mix, a, self) {
147
179
  mark = 'start';
148
180
  by = -by;
149
181
  }
150
- const t = timeOf(mix, typeof query === 'string' ? { name: query } : query, mark, self);
182
+ const t = timeOf(mix, query, mark, self);
151
183
  if (t === undefined)
152
184
  return undefined;
153
185
  if (o === null)
@@ -155,42 +187,202 @@ function resolve(mix, a, self) {
155
187
  const local = ownerReading(o, t);
156
188
  return Number.isFinite(local) ? local + by : undefined;
157
189
  }
190
+ /**
191
+ * What one member of a join answers, or the time it last gave once its target has left, as a lone
192
+ * anchor keeps its voice where its target last put it.
193
+ */
194
+ function member(mix, a, self) {
195
+ const t = resolve(mix, a, self);
196
+ if (t === undefined)
197
+ return self.answers?.get(a);
198
+ if (!mix.projecting) {
199
+ self.answers ??= new Map();
200
+ self.answers.set(a, t);
201
+ }
202
+ return t;
203
+ }
204
+ /** Picks among the times a query meets, as its resolver says, in the order they are met. */
205
+ class Pick {
206
+ resolver;
207
+ now;
208
+ any = false;
209
+ firstOrder = Number.POSITIVE_INFINITY;
210
+ lastOrder = Number.NEGATIVE_INFINITY;
211
+ first;
212
+ last;
213
+ pick;
214
+ constructor(resolver, now) {
215
+ this.resolver = resolver;
216
+ this.now = now;
217
+ }
218
+ take(order, t) {
219
+ this.any = true;
220
+ if (order < this.firstOrder) {
221
+ this.firstOrder = order;
222
+ this.first = t;
223
+ }
224
+ if (order > this.lastOrder) {
225
+ this.lastOrder = order;
226
+ this.last = t;
227
+ }
228
+ if (t !== undefined)
229
+ this.consider(t);
230
+ }
231
+ /** Takes what `o` picked as though its times had been met here. */
232
+ merge(o) {
233
+ if (!o.any)
234
+ return;
235
+ this.any = true;
236
+ if (o.firstOrder < this.firstOrder) {
237
+ this.firstOrder = o.firstOrder;
238
+ this.first = o.first;
239
+ }
240
+ if (o.lastOrder > this.lastOrder) {
241
+ this.lastOrder = o.lastOrder;
242
+ this.last = o.last;
243
+ }
244
+ if (o.pick !== undefined)
245
+ this.consider(o.pick);
246
+ }
247
+ consider(t) {
248
+ const p = this.pick;
249
+ if (this.resolver === 'earliest'
250
+ ? p === undefined || t < p
251
+ : this.resolver === 'latest'
252
+ ? p === undefined || t > p
253
+ : this.resolver === 'next' && t >= this.now && (p === undefined || t < p))
254
+ this.pick = t;
255
+ }
256
+ answer() {
257
+ if (!this.any)
258
+ return undefined;
259
+ if (this.resolver === 'first')
260
+ return this.first;
261
+ if (this.resolver === 'last')
262
+ return this.last;
263
+ return this.pick;
264
+ }
265
+ }
158
266
  /**
159
267
  * The mix time a query answers: `mark` of the voice it picks, among those cued and those that have
160
268
  * left, never the asker; or a mark the host announced on the score, which is the same time for
161
269
  * any of the four. Undefined while what it picks has no such time. A query naming no score looks
162
270
  * among the asker's siblings, the voices its owner holds.
163
271
  */
164
- function timeOf(mix, q, mark, self) {
272
+ function timeOf(mix, key, mark, self) {
273
+ const q = typeof key === 'string' ? { name: key } : key;
165
274
  const score = q.score ?? self.spec.score;
166
275
  const anywhere = q.score !== undefined;
167
- const found = [];
168
- for (const v of [...mix.gone, ...mix.cued])
169
- if (v !== self &&
170
- v.spec.score === score &&
171
- (anywhere || v.owner === self.owner) &&
172
- (q.name === undefined || v.spec.name === q.name) &&
173
- (q.tag === undefined || (v.spec.tags ?? none).includes(q.tag)) &&
174
- (q.writes === undefined || v.patch.writes.includes(q.writes)))
175
- found.push({ order: v.id, t: markOf(mix, v, mark) });
276
+ const resolver = q.resolver ?? 'last';
277
+ const now = Number.isNaN(mix.now) ? Number.NEGATIVE_INFINITY : mix.now;
278
+ const picked = new Pick(resolver, now);
279
+ const matches = (v) => v !== self &&
280
+ v.spec.score === score &&
281
+ (anywhere || v.owner === self.owner) &&
282
+ (q.name === undefined || v.spec.name === q.name) &&
283
+ (q.tag === undefined || (v.spec.tags ?? none).includes(q.tag)) &&
284
+ (q.writes === undefined || v.patch.writes.includes(q.writes));
285
+ // A query naming a score looks in every mix on the transport that holds a voice on it; one
286
+ // naming none, in its own.
287
+ const listed = anywhere && score !== undefined && !mix.projecting;
288
+ const mixes = !anywhere
289
+ ? [mix]
290
+ : listed
291
+ ? holders(mix, score)
292
+ : mix.transport.members;
293
+ for (let i = 0; i < mixes.length; i++) {
294
+ const m = mixes[i];
295
+ if (score === undefined || m.projecting || mix.projecting) {
296
+ for (const v of [...m.gone, ...m.cued])
297
+ if (matches(v))
298
+ picked.take(v.id, markOf(m, v, mark));
299
+ continue;
300
+ }
301
+ const cued = scored(m, score);
302
+ const list = scoredGone(m, score);
303
+ if (cued.size === 0 && list.length === 0) {
304
+ if (listed) {
305
+ unhold(m, score);
306
+ i--;
307
+ }
308
+ continue;
309
+ }
310
+ // `next` asks what is still ahead of now, which a running pick cannot keep.
311
+ if (resolver === 'next') {
312
+ for (const v of cued)
313
+ if (matches(v))
314
+ picked.take(v.id, markOf(m, v, mark));
315
+ for (const v of list)
316
+ if (matches(v))
317
+ picked.take(v.id, markOf(m, v, mark));
318
+ continue;
319
+ }
320
+ const fold = foldOf(m, list, key, mark, self, resolver);
321
+ const version = scoreVersion(m, score);
322
+ const reduced = m.reduced;
323
+ if (fold.live === null ||
324
+ fold.liveOf !== m.cued ||
325
+ fold.liveVersion !== version ||
326
+ fold.liveReduced !== reduced) {
327
+ const live = new Pick(resolver, Number.NEGATIVE_INFINITY);
328
+ let keeps = true;
329
+ for (const v of cued) {
330
+ if (!matches(v))
331
+ continue;
332
+ if (v.owner !== null || v.holding !== null || v.fitting !== null)
333
+ keeps = false;
334
+ live.take(v.id, markOf(m, v, mark));
335
+ }
336
+ fold.live = live;
337
+ fold.liveOf = keeps ? m.cued : null;
338
+ fold.liveVersion = version;
339
+ fold.liveReduced = reduced;
340
+ }
341
+ picked.merge(fold.live);
342
+ for (; fold.seen < list.length; fold.seen++) {
343
+ const v = list[fold.seen];
344
+ if (!matches(v))
345
+ continue;
346
+ if (v.owner === null)
347
+ fold.pick.take(v.id, markOf(m, v, mark));
348
+ else
349
+ fold.owned.push(v);
350
+ }
351
+ picked.merge(fold.pick);
352
+ for (const v of fold.owned)
353
+ picked.take(v.id, markOf(m, v, mark));
354
+ }
176
355
  if (q.writes === undefined && (anywhere || self.owner === null))
177
356
  for (const a of mix.announced)
178
357
  if (a.score === score &&
358
+ (score !== undefined || a.slot === mix.slot) &&
179
359
  (q.name === undefined || a.name === q.name) &&
180
360
  (q.tag === undefined || a.tags.includes(q.tag)))
181
- found.push({ order: a.order, t: Number.isNaN(a.at) ? undefined : mixAt(mix, a.at) });
182
- if (found.length === 0)
183
- return undefined;
184
- found.sort((x, y) => x.order - y.order);
185
- const resolver = q.resolver ?? 'last';
186
- if (resolver === 'first')
187
- return found[0]?.t;
188
- if (resolver === 'last')
189
- return found[found.length - 1]?.t;
190
- const times = found.flatMap((e) => (e.t === undefined ? [] : [e.t])).sort((x, y) => x - y);
191
- if (resolver === 'next') {
192
- const now = Number.isNaN(mix.now) ? Number.NEGATIVE_INFINITY : mix.now;
193
- return times.find((t) => t >= now);
194
- }
195
- return resolver === 'earliest' ? times[0] : times[times.length - 1];
361
+ picked.take(a.order, Number.isNaN(a.at) ? undefined : mixAt(mix, a.at));
362
+ return picked.answer();
363
+ }
364
+ /** `self`'s read of `m`'s gone voices for one query, started over where the list has changed under it. */
365
+ function foldOf(m, list, key, mark, self, resolver) {
366
+ self.folds ??= new Map();
367
+ let byMix = self.folds.get(key);
368
+ if (byMix === undefined) {
369
+ byMix = new Map();
370
+ self.folds.set(key, byMix);
371
+ }
372
+ let fold = byMix.get(m);
373
+ if (fold === undefined || fold.list !== list || fold.mark !== mark) {
374
+ fold = {
375
+ list,
376
+ mark,
377
+ seen: 0,
378
+ pick: new Pick(resolver, Number.NEGATIVE_INFINITY),
379
+ owned: [],
380
+ live: null,
381
+ liveOf: null,
382
+ liveVersion: -1,
383
+ liveReduced: false,
384
+ };
385
+ byMix.set(m, fold);
386
+ }
387
+ return fold;
196
388
  }
package/dist/project.d.ts CHANGED
@@ -1,4 +1,7 @@
1
- import type { Mixer } from './mixer.js';
2
- import type { Projection } from './types.js';
3
- /** A projection to `timestamp` through `c`, a mixer of its own the mix has just made. */
4
- export declare function project<I, O>(mix: Mixer<I, O>, c: Mixer<I, O>, timestamp: number): Projection<I, O>;
1
+ import { Transport } from './transport.js';
2
+ import type { TransportProjection } from './types.js';
3
+ /**
4
+ * Every mix on a transport read at mix time `t`, through copies of them on a transport of their
5
+ * own, moved together so an anchor across mixes answers as it would live.
6
+ */
7
+ export declare function projectAll(transport: Transport, t: number): TransportProjection;
package/dist/project.js CHANGED
@@ -1,85 +1,60 @@
1
1
  import { ownBlends } from './blend.js';
2
2
  import { index } from './chain.js';
3
- import { copyHeld, last } from './history.js';
4
- import { readingAt } from './marks.js';
3
+ import { copyHeld, last, leftAt } from './history.js';
4
+ import { Mixer } from './mixer.js';
5
5
  import { move } from './move.js';
6
6
  import { heldByInput, ownerReading, relink } from './owner.js';
7
7
  import { pin } from './place.js';
8
8
  import { reading } from './reading.js';
9
9
  import { Store } from './store.js';
10
+ import { hostAt } from './tape.js';
11
+ import { Transport } from './transport.js';
12
+ import { cover } from './unpage.js';
10
13
  import { unreached } from './unreached.js';
11
- /** A projection to `timestamp` through `c`, a mixer of its own the mix has just made. */
12
- export function project(mix, c, timestamp) {
13
- const u = timestamp - mix.offset;
14
- const pace = mix.pace;
15
- const t = readingAt(mix, u);
16
- c.projecting = true;
17
- c.pose = mix.pose;
18
- c.offset = mix.offset;
19
- c.u = u;
20
- c.wantsPose = mix.wantsPose;
21
- c.nextId = mix.nextId;
22
- c.reducedNow = mix.reducedNow;
23
- if (Number.isNaN(mix.now) || t >= mix.now) {
24
- c.now = mix.now;
25
- c.pace = pace === null ? null : pace.until(Number.POSITIVE_INFINITY);
26
- c.cued = mix.cued
27
- .filter((v) => v.state !== 'done')
28
- .map((v) => {
29
- const copy = v.copy((subject) => carry(mix, v, subject));
30
- const at = mix.pins?.get(v);
31
- if (at !== undefined)
32
- pin(c, copy, at);
33
- return copy;
34
- });
35
- ownBlends(c.cued);
36
- if (mix.owners !== null)
37
- relink(c.cued);
38
- c.announced = mix.announced.map((a) => ({ ...a }));
39
- count(c);
40
- move(c, t);
41
- }
42
- else {
43
- const history = mix.opts.history;
14
+ /**
15
+ * Every mix on a transport read at mix time `t`, through copies of them on a transport of their
16
+ * own, moved together so an anchor across mixes answers as it would live.
17
+ */
18
+ export function projectAll(transport, t) {
19
+ const ahead = Number.isNaN(transport.now) || t >= transport.now;
20
+ const tape = transport.tape;
21
+ const next = ahead && tape !== undefined ? tape.timestampAt(tape.undoDepth()) : undefined;
22
+ if (next !== undefined && next <= t)
23
+ throw new Error(`blits: the tape holds calls at ${next} that a read ahead to ${t} cannot play; seek there to see them`);
24
+ if (!ahead) {
25
+ const history = transport.history;
44
26
  if (!history)
45
27
  throw new Error('blits: reading back needs a mix made with history');
46
- if (t < mix.now - history.ms)
47
- throw new Error(`blits: ${timestamp} is older than this mix's history reaches`);
48
- c.now = t;
49
- c.pace = pace === null ? null : pace.until(u);
50
- c.backward = true;
51
- const was = last(mix.hostLog, t, true);
52
- const host = mix.opts.host;
53
- const then = was && typeof host === 'object' && host !== null
54
- ? Object.assign(Object.create(host), was.fields)
55
- : undefined;
56
- if (was)
57
- c.hostThen = was;
58
- c.announced = mix.announced.filter((a) => a.made < t).map((a) => ({ ...a }));
59
- c.cued = [...mix.cued, ...mix.gone]
60
- .filter((v) => v.cuedAt <= t && v.doneAt > t)
61
- .sort((a, b) => a.id - b.id)
62
- .map((v) => {
63
- const log = v.log;
64
- const copy = v.copy((subject) => recall(mix, v, subject, t), last(log, t, (e) => e.sync) ?? log[0]);
65
- if (then !== undefined)
66
- copy.setting.host = then;
67
- return copy;
28
+ cover(transport, t);
29
+ }
30
+ const u = hostAt(transport, t);
31
+ const pace = transport.pace;
32
+ const c = new Transport(undefined, true);
33
+ c.offset = transport.offset;
34
+ c.u = u;
35
+ c.nextId = transport.nextId;
36
+ c.now = ahead ? transport.now : t;
37
+ c.pace = pace === null ? null : pace.until(ahead ? Number.POSITIVE_INFINITY : u);
38
+ c.announced = (ahead ? transport.announced : transport.announced.filter((a) => a.made < t)).map((a) => ({ ...a }));
39
+ const copies = new Map();
40
+ for (const mix of transport.members) {
41
+ const copy = new Mixer(mix.kit, {
42
+ ...mix.opts,
43
+ history: undefined,
44
+ lanes: false,
45
+ transport: c,
68
46
  });
69
- ownBlends(c.cued);
70
- if (mix.owners !== null)
71
- relink(c.cued);
72
- for (const copy of c.cued)
73
- copy.state =
74
- (copy.owner === null ? t : ownerReading(copy.owner, t)) < copy.start
75
- ? 'pending'
76
- : copy.out && copy.out.at <= t
77
- ? 'fading'
78
- : copy.freezesAfter && copy.elapsedAt(t) >= copy.span + copy.latest
79
- ? 'frozen'
80
- : 'live';
81
- count(c);
47
+ copy.slot = mix.slot;
48
+ copy.projecting = true;
49
+ copies.set(mix, copy);
50
+ if (ahead)
51
+ copyAhead(mix, copy);
52
+ else
53
+ copyBack(mix, copy, t);
82
54
  }
55
+ if (ahead)
56
+ for (const copy of copies.values())
57
+ move(copy, t);
83
58
  const read = (f) => {
84
59
  reading.live = false;
85
60
  try {
@@ -90,14 +65,78 @@ export function project(mix, c, timestamp) {
90
65
  }
91
66
  };
92
67
  return {
93
- timestamp,
94
- probe: (subject, out) => read(() => c.fold(subject, out)),
95
- assess: (subject) => read(() => {
96
- c.fold(subject);
97
- return doubts(c, subject);
98
- }),
68
+ timestamp: t,
69
+ of(mix) {
70
+ const copy = copies.get(mix);
71
+ if (copy === undefined)
72
+ throw new Error('blits: that mix is not on this transport');
73
+ return {
74
+ timestamp: t,
75
+ probe: (subject, out) => read(() => copy.fold(subject, out)),
76
+ assess: (subject) => read(() => {
77
+ copy.fold(subject);
78
+ return doubts(copy, subject);
79
+ }),
80
+ };
81
+ },
99
82
  };
100
83
  }
84
+ /** A mix's copy for a read ahead: its voices as they stand, each subject carried from the live one. */
85
+ function copyAhead(mix, c) {
86
+ c.pose = mix.pose;
87
+ c.wantsPose = mix.wantsPose;
88
+ c.reducedNow = mix.reducedNow;
89
+ c.cued = mix.cued
90
+ .filter((v) => v.state !== 'done')
91
+ .map((v) => {
92
+ const copy = v.copy((subject) => carry(mix, v, subject));
93
+ const at = mix.pins?.get(v);
94
+ if (at !== undefined)
95
+ pin(c, copy, at);
96
+ return copy;
97
+ });
98
+ ownBlends(c.cued);
99
+ if (mix.owners !== null)
100
+ relink(c.cued);
101
+ count(c);
102
+ }
103
+ /** A mix's copy for a read back to `t`: its voices then, from the controls and copies kept. */
104
+ function copyBack(mix, c, t) {
105
+ c.pose = mix.pose;
106
+ c.wantsPose = mix.wantsPose;
107
+ c.reducedNow = mix.reducedNow;
108
+ c.backward = true;
109
+ const was = last(mix.hostLog, t, true);
110
+ const host = mix.opts.host;
111
+ const then = was && typeof host === 'object' && host !== null
112
+ ? Object.assign(Object.create(host), was.fields)
113
+ : undefined;
114
+ if (was)
115
+ c.hostThen = was;
116
+ c.cued = [...mix.cued, ...mix.gone]
117
+ .filter((v) => v.cuedAt <= t && v.doneAt > t)
118
+ .sort((a, b) => a.id - b.id)
119
+ .map((v) => {
120
+ const log = v.log;
121
+ const copy = v.copy((subject) => recall(mix, v, subject, t), last(log, t, (e) => e.sync) ?? log[0]);
122
+ if (then !== undefined)
123
+ copy.setting.host = then;
124
+ return copy;
125
+ });
126
+ ownBlends(c.cued);
127
+ if (mix.owners !== null)
128
+ relink(c.cued);
129
+ for (const copy of c.cued)
130
+ copy.state =
131
+ (copy.owner === null ? t : ownerReading(copy.owner, t)) < copy.start
132
+ ? 'pending'
133
+ : copy.out && copy.out.at <= t
134
+ ? 'fading'
135
+ : copy.freezesAfter && copy.elapsedAt(t) >= copy.span + copy.latest
136
+ ? 'frozen'
137
+ : 'live';
138
+ count(c);
139
+ }
101
140
  /** Recounts what the fold's shortcuts depend on, for a projection's freshly copied voices. */
102
141
  function count(mix) {
103
142
  mix.named = new Store();
@@ -132,7 +171,7 @@ function carry(mix, voice, subject) {
132
171
  * voice's start with fresh state, which is exact for a voice stepped at a fixed interval.
133
172
  */
134
173
  function recall(mix, voice, subject, t) {
135
- const live = recordOf(mix, voice, subject);
174
+ const live = leftAt(voice.left, subject, t) ?? recordOf(mix, voice, subject);
136
175
  if (live === undefined)
137
176
  return undefined;
138
177
  const snap = live.snaps && last(live.snaps, t, true);
@@ -192,7 +231,7 @@ function doubts(mix, subject) {
192
231
  for (const slot of voice.slots)
193
232
  out[mix.names[slot]] = 'held';
194
233
  }
195
- for (let held = mix.chain(subject, mix.now); held !== null; held = held.next) {
234
+ for (let held = mix.chain(subject); held !== null; held = held.next) {
196
235
  const voice = held.voice;
197
236
  if (voice === null || voice.state === 'done')
198
237
  continue;
package/dist/pull.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { axesOf } from './channels.js';
1
2
  import { copy } from './clone.js';
2
3
  import { clampRun, flush, pullRun, writeLater } from './columns.js';
3
4
  /** Writes a folded pose's value into subject `n`'s places in a column. */
@@ -128,14 +129,14 @@ function readAll(mix, list, columns, live, current, whole) {
128
129
  const kept = heads[n];
129
130
  const head = was[n] === subject && kept !== undefined && kept.version === version
130
131
  ? kept
131
- : mix.chain(subject, now);
132
+ : mix.chain(subject);
132
133
  was[n] = subject;
133
134
  heads[n] = head;
134
135
  slot = head.slot;
135
136
  slots[n] = slot;
136
137
  }
137
138
  }
138
- const laned = live && lanes?.prepare(slot, subject, now, mix.version, heads[n] ?? null) === true;
139
+ const laned = live && lanes?.prepare(slot, subject, mix.version, heads[n] ?? null) === true;
139
140
  if (laned && whole && lanes.whole && !lanes.owes(slot)) {
140
141
  writeLater(lanes, slot, columns, n);
141
142
  continue;
@@ -160,7 +161,7 @@ function columnsOf(mix, into) {
160
161
  throw new Error(`blits: pull was handed ${key}, which the kit lacks`);
161
162
  const channel = mix.channels[slot];
162
163
  const rest = channel.rest;
163
- const axes = Array.isArray(rest) ? rest.length : 1;
164
+ const axes = axesOf(channel);
164
165
  const bounds = channel.bounds;
165
166
  const at = new Float64Array(axes).fill(Number.NaN);
166
167
  if (typeof rest === 'number')
package/dist/qualify.js CHANGED
@@ -163,11 +163,13 @@ export function requalify(lanes, version) {
163
163
  return;
164
164
  const channel = host.channels[slot];
165
165
  const n = numericOf(channel);
166
- const rest = channel.rest;
166
+ // A rest-less channel's values are NaN until some voice passes the band: absent from the pose.
167
+ const rest = (channel.rest ?? Number.NaN);
168
+ const scalar = typeof rest === 'number' && n.axes === 1;
167
169
  const ch = {
168
170
  name: host.names[slot],
169
171
  op: n.op,
170
- scalar: typeof rest === 'number',
172
+ scalar,
171
173
  rest: typeof rest === 'number' ? rest : rest[0],
172
174
  axes: n.axes,
173
175
  values: new Float64Array(lanes.cap * n.axes),