@msb235/blits 0.6.0 → 0.7.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 (96) hide show
  1. package/CHANGELOG.md +136 -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 +78 -27
  21. package/dist/due.js +3 -0
  22. package/dist/fade.d.ts +13 -0
  23. package/dist/fade.js +55 -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/handle.d.ts +27 -2
  33. package/dist/handle.js +60 -11
  34. package/dist/held.js +4 -0
  35. package/dist/history.d.ts +15 -2
  36. package/dist/history.js +54 -9
  37. package/dist/hosts.d.ts +2 -2
  38. package/dist/hosts.js +18 -5
  39. package/dist/index.d.ts +8 -2
  40. package/dist/index.js +5 -1
  41. package/dist/lane.d.ts +1 -1
  42. package/dist/lanes.d.ts +32 -9
  43. package/dist/lanes.js +53 -24
  44. package/dist/locus.d.ts +3 -3
  45. package/dist/locus.js +1 -1
  46. package/dist/marks.d.ts +3 -0
  47. package/dist/marks.js +80 -12
  48. package/dist/meet.d.ts +3 -2
  49. package/dist/meet.js +15 -3
  50. package/dist/mixer.d.ts +52 -36
  51. package/dist/mixer.js +148 -86
  52. package/dist/motion.d.ts +2 -199
  53. package/dist/motion.js +2 -652
  54. package/dist/motions.d.ts +266 -0
  55. package/dist/motions.js +724 -0
  56. package/dist/move.d.ts +1 -1
  57. package/dist/move.js +23 -7
  58. package/dist/owner.d.ts +1 -1
  59. package/dist/owner.js +3 -3
  60. package/dist/pace.d.ts +2 -0
  61. package/dist/pace.js +8 -0
  62. package/dist/pack.d.ts +31 -0
  63. package/dist/pack.js +65 -0
  64. package/dist/paging.d.ts +63 -0
  65. package/dist/paging.js +110 -0
  66. package/dist/patch.d.ts +3 -0
  67. package/dist/patch.js +2 -0
  68. package/dist/place.js +74 -25
  69. package/dist/project.d.ts +7 -4
  70. package/dist/project.js +117 -78
  71. package/dist/pull.js +4 -3
  72. package/dist/qualify.js +4 -2
  73. package/dist/revive.d.ts +58 -0
  74. package/dist/revive.js +158 -0
  75. package/dist/sample.d.ts +1 -1
  76. package/dist/sample.js +6 -1
  77. package/dist/seek.d.ts +3 -0
  78. package/dist/seek.js +311 -0
  79. package/dist/spans.d.ts +29 -0
  80. package/dist/spans.js +165 -0
  81. package/dist/strays.d.ts +1 -1
  82. package/dist/strays.js +21 -0
  83. package/dist/tape.d.ts +21 -0
  84. package/dist/tape.js +65 -0
  85. package/dist/ticker.d.ts +28 -3
  86. package/dist/ticker.js +3 -0
  87. package/dist/transport.d.ts +121 -0
  88. package/dist/transport.js +271 -0
  89. package/dist/types.d.ts +404 -38
  90. package/dist/unpage.d.ts +8 -0
  91. package/dist/unpage.js +163 -0
  92. package/dist/voice.d.ts +73 -7
  93. package/dist/voice.js +116 -4
  94. package/dist/weigh.js +2 -2
  95. package/docs/schema.html +120 -31
  96. package/package.json +2 -1
package/dist/move.d.ts CHANGED
@@ -6,4 +6,4 @@ export declare function waits<I, O>(mix: Mixer<I, O>): boolean;
6
6
  /** Moves the mix clock to `now`: voices start, finite loops end, and fades finish. */
7
7
  export declare function move<I, O>(mix: Mixer<I, O>, now: number): void;
8
8
  /** Lets go of the voices that have left and that history no longer reaches. */
