@msb235/blits 0.16.0 → 0.16.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 (56) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +1 -1
  3. package/dist/ahead.js +6 -9
  4. package/dist/clock.d.ts +11 -1
  5. package/dist/clock.js +28 -9
  6. package/dist/contribution.js +11 -4
  7. package/dist/cue.js +12 -3
  8. package/dist/due.d.ts +18 -0
  9. package/dist/due.js +101 -2
  10. package/dist/fade.d.ts +12 -2
  11. package/dist/fade.js +41 -8
  12. package/dist/fill.js +2 -1
  13. package/dist/fit.js +8 -0
  14. package/dist/gone.js +1 -0
  15. package/dist/handle.d.ts +1 -1
  16. package/dist/handle.js +3 -1
  17. package/dist/held.d.ts +2 -1
  18. package/dist/held.js +14 -6
  19. package/dist/hosts.js +5 -1
  20. package/dist/lane.d.ts +2 -0
  21. package/dist/lane.js +2 -0
  22. package/dist/lanes.js +2 -1
  23. package/dist/meet.js +9 -0
  24. package/dist/mixer.d.ts +2 -0
  25. package/dist/mixer.js +2 -0
  26. package/dist/motion.js +6 -6
  27. package/dist/move.js +309 -82
  28. package/dist/origin.d.ts +2 -0
  29. package/dist/origin.js +4 -0
  30. package/dist/owner.js +4 -2
  31. package/dist/paging.d.ts +5 -1
  32. package/dist/paging.js +13 -3
  33. package/dist/patch.js +24 -4
  34. package/dist/place.js +67 -31
  35. package/dist/project.d.ts +5 -2
  36. package/dist/project.js +7 -4
  37. package/dist/qualify.js +6 -1
  38. package/dist/rests.js +9 -1
  39. package/dist/revive.d.ts +11 -1
  40. package/dist/revive.js +7 -0
  41. package/dist/roles.js +5 -2
  42. package/dist/rows.js +4 -1
  43. package/dist/seat.d.ts +5 -1
  44. package/dist/seat.js +5 -1
  45. package/dist/seek.js +6 -4
  46. package/dist/shared.d.ts +1 -1
  47. package/dist/shared.js +1 -1
  48. package/dist/spans.d.ts +10 -4
  49. package/dist/spans.js +121 -31
  50. package/dist/trace.js +7 -2
  51. package/dist/types/transport.d.ts +2 -1
  52. package/dist/voice.d.ts +42 -4
  53. package/dist/voice.js +45 -5
  54. package/dist/weigh.js +1 -1
  55. package/docs/schema.html +1 -1
  56. package/package.json +3 -1
package/dist/move.js CHANGED
@@ -2,17 +2,17 @@ import { settleArcs } from './arcs.js';
2
2
  import { changed } from './chain.js';
3
3
  import { elapsedWith } from './clock.js';
4
4
  import { byId, popDue, schedule } from './due.js';
5
- import { beginFade, part, retire, rouse, stay } from './fade.js';
5
+ import { beginFade, part, release, retire, rouse, stay } from './fade.js';
6
6
  import { expireGone, keepGone } from './gone.js';
7
- import { begin } from './held.js';
7
+ import { begin, reorigin } from './held.js';
8
8
  import { noted } from './history.js';
9
- import { leave as leaveClock } from './origin.js';
10
- import { ownerReading, plays } from './owner.js';
9
+ import { leave as leaveClock, startedAt } from './origin.js';
10
+ import { descendants, ownerReading, plays } from './owner.js';
11
11
  import { mixAt, place, repin, startAt, startOf } from './place.js';
12
12
  import { pageVoice } from './revive.js';
13
13
  import { delist, detour } from './roster.js';
14
14
  import { scoredLeft, scoredRemove } from './scored.js';
15
- import { lapse } from './spans.js';
15
+ import { lapse, lapses, unfit } from './spans.js';
16
16
  import { unplay } from './strays.js';
17
17
  /** Numbers every mix's frames apart, so a blend's read is never taken for another mix's or frame's. */
18
18
  let frames = 0;
@@ -59,88 +59,21 @@ function moveTo(mix, now) {
59
59
  }
60
60
  // A live mix visits only the voices due by now; a projection, which copies few, visits all.
61
61
  const visit = mix.projecting || mix.walkAll ? mix.cued : popDue(mix, now);