9
- export declare function forget<I, O>(mix: Mixer<I, O>, now: number): void;
9
+ export declare function forget<I, O>(mix: Mixer<I, O>): void;
package/dist/move.js CHANGED
@@ -4,6 +4,8 @@ import { beginFade, part, retire } from './fade.js';
4
4
  import { started } from './held.js';
5
5
  import { ownerReading } from './owner.js';
6
6
  import { mixAt, place, repin, startOf } from './place.js';
7
+ import { pageVoice } from './revive.js';
8
+ import { lapse } from './spans.js';
7
9
  import { unplay } from './strays.js';
8
10
  /** Numbers every mix's frames apart, so a blend's read is never taken for another mix's or frame's. */
9
11
  let frames = 0;
@@ -38,7 +40,7 @@ function moveTo(mix, now) {
38
40
  place(mix);
39
41
  if (mix.announced.length > 0) {
40
42
  // Kept while still ahead, or while history reaches it; anchors waiting on one were placed above.
41
- const reach = now - (mix.opts.history?.ms ?? 0);
43
+ const reach = mix.opts.history === undefined ? now : mix.transport.keepsFrom();
42
44
  mix.announced = mix.announced.filter((a) => mixAt(mix, a.at) >= reach);
43
45
  }
44
46
  // A live mix visits only the voices due by now; a projection, which copies few, visits all.
@@ -49,7 +51,7 @@ function moveTo(mix, now) {
49
51
  if (Number.isNaN(voice.opened))
50
52
  voice.opened = now;
51
53
  if ((voice.state === 'live' || voice.state === 'frozen') && voice.outAt <= now)
52
- beginFade(mix, voice, {}, Math.max(startOf(voice), voice.outAt));
54
+ beginFade(mix, voice, { over: Number.isNaN(voice.outOver) ? undefined : voice.outOver }, Math.max(startOf(voice), voice.outAt));
53
55
  if (voice.state === 'pending' &&
54
56
  (voice.owner === null ? now : ownerReading(voice.owner, now)) >= voice.start) {
55
57
  voice.state = 'live';
@@ -59,7 +61,7 @@ function moveTo(mix, now) {
59
61
  if (voice.state !== 'pending' && Number.isFinite(voice.span)) {
60
62
  const end = voice.span + voice.latest;
61
63
  if (voice.elapsedAt(now) >= end) {
62
- voice.play(true);
64
+ voice.play(true, now);
63
65
  // The fade starts when the last pass ended, not at the frame that noticed, so it plays
64
66
  // the same at any frame rate and a read at another time can find it.
65
67
  if (voice.state === 'live') {
@@ -77,6 +79,11 @@ function moveTo(mix, now) {
77
79
  for (const [subject, r] of voice.parts)
78
80
  if (now - r.at >= r.over)
79
81
  part(mix, voice, subject, r.at + r.over);
82
+ const back = voice.back;
83
+ if (back !== null && now >= back.at + (1 - back.from) * back.over && !mix.projecting) {
84
+ voice.back = null;
85
+ mix.lanes?.refill();
86
+ }
80
87
  if (voice.state === 'fading' && voice.out) {
81
88
  const { at, over, rest, deadline } = voice.out;
82
89
  const spent = now - at;
@@ -94,10 +101,12 @@ function moveTo(mix, now) {
94
101
  if (!mix.projecting)
95
102
  schedule(mix, voice);
96
103
  }
104
+ if (mix.owners !== null)
105
+ lapse(mix, now);
97
106
  mix.stirred = false;
98
107
  if (mix.retired.length > 0)
99
108
  prune(mix);
100
- forget(mix, now);
109
+ forget(mix);
101
110
  }
102
111
  /** Past this many voices retiring in one frame, one pass over the list beats a search for each. */
103
112
  const SEARCHED = 8;
@@ -181,10 +190,17 @@ function leave(mix, voice) {
181
190
  return voice.named === null;
182
191
  }
183
192
  /** Lets go of the voices that have left and that history no longer reaches. */
184
- export function forget(mix, now) {
193
+ export function forget(mix) {
185
194
  const history = mix.opts.history;
186
195
  if (history && mix.gone.length > 0) {
187
- const reach = now - history.ms;
188
- mix.gone = mix.gone.filter((v) => v.doneAt >= reach);
196
+ // With a store a voice that has left pages out where it can, and stays where it cannot.
197
+ if (mix.transport.pager !== null) {
198
+ const reach = mix.now - history.ms;
199
+ mix.gone = mix.gone.filter((v) => v.doneAt >= reach || !pageVoice(mix, v, reach));
200
+ }
201
+ else {
202
+ const reach = mix.transport.keepsFrom();
203
+ mix.gone = mix.gone.filter((v) => v.doneAt >= reach);
204
+ }
189
205
  }
190
206
  }
package/dist/owner.d.ts CHANGED
@@ -24,7 +24,7 @@ export declare function adopt<I, O>(owner: Voice<I, O>, child: Voice<I, O>): voi
24
24
  /** A child left: its owner, where that was its last child and it should leave too. */
25
25
  export declare function orphan<I, O>(child: Voice<I, O>): Voice<I, O> | null;
26
26
  /** A child finished its passes: its owner has played once every child it held has. */
27
- export declare function childPlayed<I, O>(owner: Voice<I, O>): void;
27
+ export declare function childPlayed<I, O>(owner: Voice<I, O>, at: number): void;
28
28
  /** Every voice an owner holds, its children's children included. */
29
29
  export declare function descendants<I, O>(owner: Voice<I, O>, visit: (v: Voice<I, O>) => void): void;
30
30
  /** Whether any owner above a voice reads a signal for its weight, which lanes cannot fill. */
package/dist/owner.js CHANGED
@@ -48,10 +48,10 @@ export function orphan(child) {
48
48
  return h.children.length === 0 && owner.state !== 'done' ? owner : null;
49
49
  }
50
50
  /** A child finished its passes: its owner has played once every child it held has. */
51
- export function childPlayed(owner) {
51
+ export function childPlayed(owner, at) {
52
52
  const h = owner.holding;
53
53
  if (++h.played === h.kids)
54
- owner.play(true);
54
+ owner.play(true, at);
55
55
  }
56
56
  /** Every voice an owner holds, its children's children included. */
57
57
  export function descendants(owner, visit) {
@@ -86,7 +86,7 @@ export function ownWeight(owner, now, reduced, base) {
86
86
  const b = typeof owner.spec.weight === 'function' ? base(owner) : owner.weight;
87
87
  const start = owner.owner === null ? owner.start : mixTime(owner.owner, owner.start);
88
88
  const since = owner.freezesBefore && owner.opened < start ? owner.opened : start;
89
- return clampWeight(b * envelope(owner.fade.in ?? 0, owner.out, owner.ease, reduced, now, since));
89
+ return clampWeight(b * envelope(owner.fade.in ?? 0, owner.out, owner.ease, reduced, now, since, owner.back));
90
90
  }
91
91
  /** What every owner above a voice multiplies into its weight. */
92
92
  export function ownersWeight(voice, now, reduced, base) {
package/dist/pace.d.ts CHANGED
@@ -35,6 +35,8 @@ export declare class Pace {
35
35
  * time `reach`.
36
36
  */
37
37
  change(u: number, rate: number, over: number, reach: number): void;
38
+ /** Lets go of the clocks set after host time `u`, for a seek back to then. */
39
+ cut(u: number): void;
38
40
  /** A copy holding the clocks that took effect before host time `u`, for a read back to then. */
39
41
  until(u: number): Pace;
40
42
  }
package/dist/pace.js CHANGED
@@ -102,6 +102,14 @@ export class Pace {
102
102
  if (drop > 0)
103
103
  log.splice(0, drop);
104
104
  }
105
+ /** Lets go of the clocks set after host time `u`, for a seek back to then. */
106
+ cut(u) {
107
+ const log = this.log;
108
+ let n = 1;
109
+ while (n < log.length && log[n].anchorNow <= u)
110
+ n++;
111
+ log.length = n;
112
+ }
105
113
  /** A copy holding the clocks that took effect before host time `u`, for a read back to then. */
106
114
  until(u) {
107
115
  const log = this.log;
package/dist/pack.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { Subject, Voice } from './voice.js';
2
+ /** A subject's record as plain data, for a history store. */
3
+ export interface PackedRecord {
4
+ reaches: boolean;
5
+ delay: number;
6
+ since: number;
7
+ shown: number;
8
+ weight: number;
9
+ rested: boolean;
10
+ bands: number[] | null;
11
+ state: unknown;
12
+ stepped: number;
13
+ ticks: number;
14
+ /**
15
+ * What `setting.keep` held, in the order its owners first kept it. An owner is code, which no
16
+ * store keeps, and a voice revived from a descriptor has new ones; the same code keeps in the same
17
+ * order, so each owner takes the next value on its first keep.
18
+ */
19
+ kept: unknown[] | null;
20
+ base?: Record<string, unknown>;
21
+ slope?: Record<string, unknown>;
22
+ snaps?: [at: number, held: PackedRecord][];
23
+ inputs?: {
24
+ at: number;
25
+ value: number;
26
+ }[];
27
+ }
28
+ /** A voice's record of one subject as plain data, its patch's `pack` taking its state. */
29
+ export declare function packHeld<I, O>(voice: Voice<I, O>, h: Subject<unknown>): PackedRecord;
30
+ /** A record made again from `packHeld`'s data, for `voice`. */
31
+ export declare function unpackHeld<I, O>(voice: Voice<I, O>, p: PackedRecord): Subject<unknown>;
package/dist/pack.js ADDED
@@ -0,0 +1,65 @@
1
+ import { clone } from './clone.js';
2
+ /** A voice's record of one subject as plain data, its patch's `pack` taking its state. */
3
+ export function packHeld(voice, h) {
4
+ const patch = voice.patch;
5
+ const kept = [...(h.kept?.values() ?? []), ...(h.unkept ?? [])];
6
+ const p = {
7
+ reaches: h.reaches,
8
+ delay: h.delay,
9
+ since: h.since,
10
+ shown: h.shown,
11
+ weight: h.weight,
12
+ rested: h.rested,
13
+ bands: h.bands === null ? null : Array.from(h.bands),
14
+ state: h.state === undefined ? undefined : patch.pack ? patch.pack(h.state) : clone(h.state),
15
+ stepped: h.stepped,
16
+ ticks: h.ticks,
17
+ kept: kept.length === 0 ? null : kept.map(clone),
18
+ };
19
+ if (h.base !== undefined)
20
+ p.base = clone(h.base);
21
+ if (h.slope !== undefined)
22
+ p.slope = clone(h.slope);
23
+ if (h.snaps !== undefined)
24
+ p.snaps = h.snaps.map((s) => [s.at, packHeld(voice, s.held)]);
25
+ if (h.inputs !== undefined)
26
+ p.inputs = h.inputs.map((e) => ({ ...e }));
27
+ return p;
28
+ }
29
+ /** A record made again from `packHeld`'s data, for `voice`. */
30
+ export function unpackHeld(voice, p) {
31
+ const patch = voice.patch;
32
+ const h = {
33
+ reaches: p.reaches,
34
+ delay: p.delay,
35
+ since: p.since,
36
+ shown: p.shown,
37
+ weight: p.weight,
38
+ rested: p.rested,
39
+ bands: p.bands === null ? null : Uint8Array.from(p.bands),
40
+ state: p.state === undefined ? undefined : patch.unpack ? patch.unpack(p.state) : p.state,
41
+ stepped: p.stepped,
42
+ ticks: p.ticks,
43
+ probed: Number.NaN,
44
+ delta: null,
45
+ phase: 0,
46
+ seeks: 0,
47
+ kept: null,
48
+ voice,
49
+ next: null,
50
+ version: Number.NaN,
51
+ loci: null,
52
+ slot: -1,
53
+ };
54
+ if (p.kept !== null)
55
+ h.unkept = p.kept;
56
+ if (p.base !== undefined)
57
+ h.base = p.base;
58
+ if (p.slope !== undefined)
59
+ h.slope = p.slope;
60
+ if (p.snaps !== undefined)
61
+ h.snaps = p.snaps.map(([at, held]) => ({ at, held: unpackHeld(voice, held) }));
62
+ if (p.inputs !== undefined)
63
+ h.inputs = p.inputs;
64
+ return h;
65
+ }
@@ -0,0 +1,63 @@
1
+ import type { Mixer } from './mixer.js';
2
+ import type { Transport } from './transport.js';
3
+ import type { HistoryStore, Paged, PagedStream } from './types.js';
4
+ /**
5
+ * A `seek` or `project` reached past memory into records `prepare` had not loaded. Thrown before
6
+ * anything moves, so a restore is never partial: `await prepare(at)` and ask again.
7
+ *
8
+ * @category mix
9
+ */
10
+ export declare class HistoryMiss extends Error {
11
+ /** The mix whose records were missing, by `name`. */
12
+ readonly mix: string;
13
+ readonly stream: string;
14
+ /** The mix time asked for. */
15
+ readonly at: number;
16
+ constructor(
17
+ /** The mix whose records were missing, by `name`. */
18
+ mix: string, stream: string,
19
+ /** The mix time asked for. */
20
+ at: number);
21
+ }
22
+ /** A transport's history store: what left memory, and what `prepare` loaded back. */
23
+ export declare class Pager {
24
+ readonly store: HistoryStore;
25
+ /** What the last `prepare` loaded, and every record paged since, until a seek or read takes it. */
26
+ loaded: {
27
+ t: number;
28
+ records: Paged[];
29
+ } | null;
30
+ /** The latest record paged, which a miss names. */
31
+ last: {
32
+ mix: string;
33
+ stream: PagedStream;
34
+ } | null;
35
+ constructor(store: HistoryStore);
36
+ }
37
+ /** A subject's key in history records; undefined for the unit subject. */
38
+ export type Key = string | number | undefined;
39
+ /**
40
+ * Object subjects by key, for a mix with a store, held weakly: a subject the host has let go of
41
+ * can never be probed again, so nothing it kept is put back.
42
+ */
43
+ export declare class Keys {
44
+ private readonly of;
45
+ private readonly refs;
46
+ private readonly gone;
47
+ constructor(of: ((subject: never) => string | number) | undefined);
48
+ /** The key a subject is paged under; throws for an object subject without `keyOf`. */
49
+ key(subject: unknown): Key;
50
+ /** The subject a key stands for: an object the mix met under it, else the key itself. */
51
+ subject(key: Key): unknown;
52
+ }
53
+ /**
54
+ * Hands records leaving memory to the store, the last of any that share a time, which is the one
55
+ * a read finds. `reach` is how far back memory still reaches.
56
+ */
57
+ export declare function pageOut<I, O, E extends {
58
+ at: number;
59
+ }>(mix: Mixer<I, O>, stream: PagedStream, voice: number | undefined, subject: Key, out: readonly E[], data: (e: E) => unknown, reach: number): void;
60
+ /** Gives records to the store; memory now reaches back no further than `reach`. */
61
+ export declare function page(transport: Transport, records: Paged[], reach: number): void;
62
+ /** Loads what a seek or read to `t` needs where it reaches past memory. */
63
+ export declare function prepare(transport: Transport, t: number): Promise<void>;
package/dist/paging.js ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * A `seek` or `project` reached past memory into records `prepare` had not loaded. Thrown before
3
+ * anything moves, so a restore is never partial: `await prepare(at)` and ask again.
4
+ *
5
+ * @category mix
6
+ */
7
+ export class HistoryMiss extends Error {
8
+ mix;
9
+ stream;
10
+ at;
11
+ constructor(
12
+ /** The mix whose records were missing, by `name`. */
13
+ mix, stream,
14
+ /** The mix time asked for. */
15
+ at) {
16
+ super(`blits: ${at} reaches past memory into ${stream} records of ${mix === '' ? 'the mix' : mix} that were not loaded; await prepare(${at}) first`);
17
+ this.mix = mix;
18
+ this.stream = stream;
19
+ this.at = at;
20
+ this.name = 'HistoryMiss';
21
+ }
22
+ }
23
+ /** A transport's history store: what left memory, and what `prepare` loaded back. */
24
+ export class Pager {
25
+ store;
26
+ /** What the last `prepare` loaded, and every record paged since, until a seek or read takes it. */
27
+ loaded = null;
28
+ /** The latest record paged, which a miss names. */
29
+ last = null;
30
+ constructor(store) {
31
+ this.store = store;
32
+ }
33
+ }
34
+ /**
35
+ * Object subjects by key, for a mix with a store, held weakly: a subject the host has let go of
36
+ * can never be probed again, so nothing it kept is put back.
37
+ */
38
+ export class Keys {
39
+ of;
40
+ refs = new Map();
41
+ gone = new FinalizationRegistry((key) => {
42
+ const ref = this.refs.get(key);
43
+ if (ref !== undefined && ref.deref() === undefined)
44
+ this.refs.delete(key);
45
+ });
46
+ constructor(of) {
47
+ this.of = of;
48
+ }
49
+ /** The key a subject is paged under; throws for an object subject without `keyOf`. */
50
+ key(subject) {
51
+ if (subject === undefined || typeof subject === 'string' || typeof subject === 'number')
52
+ return subject;
53
+ const of = this.of;
54
+ if (of === undefined || typeof subject !== 'object' || subject === null)
55
+ throw new Error('blits: a mix whose history has a store pages its subjects by key; give it keyOf for subjects that are not strings or numbers');
56
+ const key = of(subject);
57
+ if (this.refs.get(key)?.deref() !== subject) {
58
+ this.refs.set(key, new WeakRef(subject));
59
+ this.gone.register(subject, key);
60
+ }
61
+ return key;
62
+ }
63
+ /** The subject a key stands for: an object the mix met under it, else the key itself. */
64
+ subject(key) {
65
+ if (key === undefined)
66
+ return undefined;
67
+ return this.refs.get(key)?.deref() ?? key;
68
+ }
69
+ }
70
+ /**
71
+ * Hands records leaving memory to the store, the last of any that share a time, which is the one
72
+ * a read finds. `reach` is how far back memory still reaches.
73
+ */
74
+ export function pageOut(mix, stream, voice, subject, out, data, reach) {
75
+ const pager = mix.transport.pager;
76
+ if (pager === null || out.length === 0)
77
+ return;
78
+ const name = mix.name ?? '';
79
+ const records = [];
80
+ for (let i = 0; i < out.length; i++) {
81
+ const e = out[i];
82
+ if (i + 1 < out.length && out[i + 1].at === e.at)
83
+ continue;
84
+ const r = { mix: name, stream, at: e.at, data: data(e) };
85
+ if (voice !== undefined)
86
+ r.voice = voice;
87
+ if (subject !== undefined)
88
+ r.subject = subject;
89
+ records.push(r);
90
+ }
91
+ page(mix.transport, records, reach);
92
+ }
93
+ /** Gives records to the store; memory now reaches back no further than `reach`. */
94
+ export function page(transport, records, reach) {
95
+ const pager = transport.pager;
96
+ pager.store.page(records);
97
+ pager.loaded?.records.push(...records);
98
+ const last = records[records.length - 1];
99
+ pager.last = { mix: last.mix, stream: last.stream };
100
+ if (reach > transport.floor)
101
+ transport.floor = reach;
102
+ }
103
+ /** Loads what a seek or read to `t` needs where it reaches past memory. */
104
+ export async function prepare(transport, t) {
105
+ const pager = transport.pager;
106
+ if (pager === null || transport.reaches(t))
107
+ return;
108
+ const records = await pager.store.load(t);
109
+ pager.loaded = { t, records: [...records] };
110
+ }
package/dist/patch.d.ts CHANGED
@@ -15,6 +15,9 @@ export interface PatchOptions<I, O, S, H = unknown> {
15
15
  reads?: readonly string[];
16
16
  state?(subject: I): S;
17
17
  step?(state: S, dt: number, subject: I, setting: Setting<S, H>): void;
18
+ /** `state` as plain data and back, for a history store: see `Patch.pack`. */
19
+ pack?(state: S): unknown;
20
+ unpack?(data: unknown): S;
18
21
  }
19
22
  /**
20
23
  * The procedural form. Flicker, roving, a hash walk, a starter strike.
package/dist/patch.js CHANGED
@@ -19,6 +19,8 @@ export function patch(duration, at, opts) {
19
19
  at,
20
20
  state: opts.state,
21
21
  step: opts.step,
22
+ pack: opts.pack,
23
+ unpack: opts.unpack,
22
24
  };
23
25
  }
24
26
  const options = new WeakMap();
package/dist/place.js CHANGED
@@ -17,6 +17,7 @@ export function mixAt(mix, u) {
17
17
  export function pin(mix, voice, u) {
18
18
  mix.pins ??= new Map();
19
19
  mix.pins.set(voice, u);
20
+ voice.pinned = u;
20
21
  }
21
22
  /** Puts each pending voice's start, given in host time, where the mix's rate now puts it. */
22
23
  export function repin(mix) {
@@ -49,20 +50,23 @@ export function checkPlacement(mix, spec, anchor, owner) {
49
50
  const name = spec.name;
50
51
  if (name === undefined)
51
52
  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 [];
53
+ // A voice is known by its owner, its score and its name, and by its mix where it names no score;
54
+ // a bare name in an anchor is in the asker's score under the asker's owner.
55
+ const key = (o, score, n, slot) => `${score === undefined ? slot : ''}\u0000${o?.id ?? 0}\u0000${score ?? ''}\u0000${n}`;
56
+ const named = (a, score, o, slot) => {
57
+ if ('all' in a)
58
+ return a.all.flatMap((m) => named(m, score, o, slot));
59
+ if ('any' in a)
60
+ return a.any.flatMap((m) => named(m, score, o, slot));
58
61
  const q = 'of' in a ? a.of : 'after' in a ? a.after : 'with' in a ? a.with : a.before;
59
62
  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);
63
+ return [key(o, score, q, slot)];
64
+ return q.name === undefined ? [] : [key(o, q.score ?? score, q.name, slot)];
65
+ };
66
+ 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));
67
+ const self = key(owner, spec.score, name, mix.slot);
64
68
  const seen = new Set();
65
- const waits = names(anchor, spec.score, owner);
69
+ const waits = names(anchor, spec.score, owner, mix.slot);
66
70
  while (waits.length > 0) {
67
71
  const n = waits.pop();
68
72
  if (n === self)
@@ -70,11 +74,12 @@ export function checkPlacement(mix, spec, anchor, owner) {
70
74
  if (seen.has(n))
71
75
  continue;
72
76
  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));
77
+ for (const m of mix.transport.members)
78
+ for (const v of m.cued)
79
+ if (v.spec.name !== undefined &&
80
+ key(v.owner, v.spec.score, v.spec.name, m.slot) === n &&
81
+ v.spec.anchor)
82
+ waits.push(...names(v.spec.anchor, v.spec.score, v.owner, m.slot));
78
83
  }
79
84
  }
80
85
  /**
@@ -104,7 +109,7 @@ export function place(mix) {
104
109
  moved = true;
105
110
  }
106
111
  }
107
- if (voice.state !== 'fading') {
112
+ if (voice.state !== 'fading' && !voice.outSet) {
108
113
  const by = anchor.out ?? anchor.end;
109
114
  const t = by === undefined ? undefined : resolve(mix, by, voice);
110
115
  if (t !== undefined) {
@@ -127,6 +132,32 @@ function resolve(mix, a, self) {
127
132
  const o = self.owner;
128
133
  if (typeof a === 'number')
129
134
  return o === null ? mixAt(mix, a - mix.offset) : a;
135
+ if ('all' in a) {
136
+ let latest = Number.NEGATIVE_INFINITY;
137
+ for (const m of a.all) {
138
+ const t = member(mix, m, self);
139
+ if (t === undefined)
140
+ return undefined;
141
+ latest = Math.max(latest, t);
142
+ }
143
+ return a.all.length === 0 ? undefined : latest;
144
+ }
145
+ if ('any' in a) {
146
+ // A member still unknown may yet answer earlier than every known one, until one has passed.
147
+ const now = Number.isNaN(mix.now) ? Number.NEGATIVE_INFINITY : localNow(mix, self);
148
+ let earliest = Number.POSITIVE_INFINITY;
149
+ let unknown = false;
150
+ for (const m of a.any) {
151
+ const t = member(mix, m, self);
152
+ if (t === undefined)
153
+ unknown = true;
154
+ else
155
+ earliest = Math.min(earliest, t);
156
+ }
157
+ if (earliest === Number.POSITIVE_INFINITY)
158
+ return undefined;
159
+ return unknown && earliest > now ? undefined : earliest;
160
+ }
130
161
  let query;
131
162
  let mark;
132
163
  let by = a.by ?? 0;
@@ -155,6 +186,20 @@ function resolve(mix, a, self) {
155
186
  const local = ownerReading(o, t);
156
187
  return Number.isFinite(local) ? local + by : undefined;
157
188
  }
189
+ /**
190
+ * What one member of a join answers, or the time it last gave once its target has left, as a lone
191
+ * anchor keeps its voice where its target last put it.
192
+ */
193
+ function member(mix, a, self) {
194
+ const t = resolve(mix, a, self);
195
+ if (t === undefined)
196
+ return self.answers?.get(a);
197
+ if (!mix.projecting) {
198
+ self.answers ??= new Map();
199
+ self.answers.set(a, t);
200
+ }
201
+ return t;
202
+ }
158
203
  /**
159
204
  * The mix time a query answers: `mark` of the voice it picks, among those cued and those that have
160
205
  * left, never the asker; or a mark the host announced on the score, which is the same time for
@@ -165,17 +210,21 @@ function timeOf(mix, q, mark, self) {
165
210
  const score = q.score ?? self.spec.score;
166
211
  const anywhere = q.score !== undefined;
167
212
  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) });
213
+ // A query naming a score looks in every mix on the transport; one naming none, in its own.
214
+ const mixes = anywhere ? mix.transport.members : [mix];
215
+ for (const m of mixes)
216
+ for (const v of [...m.gone, ...m.cued])
217
+ if (v !== self &&
218
+ v.spec.score === score &&
219
+ (anywhere || v.owner === self.owner) &&
220
+ (q.name === undefined || v.spec.name === q.name) &&
221
+ (q.tag === undefined || (v.spec.tags ?? none).includes(q.tag)) &&
222
+ (q.writes === undefined || v.patch.writes.includes(q.writes)))
223
+ found.push({ order: v.id, t: markOf(m, v, mark) });
176
224
  if (q.writes === undefined && (anywhere || self.owner === null))
177
225
  for (const a of mix.announced)
178
226
  if (a.score === score &&
227
+ (score !== undefined || a.slot === mix.slot) &&
179
228
  (q.name === undefined || a.name === q.name) &&
180
229
  (q.tag === undefined || a.tags.includes(q.tag)))
181
230
  found.push({ order: a.order, t: Number.isNaN(a.at) ? undefined : mixAt(mix, a.at) });
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;