62
+ let held = null;
62
63
  for (const voice of visit) {
63
64
  if (voice.state === 'done')
64
65
  continue;
65
- if (Number.isNaN(voice.opened))
66
- voice.opened = now;
67
- if (voice.state === 'pending' &&
68
- (voice.owner === null ? now : ownerReading(voice.owner, now)) >= voice.start)
69
- begin(mix, voice, now);
70
- // After it starts, so a fade due by now on a voice that started since the last frame begins.
71
- // Not one set for after its passes run out, where a move across both would begin it first.
72
- if ((voice.state === 'live' || voice.state === 'frozen') &&
73
- voice.outAt <= now &&
74
- !(voice.state === 'live' &&
75
- !voice.freezesAfter &&
76
- !voice.freezesBefore &&
77
- voice.endsAt() < voice.outAt))
78
- beginFade(mix, voice, { over: Number.isNaN(voice.outOver) ? undefined : voice.outOver }, Math.max(startOf(voice), voice.outAt));
79
- if (voice.state !== 'pending') {
80
- if (voice.turns)
81
- turn(mix, voice, now);
82
- if (voice.state === 'live' && voice.out !== null && !voice.quietAt(now))
83
- rouse(mix, voice, now);
84
- // Its passes run out at its end running forward, and at its start running back. A clock
85
- // standing still at or before its start is where it was: waiting, or frozen there.
86
- const way = heading(voice, now);
87
- const at = voice.elapsedAt(now);
88
- const back = way < 0;
89
- const atStart = !voice.beginless && at <= voice.soonest;
90
- // An owner run back to its start stays for what it holds, and leaves with its last child.
91
- const holds = voice.holding !== null && plays(voice, now);
92
- const over = back
93
- ? atStart && !holds
94
- : Number.isFinite(voice.span) && at >= voice.span + voice.latest;
95
- if (over) {
96
- voice.play(true, now, mix.transport.seq);
97
- // The fade starts when its passes ran out, not at the frame that noticed, so it plays
98
- // the same at any frame rate and a read at another time can find it.
99
- if (voice.state === 'live' && voice.out === null) {
100
- if (back ? voice.freezesBefore : voice.freezesAfter)
101
- voice.state = 'frozen';
102
- else
103
- beginFade(mix, voice, { back, stays: voice.owner !== null && mix.opts.history !== undefined }, Math.max(startOf(voice), Math.min(now, voice.endsAt())));
104
- }
105
- }
106
- else if (voice.state === 'frozen' && !(atStart && way === 0))
107
- voice.state = 'live';
108
- }
109
- // A projection reads a finished ramp as weight 0, and leaves the live voice's subjects be.
110
- if (voice.parts !== null && !mix.projecting)
111
- for (const [subject, r] of voice.parts)
112
- if (now - r.at >= r.over)
113
- part(mix, voice, subject, r.at + r.over);
114
- const back = voice.back;
115
- if (back !== null && now >= back.at + (1 - back.from) * back.over && !mix.projecting) {
116
- voice.back = null;
117
- mix.lanes?.refill();
66
+ if (visitOne(mix, voice, now, voice.owner !== null && voice.owner.fitting !== null)) {
67
+ held ??= [];
68
+ held.push(voice);
118
69
  }
119
- if (voice.state === 'fading' && voice.out) {
120
- const { at, over, rest, deadline } = voice.out;
121
- const spent = now - at;
122
- if (rest) {
123
- const out = deadline !== undefined && spent >= deadline;
124
- const settled = voice.seen > 0 && voice.restedCount >= voice.seen;
125
- if (out)
126
- retire(mix, voice, at + deadline);
127
- else if (settled)
128
- retire(mix, voice);
129
- }
130
- else if (spent >= over) {
131
- if (voice.out.stays)
132
- stay(mix, voice, at + over);
133
- else
134
- retire(mix, voice, at + over);
135
- }
136
- }
137
- if (!mix.projecting)
138
- schedule(mix, voice);
139
- }
140
- if (mix.owners !== null) {
141
- lapse(mix, now);
142
- settleArcs(mix, now);
143
70
  }
71
+ const back = mix.leapt === null && held === null ? null : turnedBack(mix, held, now);
72
+ mix.leapt = null;
73
+ if (back === null)
74
+ settle(mix, held, now);
75
+ else
76
+ ahead(mix, back, held, now);
144
77
  if (mix.sharers.length > 0 && !mix.projecting)
145
78
  mix.share?.outgrow(mix);
146
79
  mix.stirred = false;
@@ -149,6 +82,300 @@ function moveTo(mix, now) {
149
82
  prune(mix);
150
83
  forget(mix);
151
84
  }
85
+ /** Takes the span children waiting their turn, then lets go of spans and arcs that are over. */
86
+ function settle(mix, held, now) {
87
+ if (held !== null)
88
+ inTurn(mix, held, now);
89
+ if (mix.owners !== null) {
90
+ lapse(mix, now);
91
+ settleArcs(mix, now);
92
+ }
93
+ }
94
+ /**
95
+ * The owners sought forward since the last frame, or above the span children waiting their turn,
96
+ * whose clocks do not run forward at `now`, with those above them that do not either; null where
97
+ * there are none. An owner a span holds is left out, since its span's fits change its clock.
98
+ */
99
+ function turnedBack(mix, held, now) {
100
+ let back = null;
101
+ if (mix.leapt !== null)
102
+ for (const o of mix.leapt)
103
+ back = up(o, now, back);
104
+ if (held !== null)
105
+ for (const v of held)
106
+ back = up(v.owner, now, back);
107
+ return back;
108
+ }
109
+ /** Adds to `back` the owners from `from` up that `turnedBack` takes. */
110
+ function up(from, now, back) {
111
+ for (let o = from; o !== null; o = o.owner) {
112
+ if (o.state === 'done' || o.owner?.fitting || back?.includes(o))
113
+ continue;
114
+ if (o.rateAt(o.owner === null ? now : ownerReading(o.owner, now)) > 0)
115
+ continue;
116
+ back ??= [];
117
+ back.push(o);
118
+ }
119
+ return back;
120
+ }
121
+ /**
122
+ * Settles a frame in which a seek took owners whose clocks do not run forward ahead: the span
123
+ * children and spans it passed start and end in order, as played straight through, with those
124
+ * clocks run forward to where they read now, then back on their own.
125
+ */
126
+ function ahead(mix, back, held, now) {
127
+ const kept = back.map((o) => {
128
+ const { anchorNow, anchorElapsed, rate, ramp, start } = o;
129
+ const at = o.owner === null ? now : ownerReading(o.owner, now);
130
+ o.anchorElapsed = elapsedWith(o, at);
131
+ o.anchorNow = at;
132
+ o.rate = 1;
133
+ o.ramp = null;
134
+ // Started where the clock run forward reads 0, so a time on it before now maps back to it.
135
+ o.start = at - o.anchorElapsed;
136
+ return { anchorNow, anchorElapsed, rate, ramp, start, logged: o.log?.length ?? 0 };
137
+ });
138
+ try {
139
+ // Anchors are placed on those clocks too, and a plain child placed behind them starts there:
140
+ // on a clock standing still, an owner's time maps to the seek's moment and back to where it
141
+ // was sought, so a later frame would place it again past it.
142
+ if (mix.anchored > 0) {
143
+ place(mix);
144
+ for (const o of back)
145
+ descendants(o, (v) => {
146
+ if (v.state === 'pending' && v.spec.anchor !== undefined && v.owner?.fitting === null)
147
+ visitOne(mix, v, now);
148
+ });
149
+ }
150
+ settle(mix, held, now);
151
+ }
152
+ finally {
153
+ back.forEach((o, i) => {
154
+ const { logged, ...clock } = kept[i];
155
+ Object.assign(o, clock);
156
+ const log = o.log;
157
+ if (log !== null)
158
+ for (let k = logged; k < log.length; k++)
159
+ Object.assign(log[k], clock);
160
+ });
161
+ }
162
+ // What was worked out in mix time from the clocks run forward is worked out again on their own.
163
+ mix.frame = nextFrame();
164
+ for (const o of back) {
165
+ if (o.state !== 'done')
166
+ schedule(mix, o);
167
+ descendants(o, (v) => {
168
+ if (v.state === 'done')
169
+ return;
170
+ reorigin(mix, v, now);
171
+ schedule(mix, v);
172
+ });
173
+ }
174
+ mix.lanes?.refill();
175
+ }
176
+ /**
177
+ * Starts, ends and fades one voice as of `now`; with `wait`, a span's child, it does none of that
178
+ * and answers true where the child `meets` now, to wait its turn.
179
+ */
180
+ function visitOne(mix, voice, now, wait = false) {
181
+ if (Number.isNaN(voice.opened))
182
+ voice.opened = now;
183
+ if (voice.earlier !== null || voice.later !== null)
184
+ unfit(mix, voice, now);
185
+ const t = voice.owner === null ? now : ownerReading(voice.owner, now);
186
+ if (wait && meetsAt(voice, t, now))
187
+ return true;
188
+ if (voice.state === 'pending' && starts(voice, t, now))
189
+ begin(mix, voice, now);
190
+ // After it starts, so a fade due by now on a voice that started since the last frame begins.
191
+ // Not one set for after its passes run out, where a move across both would begin it first.
192
+ if ((voice.state === 'live' || voice.state === 'frozen') &&
193
+ voice.outAt <= now &&
194
+ !(voice.state === 'live' &&
195
+ !voice.freezesAfter &&
196
+ !voice.freezesBefore &&
197
+ voice.endsAt() < voice.outAt))
198
+ beginFade(mix, voice, { over: Number.isNaN(voice.outOver) ? undefined : voice.outOver }, Math.max(startedAt(voice), voice.outAt));
199
+ if (voice.state !== 'pending') {
200
+ if (voice.turns)
201
+ turn(mix, voice, now);
202
+ if (voice.state === 'live' && voice.out !== null && !voice.quietAt(now))
203
+ rouse(mix, voice, now);
204
+ // Its passes run out at its end running forward, and at its start running back. A clock
205
+ // standing still at or before its start is where it was: waiting, or frozen there.
206
+ const heads = Math.sign(voice.rateAt(t)) * (voice.owner === null ? 1 : heading(voice.owner, now));
207
+ const at = t === Number.NEGATIVE_INFINITY ? t : elapsedWith(voice, t);
208
+ const back = heads < 0;
209
+ const atStart = !voice.beginless && at <= voice.soonest;
210
+ // An owner run back to its start stays for what it holds, and leaves with its last child.
211
+ const holds = voice.holding !== null && plays(voice, now);
212
+ const over = back
213
+ ? atStart && !holds
214
+ : Number.isFinite(voice.span) && at >= voice.span + voice.latest;
215
+ if (over) {
216
+ voice.play(true, now, mix.transport.seq);
217
+ // The fade starts when its passes ran out, not at the frame that noticed, so it plays
218
+ // the same at any frame rate and a read at another time can find it.
219
+ if (voice.state === 'live' && voice.out === null) {
220
+ if (back ? voice.freezesBefore : voice.freezesAfter)
221
+ voice.state = 'frozen';
222
+ else
223
+ beginFade(mix, voice, { back, stays: voice.owner !== null && mix.opts.history !== undefined }, Math.max(startOf(voice), Math.min(now, voice.endsAt())));
224
+ }
225
+ }
226
+ else if (voice.state === 'frozen' && !(atStart && heads === 0))
227
+ voice.state = 'live';
228
+ }
229
+ // A projection reads a finished ramp as weight 0, and leaves the live voice's subjects be.
230
+ if (voice.parts !== null && !mix.projecting)
231
+ for (const [subject, r] of voice.parts)
232
+ if (now - r.at >= r.over)
233
+ part(mix, voice, subject, r.at + r.over);
234
+ const back = voice.back;
235
+ if (back !== null && now >= back.at + (1 - back.from) * back.over && !mix.projecting) {
236
+ voice.back = null;
237
+ mix.lanes?.refill();
238
+ }
239
+ if (voice.state === 'fading' && voice.out) {
240
+ const { at, over, rest, deadline } = voice.out;
241
+ const spent = now - at;
242
+ if (rest) {
243
+ const out = deadline !== undefined && spent >= deadline;
244
+ const settled = voice.seen > 0 && voice.restedCount >= voice.seen;
245
+ if (out)
246
+ retire(mix, voice, at + deadline);
247
+ else if (settled)
248
+ retire(mix, voice);
249
+ }
250
+ else if (spent >= over) {
251
+ if (voice.out.stays)
252
+ stay(mix, voice, at + over);
253
+ else
254
+ retire(mix, voice, at + over);
255
+ }
256
+ }
257
+ if (!mix.projecting)
258
+ schedule(mix, voice);
259
+ return false;
260
+ }
261
+ /**
262
+ * Whether a pending voice starts with its owner's clock at `t`: at its very start only on a clock
263
+ * not running back, where it would leave as it starts, as one passed between frames never starts.
264
+ */
265
+ function starts(voice, t, now) {
266
+ return (t > voice.start ||
267
+ (t === voice.start && (voice.owner === null || heading(voice.owner, now) >= 0)));
268
+ }
269
+ /**
270
+ * Whether a span's child starts, comes back, or reaches the end of its passes by `now`: a fit from
271
+ * an end earlier in the frame can move any of these, so they wait for the ends before them. One
272
+ * kept quiet at its start comes back as its clock passes it. Past its end, or its start, on a clock
273
+ * not running forward, a seek of an owner took it there going forward. Takes first the changes a
274
+ * fit made to it that its owner's clock has come back to, as `visitOne` would.
275
+ */
276
+ function meets(mix, voice, now) {
277
+ if (voice.earlier !== null || voice.later !== null)
278
+ unfit(mix, voice, now);
279
+ return meetsAt(voice, ownerReading(voice.owner, now), now);
280
+ }
281
+ /** `meets`, given what its owner's clock reads at `now`, `t`. */
282
+ function meetsAt(voice, t, now) {
283
+ if (voice.state === 'pending')
284
+ return starts(voice, t, now);
285
+ if (voice.state !== 'live')
286
+ return false;
287
+ const out = voice.out;
288
+ if (voice.fitting !== null)
289
+ return out === null && lapses(voice, now);
290
+ if (!Number.isFinite(voice.span))
291
+ return false;
292
+ if (out !== null && !(out.stays === true && out.back === true))
293
+ return false;
294
+ const at = t === Number.NEGATIVE_INFINITY ? t : elapsedWith(voice, t);
295
+ const edge = out === null ? voice.span + voice.latest : voice.soonest;
296
+ return at > edge || (at === edge && heading(voice, now) > 0);
297
+ }
298
+ /**
299
+ * Visits span children that start, come back or end within the frame in the order those happen,
300
+ * one at a time, since a fit from each end can move the starts and ends after it. A start or a
301
+ * return only does that here; a child a fit moves to meet `now`, or whose span starts, joins them.
302
+ */
303
+ function inTurn(mix, held, now) {
304
+ // A lone start of a child holding nothing moves nothing else: it starts and is visited.
305
+ const lone = held.length === 1 ? held[0] : null;
306
+ if (lone !== null && lone.state === 'pending' && lone.holding === null) {
307
+ begin(mix, lone, now);
308
+ if (!meets(mix, lone, now)) {
309
+ visitOne(mix, lone, now);
310
+ return;
311
+ }
312
+ }
313
+ // `held` is this frame's own list, so it is the one the children waiting their turn join.
314
+ const waiting = held;
315
+ const ended = [];
316
+ for (;;) {
317
+ let next;
318
+ let when = Number.POSITIVE_INFINITY;
319
+ for (let i = 0; i < waiting.length; i++) {
320
+ const v = waiting[i];
321
+ if (v.state === 'done' || ended.includes(v) || !meets(mix, v, now))
322
+ continue;
323
+ const t = v.state === 'pending'
324
+ ? startOf(v)
325
+ : v.fitting !== null
326
+ ? lapseOf(v)
327
+ : v.out !== null
328
+ ? v.reaches(v.soonest)
329
+ : v.endsAt();
330
+ // At one time an end comes first, as a fit from it counts a start there as still to come.
331
+ if (next === undefined || t < when || (t === when && rank(v) < rank(next))) {
332
+ next = v;
333
+ when = t;
334
+ }
335
+ }
336
+ if (next === undefined)
337
+ break;
338
+ if (next.state === 'pending') {
339
+ begin(mix, next, now);
340
+ if (next.holding !== null)
341
+ join(mix, next, now, waiting, ended);
342
+ }
343
+ else if (next.out !== null)
344
+ rouse(mix, next, now);
345
+ else {
346
+ ended.push(next);
347
+ if (next.fitting !== null)
348
+ release(mix, next, when);
349
+ else
350
+ visitOne(mix, next, now);
351
+ for (let o = next.owner; o?.fitting; o = o.owner)
352
+ join(mix, o, now, waiting, ended);
353
+ }
354
+ }
355
+ byId(waiting);
356
+ for (let i = 0; i < waiting.length; i++) {
357
+ const v = waiting[i];
358
+ if (!ended.includes(v) && v.state !== 'done')
359
+ visitOne(mix, v, now);
360
+ }
361
+ }
362
+ /** When a span's budget ran out, mix time. */
363
+ function lapseOf(span) {
364
+ return span.timeAt(span.fitting.budget);
365
+ }
366
+ /** Adds to `waiting` the children of `o` that meet `now` and have not yet ended this frame. */
367
+ function join(mix, o, now, waiting, ended) {
368
+ const children = o.holding.children;
369
+ for (let i = 0; i < children.length; i++) {
370
+ const c = children[i];
371
+ if (c.state !== 'done' && !ended.includes(c) && !waiting.includes(c) && meets(mix, c, now))
372
+ waiting.push(c);
373
+ }
374
+ }
375
+ /** Which of two events at one time comes first: an end before a start or return, then cue order. */
376
+ function rank(v) {
377
+ return (v.state === 'pending' || v.out !== null ? 2 ** 40 : 0) + v.id;
378
+ }
152
379
  /** Starts each voice cued while the mix slept at `now`, the first sync after its cue. */
153
380
  function wake(mix, now) {
154
381
  mix.waking = false;
package/dist/origin.d.ts CHANGED
@@ -19,3 +19,5 @@ export declare function leave<I, O>(voice: Voice<I, O>, u: number, sought?: bool
19
19
  * `rebuilt` starts from the last seek that rebuilt state, as a stateful record does.
20
20
  */
21
21
  export declare function originOf<I, O>(voice: Voice<I, O>, elapsed: number, rebuilt: boolean): number;
22
+ /** The mix time a voice started, or will by its owners' controls now: where its clock read 0. */
23
+ export declare function startedAt<I, O>(voice: Voice<I, O>): number;
package/dist/origin.js CHANGED
@@ -80,3 +80,7 @@ export function originOf(voice, elapsed, rebuilt) {
80
80
  }
81
81
  return voice.owner === null ? t : originOf(voice.owner, t, rebuilt);
82
82
  }
83
+ /** The mix time a voice started, or will by its owners' controls now: where its clock read 0. */
84
+ export function startedAt(voice) {
85
+ return voice.owner === null ? voice.start : originOf(voice.owner, voice.start, false);
86
+ }
package/dist/owner.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { arcShare } from './arcs.js';
2
2
  import { backWith, clampWeight, elapsedWith, envelope, timeWith } from './clock.js';
3
+ import { startedAt } from './origin.js';
3
4
  /** What an owner is cued with in place of a patch: it writes nothing, so no fold ever meets it. */
4
5
  export const ownerPatch = Object.freeze({
5
6
  form: 'fn',
@@ -132,9 +133,10 @@ export function heldByInput(voice) {
132
133
  */
133
134
  export function ownWeight(owner, now, reduced, base) {
134
135
  const b = typeof owner.spec.weight === 'function' ? base(owner) : owner.weight;
135
- const start = owner.owner === null ? owner.start : mixTime(owner.owner, owner.start);
136
+ const start = startedAt(owner);
136
137
  const since = owner.freezesBefore && owner.opened < start ? owner.opened : start;
137
- return clampWeight(b * envelope(owner.fade.in ?? 0, owner.out, owner.ease, reduced, now, since, owner.back));
138
+ return clampWeight(b *
139
+ envelope(owner.fade.in ?? 0, owner.out, owner.ease, reduced, now, since, owner.back, owner.stalls));
138
140
  }
139
141
  /** What every owner above a voice multiplies into its weight. */
140
142
  export function ownersWeight(voice, now, reduced, base) {
package/dist/paging.d.ts CHANGED
@@ -22,9 +22,13 @@ export declare class HistoryMiss extends Error {
22
22
  /** A transport's history store: what left memory, and what `prepare` loaded back. */
23
23
  export declare class Pager {
24
24
  readonly store: HistoryStore;
25
- /** What the last `prepare` loaded, and every record paged since, until a seek or read takes it. */
25
+ /**
26
+ * What the last `prepare` loaded at mix time `now`, and every record paged since, until a seek or
27
+ * read takes it, or memory's length of mix time passes.
28
+ */
26
29
  loaded: {
27
30
  t: number;
31
+ now: number;
28
32
  records: Paged[];
29
33
  } | null;
30
34
  /** The latest record paged, which a miss names. */
package/dist/paging.js CHANGED
@@ -23,7 +23,10 @@ export class HistoryMiss extends Error {
23
23
  /** A transport's history store: what left memory, and what `prepare` loaded back. */
24
24
  export class Pager {
25
25
  store;
26
- /** What the last `prepare` loaded, and every record paged since, until a seek or read takes it. */
26
+ /**
27
+ * What the last `prepare` loaded at mix time `now`, and every record paged since, until a seek or
28
+ * read takes it, or memory's length of mix time passes.
29
+ */
27
30
  loaded = null;
28
31
  /** The latest record paged, which a miss names. */
29
32
  last = null;
@@ -94,7 +97,14 @@ export function pageOut(mix, stream, voice, subject, out, data, reach) {
94
97
  export function page(transport, records, reach) {
95
98
  const pager = transport.pager;
96
99
  pager.store.page(records);
97
- pager.loaded?.records.push(...records);
100
+ const loaded = pager.loaded;
101
+ if (loaded !== null) {
102
+ // Left unread for longer than memory reaches, it is let go: the store still has it all.
103
+ if (transport.now - loaded.now > (transport.history?.ms ?? 0))
104
+ pager.loaded = null;
105
+ else
106
+ loaded.records.push(...records);
107
+ }
98
108
  const last = records[records.length - 1];
99
109
  pager.last = { mix: last.mix, stream: last.stream };
100
110
  if (reach > transport.floor)
@@ -141,5 +151,5 @@ export async function prepare(transport, t) {
141
151
  if (pager === null || transport.reaches(t))
142
152
  return;
143
153
  const records = await pager.store.load(t);
144
- pager.loaded = { t, records: [...records] };
154
+ pager.loaded = { t, now: transport.now, records: [...records] };
145
155
  }
package/dist/patch.js CHANGED
@@ -262,13 +262,33 @@ slope, duration, into, reuse) {
262
262
  return value + bend(slope, aValue, bValue);
263
263
  if (Array.isArray(value) && Array.isArray(slope)) {
264
264
  const out = value === lastRead.wrote ? value : new Array(value.length);
265
+ // A stop shorter than the channel reads its missing axes as the lerp does, at each end.
266
+ let ends;
267
+ const endsOf = () => {
268
+ const by = track.lerp ?? lerp;
269
+ const at = (t) => into !== undefined
270
+ ? into(undefined, aValue, bValue, t)
271
+ : by
272
+ ? by(aValue, bValue, t)
273
+ : interpolate(aValue, bValue, t);
274
+ ends = [at(0), at(1)];
275
+ return ends;
276
+ };
265
277
  for (let i = 0; i < value.length; i++) {
266
278
  const v = value[i];
267
279
  const s = slope[i];
268
- out[i] =
269
- typeof v === 'number' && typeof s === 'number'
270
- ? v + bend(s, aValue[i], bValue[i])
271
- : v;
280
+ if (typeof v !== 'number' || typeof s !== 'number') {
281
+ out[i] = v;
282
+ continue;
283
+ }
284
+ let a = aValue[i];
285
+ let z = bValue[i];
286
+ if (typeof a !== 'number' || typeof z !== 'number') {
287
+ const [a0, z1] = ends ?? endsOf();
288
+ a = a0?.[i];
289
+ z = z1?.[i];
290
+ }
291
+ out[i] = typeof a === 'number' && typeof z === 'number' ? v + bend(s, a, z) : v;
272
292
  }
273
293
  return out;
274
294
  }
package/dist/place.js CHANGED
@@ -56,39 +56,75 @@ export function checkPlacement(mix, spec, anchor, owner) {
56
56
  throw new Error('blits: a placement anchors out or end, not both');
57
57
  if (spec.start !== undefined && (anchor.start !== undefined || anchor.in !== undefined))
58
58
  throw new Error('blits: a voice takes start or an anchored start, not both');
59
- const name = spec.name;
60
- if (name === undefined)
61
- return;
62
- // A voice is known by its owner, its score and its name, and by its mix where it names no score;
63
- // a bare name in an anchor is in the asker's score under the asker's owner.
64
- const key = (o, score, n, slot) => `${score === undefined ? slot : ''}\u0000${o?.id ?? 0}\u0000${score ?? ''}\u0000${n}`;
65
- const named = (a, score, o, slot) => {
59
+ const self = {
60
+ id: mix.nextId,
61
+ spec,
62
+ owner,
63
+ slot: mix.slot,
64
+ writes: spec.patch?.writes ?? none,
65
+ };
66
+ const voices = [self];
67
+ for (const m of mix.transport.members)
68
+ for (const list of [m.gone, m.cued])
69
+ for (const v of list)
70
+ voices.push({
71
+ id: v.id,
72
+ spec: v.spec,
73
+ owner: v.owner,
74
+ slot: m.slot,
75
+ writes: v.patch.writes,
76
+ });
77
+ const refuse = () => {
78
+ throw new Error(`blits: ${spec.name ?? 'a voice'}'s placement waits on itself`);
79
+ };
80
+ // Where the marks a query asks for are fixed: a voice's start, in, coast and inner marks by its
81
+ // start's anchors, its out and end by its out's anchors and its start.
82
+ const targets = (asker, a) => {
66
83
  if ('all' in a)
67
- return a.all.flatMap((m) => named(m, score, o, slot));
84
+ return a.all.flatMap((m) => targets(asker, m));
68
85
  if ('any' in a)
69
- return a.any.flatMap((m) => named(m, score, o, slot));
70
- const q = 'of' in a ? a.of : 'after' in a ? a.after : 'with' in a ? a.with : a.before;
71
- if (typeof q === 'string')
72
- return [key(o, score, q, slot)];
73
- return q.name === undefined ? [] : [key(o, q.score ?? score, q.name, slot)];
86
+ return a.any.flatMap((m) => targets(asker, m));
87
+ const given = 'of' in a ? a.of : 'after' in a ? a.after : 'with' in a ? a.with : a.before;
88
+ const mark = 'of' in a ? a.mark : 'after' in a ? 'end' : 'start';
89
+ const q = typeof given === 'string' ? { name: given } : given;
90
+ const score = q.score ?? asker.spec.score;
91
+ const anywhere = q.score !== undefined;
92
+ if (asker === self && q.name !== undefined && q.name === spec.name && score === spec.score)
93
+ refuse();
94
+ const met = voices.filter((v) => v !== asker &&
95
+ v.spec.score === score &&
96
+ (anywhere || (v.owner === asker.owner && v.slot === asker.slot)) &&
97
+ (q.name === undefined || v.spec.name === q.name) &&
98
+ (q.tag === undefined || (v.spec.tags ?? none).includes(q.tag)) &&
99
+ (q.writes === undefined || v.writes.includes(q.writes)));
100
+ const resolver = q.resolver ?? 'last';
101
+ const picked = met.length > 1 && (resolver === 'first' || resolver === 'last')
102
+ ? [met.reduce((p, v) => ((resolver === 'last' ? v.id > p.id : v.id < p.id) ? v : p))]
103
+ : met;
104
+ const side = mark === 'out' || mark === 'end' ? 'out' : 'start';
105
+ return picked.map((v) => ({ voice: v, side }));
74
106
  };
75
- 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));
76
- const self = key(owner, spec.score, name, mix.slot);
77
- const seen = new Set();
78
- const waits = names(anchor, spec.score, owner, mix.slot);
79
- while (waits.length > 0) {
80
- const n = waits.pop();
81
- if (n === self)
82
- throw new Error(`blits: ${name}'s placement waits on itself`);
83
- if (seen.has(n))
84
- continue;
85
- seen.add(n);
86
- for (const m of mix.transport.members)
87
- for (const v of m.cued)
88
- if (v.spec.name !== undefined &&
89
- key(v.owner, v.spec.score, v.spec.name, m.slot) === n &&
90
- v.spec.anchor)
91
- waits.push(...names(v.spec.anchor, v.spec.score, v.owner, m.slot));
107
+ const waitsOf = (p, side) => {
108
+ const a = p.spec.anchor;
109
+ const by = side === 'start' ? [a?.start, a?.in] : [a?.out, a?.end];
110
+ const out = side === 'out' ? [{ voice: p, side: 'start' }] : [];
111
+ for (const b of by)
112
+ if (b !== undefined && typeof b !== 'number')
113
+ out.push(...targets(p, b));
114
+ return out;
115
+ };
116
+ for (const from of ['start', 'out']) {
117
+ const seen = { start: new Set(), out: new Set() };
118
+ const stack = waitsOf(self, from);
119
+ while (stack.length > 0) {
120
+ const { voice, side } = stack.pop();
121
+ if (voice === self && side === from)
122
+ refuse();
123
+ if (seen[side].has(voice))
124
+ continue;
125
+ seen[side].add(voice);
126
+ stack.push(...waitsOf(voice, side));
127
+ }
92
128
  }
93
129
  }
94
130
  /**
@@ -158,7 +194,7 @@ export function place(mix) {
158
194
  export function resolve(mix, a, self, before) {
159
195
  const o = self.owner;
160
196
  if (typeof a === 'number')
161
- return o === null ? mixAt(mix, a - mix.offset) : a;
197
+ return o === null ? mixAt(mix, a - self.offset) : a;
162
198
  if ('all' in a) {
163
199
  let latest = Number.NEGATIVE_INFINITY;
164
200
  for (const m of a.all) {