@msb235/blits 0.7.0 → 0.8.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 (156) hide show
  1. package/CHANGELOG.md +196 -0
  2. package/README.md +22 -4
  3. package/dist/book.js +4 -4
  4. package/dist/chain.d.ts +1 -1
  5. package/dist/chain.js +57 -54
  6. package/dist/channels.d.ts +26 -5
  7. package/dist/channels.js +12 -8
  8. package/dist/clock.d.ts +8 -1
  9. package/dist/clock.js +14 -3
  10. package/dist/clone.d.ts +3 -0
  11. package/dist/clone.js +16 -1
  12. package/dist/closed.d.ts +1 -1
  13. package/dist/closed.js +3 -2
  14. package/dist/color.d.ts +0 -11
  15. package/dist/color.js +4 -24
  16. package/dist/columns.js +0 -1
  17. package/dist/copyout.d.ts +6 -0
  18. package/dist/copyout.js +38 -0
  19. package/dist/crowd.d.ts +7 -5
  20. package/dist/crowd.js +32 -18
  21. package/dist/cue.d.ts +3 -1
  22. package/dist/cue.js +33 -20
  23. package/dist/departed.d.ts +34 -0
  24. package/dist/departed.js +73 -0
  25. package/dist/easing.d.ts +5 -0
  26. package/dist/easing.js +31 -2
  27. package/dist/everyone.d.ts +56 -0
  28. package/dist/everyone.js +221 -0
  29. package/dist/fade.d.ts +8 -1
  30. package/dist/fade.js +36 -6
  31. package/dist/fill.d.ts +22 -13
  32. package/dist/fill.js +104 -41
  33. package/dist/fit.js +2 -1
  34. package/dist/fold.d.ts +37 -0
  35. package/dist/fold.js +213 -28
  36. package/dist/gather.d.ts +15 -9
  37. package/dist/gather.js +60 -25
  38. package/dist/gone.d.ts +32 -0
  39. package/dist/gone.js +144 -0
  40. package/dist/handle.d.ts +16 -5
  41. package/dist/handle.js +56 -6
  42. package/dist/held.d.ts +8 -5
  43. package/dist/held.js +56 -58
  44. package/dist/history.d.ts +17 -13
  45. package/dist/history.js +49 -28
  46. package/dist/hosts.js +57 -9
  47. package/dist/index.d.ts +9 -4
  48. package/dist/index.js +5 -2
  49. package/dist/keyfill.d.ts +7 -0
  50. package/dist/keyfill.js +124 -0
  51. package/dist/keyrows.d.ts +1 -0
  52. package/dist/keyrows.js +1 -1
  53. package/dist/lane.d.ts +32 -2
  54. package/dist/lane.js +5 -0
  55. package/dist/lanes.d.ts +32 -8
  56. package/dist/lanes.js +69 -37
  57. package/dist/leavings.d.ts +26 -0
  58. package/dist/leavings.js +70 -0
  59. package/dist/meet.js +14 -8
  60. package/dist/mixer.d.ts +43 -7
  61. package/dist/mixer.js +60 -11
  62. package/dist/motion.d.ts +2 -0
  63. package/dist/motion.js +4 -1
  64. package/dist/motions.d.ts +22 -15
  65. package/dist/motions.js +40 -34
  66. package/dist/move.js +31 -20
  67. package/dist/moved.d.ts +11 -0
  68. package/dist/moved.js +29 -0
  69. package/dist/origin.d.ts +19 -0
  70. package/dist/origin.js +68 -0
  71. package/dist/owed.d.ts +4 -4
  72. package/dist/owed.js +8 -8
  73. package/dist/owner.d.ts +1 -1
  74. package/dist/owner.js +2 -2
  75. package/dist/pace.d.ts +13 -5
  76. package/dist/pace.js +21 -8
  77. package/dist/pack.d.ts +3 -1
  78. package/dist/pack.js +15 -24
  79. package/dist/paging.d.ts +8 -2
  80. package/dist/paging.js +44 -4
  81. package/dist/patch.d.ts +14 -9
  82. package/dist/patch.js +83 -21
  83. package/dist/place.js +188 -30
  84. package/dist/project.d.ts +12 -1
  85. package/dist/project.js +101 -83
  86. package/dist/pull.d.ts +2 -0
  87. package/dist/pull.js +8 -0
  88. package/dist/qualify.js +10 -2
  89. package/dist/reading.d.ts +5 -0
  90. package/dist/reading.js +5 -0
  91. package/dist/record.d.ts +9 -0
  92. package/dist/record.js +42 -0
  93. package/dist/relink.d.ts +11 -5
  94. package/dist/relink.js +7 -5
  95. package/dist/restored.d.ts +41 -0
  96. package/dist/restored.js +91 -0
  97. package/dist/revive.d.ts +10 -3
  98. package/dist/revive.js +31 -23
  99. package/dist/roster.d.ts +11 -0
  100. package/dist/roster.js +46 -0
  101. package/dist/rotation.d.ts +29 -0
  102. package/dist/rotation.js +91 -0
  103. package/dist/rows.d.ts +5 -5
  104. package/dist/rows.js +5 -5
  105. package/dist/sample.d.ts +3 -2
  106. package/dist/sample.js +32 -15
  107. package/dist/scored.d.ts +57 -0
  108. package/dist/scored.js +196 -0
  109. package/dist/seek.js +97 -114
  110. package/dist/shared.d.ts +6 -0
  111. package/dist/shared.js +15 -0
  112. package/dist/signals.d.ts +24 -3
  113. package/dist/signals.js +50 -11
  114. package/dist/spans.js +7 -0
  115. package/dist/stack.d.ts +11 -0
  116. package/dist/stack.js +47 -0
  117. package/dist/standing.d.ts +10 -0
  118. package/dist/standing.js +79 -0
  119. package/dist/store.d.ts +3 -3
  120. package/dist/store.js +3 -3
  121. package/dist/strays.js +3 -0
  122. package/dist/tape.d.ts +8 -5
  123. package/dist/tape.js +21 -13
  124. package/dist/testing.d.ts +35 -0
  125. package/dist/testing.js +116 -0
  126. package/dist/touch.d.ts +6 -0
  127. package/dist/touch.js +28 -0
  128. package/dist/transport.d.ts +53 -6
  129. package/dist/transport.js +102 -20
  130. package/dist/turns.d.ts +18 -0
  131. package/dist/turns.js +72 -0
  132. package/dist/tweened.js +1 -1
  133. package/dist/types/channel.d.ts +46 -0
  134. package/dist/types/channel.js +1 -0
  135. package/dist/types/history.d.ts +139 -0
  136. package/dist/types/history.js +1 -0
  137. package/dist/types/mix.d.ts +336 -0
  138. package/dist/types/mix.js +1 -0
  139. package/dist/types/patch.d.ts +187 -0
  140. package/dist/types/patch.js +1 -0
  141. package/dist/types/place.d.ts +157 -0
  142. package/dist/types/place.js +1 -0
  143. package/dist/types/transport.d.ts +71 -0
  144. package/dist/types/transport.js +1 -0
  145. package/dist/types/voice.d.ts +323 -0
  146. package/dist/types/voice.js +1 -0
  147. package/dist/types.d.ts +7 -1162
  148. package/dist/unpage.d.ts +2 -2
  149. package/dist/unpage.js +55 -25
  150. package/dist/voice.d.ts +92 -22
  151. package/dist/voice.js +108 -43
  152. package/dist/wave.d.ts +42 -0
  153. package/dist/wave.js +62 -0
  154. package/dist/weigh.js +1 -1
  155. package/docs/schema.html +42 -36
  156. package/package.json +8 -3
package/dist/gone.js ADDED
@@ -0,0 +1,144 @@
1
+ import { scoredCut } from './scored.js';
2
+ import { Store } from './store.js';
3
+ /**
4
+ * A mix's gone voices as `drop` and a freed slot look for them: those over every subject, and the
5
+ * rest by each subject they name. Kept in step with `gone`, which only grows by push and is
6
+ * otherwise replaced whole, so it catches up lazily and rebuilds only on a replacement it did not
7
+ * make itself. Under history a churning mix holds thousands, too many to walk per subject dropped.
8
+ */
9
+ export class GoneIndex {
10
+ /** The `gone` list it was built over, and how much of it. */
11
+ of = null;
12
+ count = 0;
13
+ general = [];
14
+ named = new Store();
15
+ /** The latest `doneAt` in `of`, and how far any voice finished before one listed ahead of it. */
16
+ latest = Number.NEGATIVE_INFINITY;
17
+ slack = 0;
18
+ }
19
+ function add(ix, voice) {
20
+ if (voice.doneAt < ix.latest)
21
+ ix.slack = Math.max(ix.slack, ix.latest - voice.doneAt);
22
+ else
23
+ ix.latest = voice.doneAt;
24
+ if (voice.named === null) {
25
+ ix.general.push(voice);
26
+ return;
27
+ }
28
+ for (const subject of voice.named) {
29
+ const list = ix.named.get(subject);
30
+ if (list === undefined)
31
+ ix.named.set(subject, [voice]);
32
+ else
33
+ list.push(voice);
34
+ }
35
+ }
36
+ function remove(ix, out) {
37
+ let general = false;
38
+ for (const v of out) {
39
+ if (v.named === null) {
40
+ general = true;
41
+ continue;
42
+ }
43
+ for (const subject of v.named) {
44
+ const named = ix.named.get(subject);
45
+ if (named === undefined)
46
+ continue;
47
+ const at = named.indexOf(v);
48
+ if (at >= 0)
49
+ named.splice(at, 1);
50
+ if (named.length === 0)
51
+ ix.named.delete(subject);
52
+ }
53
+ }
54
+ if (general) {
55
+ const leaving = new Set(out);
56
+ ix.general = ix.general.filter((v) => !leaving.has(v));
57
+ }
58
+ }
59
+ /** The index caught up with `mix.gone`. */
60
+ export function goneIndex(mix) {
61
+ const ix = mix.goneIx;
62
+ const list = mix.gone;
63
+ if (ix.of !== list || ix.count > list.length) {
64
+ ix.general = [];
65
+ ix.named = new Store();
66
+ ix.count = 0;
67
+ ix.of = list;
68
+ ix.latest = Number.NEGATIVE_INFINITY;
69
+ ix.slack = 0;
70
+ }
71
+ for (let i = ix.count; i < list.length; i++)
72
+ add(ix, list[i]);
73
+ ix.count = list.length;
74
+ return ix;
75
+ }
76
+ /** Hands voices let go of from `gone` to the departed index, noting the latest that left. */
77
+ function forgot(mix, out) {
78
+ for (const v of out) {
79
+ mix.departed.add(mix, v);
80
+ if (v.doneAt > mix.transport.forgotTo)
81
+ mix.transport.forgotTo = v.doneAt;
82
+ }
83
+ }
84
+ /**
85
+ * Keeps the gone voices `keep` says yes to, asking it once each, and takes the rest out of the
86
+ * index one by one. Leaves `gone` as it was when every one stays.
87
+ */
88
+ export function keepGone(mix, keep) {
89
+ const ix = goneIndex(mix);
90
+ const list = mix.gone;
91
+ let i = 0;
92
+ while (i < list.length && keep(list[i]))
93
+ i++;
94
+ if (i === list.length)
95
+ return;
96
+ const kept = list.slice(0, i);
97
+ const out = [list[i]];
98
+ for (i++; i < list.length; i++) {
99
+ const v = list[i];
100
+ (keep(v) ? kept : out).push(v);
101
+ }
102
+ remove(ix, out);
103
+ forgot(mix, out);
104
+ mix.gone = kept;
105
+ scoredCut(mix, list, out);
106
+ ix.of = kept;
107
+ ix.count = kept.length;
108
+ }
109
+ /**
110
+ * Lets go of the gone voices that finished before `reach`. Voices go in about the order they
111
+ * finish, so only the front can hold one: past the point where a voice finished `slack` after
112
+ * `reach`, none behind it finished before `reach`.
113
+ */
114
+ export function expireGone(mix, reach) {
115
+ const ix = goneIndex(mix);
116
+ if (!Number.isFinite(ix.slack)) {
117
+ keepGone(mix, (v) => v.doneAt >= reach);
118
+ return;
119
+ }
120
+ const list = mix.gone;
121
+ const edge = reach + ix.slack;
122
+ let end = 0;
123
+ let any = false;
124
+ for (let seen = Number.NEGATIVE_INFINITY; end < list.length && seen < edge; end++) {
125
+ const done = list[end].doneAt;
126
+ if (done < reach)
127
+ any = true;
128
+ if (done > seen)
129
+ seen = done;
130
+ }
131
+ if (!any)
132
+ return;
133
+ const kept = [];
134
+ const out = [];
135
+ for (let i = 0; i < end; i++) {
136
+ const v = list[i];
137
+ (v.doneAt < reach ? out : kept).push(v);
138
+ }
139
+ list.splice(0, end, ...kept);
140
+ remove(ix, out);
141
+ forgot(mix, out);
142
+ scoredCut(mix, list, out);
143
+ ix.count = list.length;
144
+ }
package/dist/handle.d.ts CHANGED
@@ -1,13 +1,17 @@
1
- import type { FadeOptions, FitResult, Handle } from './types.js';
1
+ import { type Motion, type Value } from './motion.js';
2
+ import type { Doubt, FadeOptions, FitResult, Handle, SeekOptions } from './types.js';
2
3
  import type { Voice } from './voice.js';
3
4
  /** What a voice's handle asks of the mix that cued it: one per mix, shared by every handle. */
4
5
  export interface HandleHost<I, O> {
5
- /** The mix clock, or the voice's start before the first sync. */
6
+ /** The mix clock, or its owner's for a voice one holds; the voice's start before either reads. */
6
7
  nowFor(voice: Voice<I, O>): number;
7
8
  /** A handle write changed the voice: reschedule it, and refill the lanes. */
8
9
  changed(voice: Voice<I, O>): void;
9
- /** The voice's clock jumped: for an owner, so did every clock it holds. */
10
- sought(voice: Voice<I, O>): void;
10
+ /**
11
+ * The voice's clock jumped: for an owner, so did every clock it holds. With `rebuild`, their
12
+ * state is made again for where they now are. How sure that leaves it.
13
+ */
14
+ sought(voice: Voice<I, O>, rebuild: boolean): Doubt;
11
15
  fade(voice: Voice<I, O>, opts: FadeOptions<I> | undefined): void;
12
16
  rise(voice: Voice<I, O>, opts: {
13
17
  over?: number;
@@ -16,6 +20,10 @@ export interface HandleHost<I, O> {
16
20
  /** Records a write the host made, which `again` makes once more where a seek replays it. */
17
21
  record(label: string, again: () => void): void;
18
22
  }
23
+ /** Refuses a voice rate no clock can run at. */
24
+ export declare function finiteRate(rate: number): void;
25
+ /** Refuses a voice weight that is no number: NaN would fold into every channel the voice writes. */
26
+ export declare function plainWeight(weight: number): void;
19
27
  /** What a handle answers for a voice a history store paged out, which left long ago. */
20
28
  export interface Gone {
21
29
  weight: number;
@@ -47,10 +55,13 @@ export declare class VoiceHandle<I, O> implements Handle<I> {
47
55
  get rate(): number;
48
56
  set rate(r: number);
49
57
  ramp(r: number, over: number): void;
50
- seek(elapsed: number): void;
58
+ seek(elapsed: number, opts?: SeekOptions): Doubt;
51
59
  fade(opts?: FadeOptions<I>): void;
52
60
  rise(opts?: {
53
61
  over?: number;
54
62
  }): void;
55
63
  weightOf(subject: I): number;
64
+ to(subject: I, target: Value, at?: number): void;
65
+ push(subject: I, velocity: Value, at?: number): void;
66
+ read(subject: I, at?: number): Motion<Value> | undefined;
56
67
  }
package/dist/handle.js CHANGED
@@ -1,4 +1,16 @@
1
1
  import { retime } from './clock.js';
2
+ import { motionOf } from './motion.js';
3
+ import { leave } from './origin.js';
4
+ /** Refuses a voice rate no clock can run at. */
5
+ export function finiteRate(rate) {
6
+ if (!Number.isFinite(rate))
7
+ throw new RangeError(`blits: a voice's rate is a finite number, not ${rate}`);
8
+ }
9
+ /** Refuses a voice weight that is no number: NaN would fold into every channel the voice writes. */
10
+ export function plainWeight(weight) {
11
+ if (Number.isNaN(weight))
12
+ throw new RangeError('blits: a voice’s weight is a number, not NaN');
13
+ }
2
14
  /**
3
15
  * A voice's handle: the host's only way to control a voice once cued. A voice a history store
4
16
  * pages out is let go of here too, so the handle answers as a gone voice's does, and a seek that
@@ -49,9 +61,11 @@ export class VoiceHandle {
49
61
  }
50
62
  // A write to a paged voice changes nothing: it had left when the host made it, as it has on replay.
51
63
  set weight(w) {
64
+ plainWeight(w);
52
65
  const voice = this.#voice;
53
66
  if (voice === null)
54
67
  return;
68
+ voice.written = true;
55
69
  voice.weight = w;
56
70
  this.#host.changed(voice);
57
71
  this.#host.record('weight', () => {
@@ -66,28 +80,40 @@ export class VoiceHandle {
66
80
  this.ramp(r, 0);
67
81
  }
68
82
  ramp(r, over) {
83
+ finiteRate(r);
69
84
  const voice = this.#voice;
70
85
  if (voice === null)
71
86
  return;
72
- retime(voice, this.#host.nowFor(voice), r, over);
87
+ voice.written = true;
88
+ const now = this.#host.nowFor(voice);
89
+ // A pending voice's clock reads 0 at its start, and a rate taken from now would cross 0 sooner:
90
+ // a subject it freezes before would play, and step, ahead of a voice that has not started.
91
+ const u = voice.state === 'pending' && voice.start > now && r > 0 ? voice.start : now;
92
+ leave(voice, u);
93
+ retime(voice, u, r, over);
73
94
  this.#host.changed(voice);
74
95
  this.#host.record('rate', () => this.ramp(r, over));
75
96
  }
76
- seek(elapsed) {
97
+ seek(elapsed, opts) {
77
98
  const voice = this.#voice;
78
99
  if (voice === null)
79
- return;
80
- voice.rebase(this.#host.nowFor(voice));
100
+ return 'exact';
101
+ voice.written = true;
102
+ const u = this.#host.nowFor(voice);
103
+ leave(voice, u);
104
+ voice.rebase(u);
81
105
  voice.anchorElapsed = elapsed;
82
106
  voice.seeks++;
83
- this.#host.sought(voice);
107
+ const doubt = this.#host.sought(voice, opts?.state !== 'keep');
84
108
  this.#host.changed(voice);
85
- this.#host.record('seek', () => this.seek(elapsed));
109
+ this.#host.record('seek', () => this.seek(elapsed, opts));
110
+ return doubt;
86
111
  }
87
112
  fade(opts) {
88
113
  const voice = this.#voice;
89
114
  if (voice === null)
90
115
  return;
116
+ voice.written = true;
91
117
  this.#host.fade(voice, opts);
92
118
  this.#host.record('fade', () => this.fade(opts));
93
119
  }
@@ -95,6 +121,7 @@ export class VoiceHandle {
95
121
  const voice = this.#voice;
96
122
  if (voice === null)
97
123
  return;
124
+ voice.written = true;
98
125
  this.#host.rise(voice, opts);
99
126
  this.#host.record('rise', () => this.rise(opts));
100
127
  }
@@ -102,4 +129,27 @@ export class VoiceHandle {
102
129
  const voice = this.#voice;
103
130
  return voice === null ? 0 : this.#host.weightOf(voice, subject);
104
131
  }
132
+ // The patch records each of these itself, so a seek plays them again without the handle.
133
+ to(subject, target, at) {
134
+ const voice = this.#voice;
135
+ if (voice !== null)
136
+ moves(voice.patch, 'to', 'a spring or a tween').to(subject, target, at);
137
+ }
138
+ push(subject, velocity, at) {
139
+ const voice = this.#voice;
140
+ if (voice !== null)
141
+ moves(voice.patch, 'push', 'a spring or a glide').push(subject, velocity, at);
142
+ }
143
+ read(subject, at) {
144
+ const voice = this.#voice;
145
+ if (voice === null || motionOf(voice.patch) === undefined)
146
+ return undefined;
147
+ return voice.patch.read(subject, at);
148
+ }
149
+ }
150
+ /** A voice's patch as a motion that takes `verb`, or a throw naming the kinds that do. */
151
+ function moves(patch, verb, kinds) {
152
+ if (motionOf(patch) === undefined || typeof patch[verb] !== 'function')
153
+ throw new Error(`blits: ${verb} needs a voice playing ${kinds}`);
154
+ return patch;
105
155
  }
package/dist/held.d.ts CHANGED
@@ -3,12 +3,15 @@ import type { Subject, Voice } from './voice.js';
3
3
  /** The record a voice gives every subject it does not reach. */
4
4
  export declare function unreachedOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, now: number): Subject<unknown>;
5
5
  /** What this voice holds for this subject, made on first sight with `target` and `stagger` asked once. */
6
- export declare function held<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, now: number, slot: number): Subject<unknown>;
6
+ export declare function held<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, slot: number): Subject<unknown>;
7
7
  /**
8
- * When the voice clock reads `delay`, from where it is anchored now; during a ramp this assumes
9
- * the rate it is ramping to.
8
+ * The mix time the voice clock first read `delay`, or will by its controls now. A stateful voice
9
+ * counts from its last seek that rebuilt state, as its records do.
10
10
  */
11
11
  export declare function sinceOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, delay: number): number;
12
12
  export declare function shownOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, since: number): number;
13
- /** A voice has gone live: the records it made while pending count from where it started. */
14
- export declare function started<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
13
+ /**
14
+ * A voice's start or clock moved, or it went live at `now`: the records whose `since` was still
15
+ * ahead count from where its clock now puts them.
16
+ */
17
+ export declare function reorigin<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, now: number): void;
package/dist/held.js CHANGED
@@ -1,41 +1,32 @@
1
1
  import { schedule } from './due.js';
2
- import { mixTime } from './owner.js';
2
+ import { everyoneOf, inherit, sighted } from './everyone.js';
3
+ import { originOf } from './origin.js';
4
+ import { record } from './record.js';
3
5
  import { unreach, unreached } from './unreached.js';
4
6
  /** The record a voice gives every subject it does not reach. */
5
7
  export function unreachedOf(voice, now) {
6
8
  const since = this.sinceOf(voice, 0);
7
- const none = {
8
- reaches: false,
9
- delay: 0,
10
- since,
11
- shown: this.shownOf(voice, since),
12
- weight: 0,
13
- rested: false,
14
- bands: null,
15
- state: undefined,
16
- stepped: now,
17
- ticks: 0,
18
- probed: Number.NaN,
19
- delta: null,
20
- phase: 0,
21
- seeks: 0,
22
- kept: null,
23
- voice,
24
- next: null,
25
- version: Number.NaN,
26
- loci: null,
27
- slot: -1,
28
- };
9
+ const none = record(voice, false, undefined);
10
+ none.since = since;
11
+ none.shown = this.shownOf(voice, since);
12
+ none.stepped = now;
13
+ none.rebuilt = voice.rebuilds;
29
14
  voice.unreached = none;
30
15
  return none;
31
16
  }
32
17
  /** What this voice holds for this subject, made on first sight with `target` and `stagger` asked once. */
33
- export function held(voice, subject, now, slot) {
18
+ export function held(voice, subject, slot) {
19
+ if (voice.sharing) {
20
+ const all = everyoneOf(this, voice);
21
+ if (all !== null)
22
+ return all;
23
+ }
34
24
  if (unreached(voice, slot))
35
25
  return voice.unreached;
36
26
  let held = voice.subjects.get(subject);
37
27
  if (held !== undefined)
38
28
  return held;
29
+ const now = this.now;
39
30
  const reaches = this.aims(voice, subject);
40
31
  if (!reaches) {
41
32
  // Nothing is kept for a subject the voice does not reach, so one record stands for them all:
@@ -55,38 +46,36 @@ export function held(voice, subject, now, slot) {
55
46
  }
56
47
  const delay = voice.spec.stagger ? voice.spec.stagger(subject) : 0;
57
48
  const since = this.sinceOf(voice, delay);
58
- held = {
59
- reaches: true,
60
- delay,
61
- since,
62
- shown: this.shownOf(voice, since),
63
- weight: 0,
64
- rested: false,
65
- bands: null,
66
- state: voice.patch.state ? voice.patch.state(subject) : undefined,
67
- stepped: this.backward && since < now ? since : now,
68
- ticks: 0,
69
- probed: Number.NaN,
70
- delta: null,
71
- phase: 0,
72
- seeks: 0,
73
- kept: null,
74
- voice,
75
- next: null,
76
- version: Number.NaN,
77
- loci: null,
78
- slot: -1,
79
- };
49
+ const shown = this.shownOf(voice, since);
50
+ held = record(voice, true, voice.patch.state ? voice.patch.state(subject) : undefined);
51
+ held.delay = delay;
52
+ held.since = since;
53
+ held.shown = shown;
54
+ held.stepped = since < now ? since : now;
55
+ held.rebuilt = voice.rebuilds;
80
56
  if (this.projecting) {
81
57
  held.from = held.stepped;
82
58
  held.unknown = this.backward && voice.spec.from === 'current';
83
59
  }
84
60
  voice.subjects.set(subject, held);
85
- if (voice.state === 'pending') {
61
+ if (!(since <= now)) {
86
62
  voice.early ??= [];
87
- voice.early.push(held);
63
+ const early = voice.early;
64
+ // Prunes at each doubling, so records whose origin has passed are not kept to the voice's end.
65
+ if (early.length >= 64 && (early.length & (early.length - 1)) === 0) {
66
+ let kept = 0;
67
+ for (const h of early)
68
+ if (!(h.since <= now))
69
+ early[kept++] = h;
70
+ early.length = kept;
71
+ }
72
+ early.push(held);
88
73
  }
89
- voice.seen++;
74
+ // A subject a voice saw while it shared one record was counted then.
75
+ if (sighted(voice, slot))
76
+ inherit(voice, held, slot);
77
+ else
78
+ voice.seen++;
90
79
  if (delay > voice.latest) {
91
80
  voice.latest = delay;
92
81
  // A later end can take a frozen voice live again.
@@ -96,26 +85,35 @@ export function held(voice, subject, now, slot) {
96
85
  return held;
97
86
  }
98
87
  /**
99
- * When the voice clock reads `delay`, from where it is anchored now; during a ramp this assumes
100
- * the rate it is ramping to.
88
+ * The mix time the voice clock first read `delay`, or will by its controls now. A stateful voice
89
+ * counts from its last seek that rebuilt state, as its records do.
101
90
  */
102
91
  export function sinceOf(voice, delay) {
103
- const t = voice.rate > 0
104
- ? voice.anchorNow + (delay - voice.anchorElapsed) / voice.rate
105
- : voice.start + delay;
106
- return voice.owner === null ? t : mixTime(voice.owner, t);
92
+ const p = voice.patch;
93
+ return originOf(voice, delay, p.state !== undefined || p.step !== undefined || voice.keeping);
107
94
  }
108
95
  export function shownOf(voice, since) {
109
96
  return voice.freezesBefore && voice.opened < since ? voice.opened : since;
110
97
  }
111
- /** A voice has gone live: the records it made while pending count from where it started. */
112
- export function started(mix, voice) {
98
+ /**
99
+ * A voice's start or clock moved, or it went live at `now`: the records whose `since` was still
100
+ * ahead count from where its clock now puts them.
101
+ */
102
+ export function reorigin(mix, voice, now) {
113
103
  const early = voice.early;
114
104
  if (early === null)
115
105
  return;
116
- voice.early = null;
106
+ let kept = 0;
117
107
  for (const held of early) {
118
108
  held.since = mix.sinceOf(voice, held.delay);
119
109
  held.shown = mix.shownOf(voice, held.since);
110
+ if (held.ticks === 0 && held.stepped > held.since)
111
+ held.stepped = held.since;
112
+ if (!(held.since <= now))
113
+ early[kept++] = held;
120
114
  }
115
+ early.length = kept;
116
+ if (kept === 0)
117
+ voice.early = null;
118
+ mix.lanes?.reshown(voice.id);
121
119
  }
package/dist/history.d.ts CHANGED
@@ -1,13 +1,20 @@
1
1
  import type { Mixer } from './mixer.js';
2
+ import { type Cut } from './transport.js';
2
3
  import { type Subject, type Voice } from './voice.js';
3
- /**
4
- * The last entry taken before `t`, or at it where `inclusive` says so, as it does for a change a
5
- * sync made. A host's change between frames is taken strictly: it shows from the next frame on, as
6
- * it did live.
7
- */
4
+ /** The last entry taken before `t`, or at it where `inclusive` says so. */
8
5
  export declare function last<T extends {
9
6
  at: number;
10
7
  }>(list: readonly T[], t: number, inclusive: boolean | ((e: T) => boolean)): T | undefined;
8
+ /**
9
+ * The last entry of a list kept in the order it was made that falls inside `cut`, an entry made
10
+ * before its frame was read counting as `early`.
11
+ */
12
+ export declare function lastWithin<T extends {
13
+ seq: number;
14
+ sync?: boolean;
15
+ }>(list: readonly T[], cut: Cut): T | undefined;
16
+ /** Deep equality over plain data, for telling whether a host field changed. */
17
+ export declare function same(a: unknown, b: unknown): boolean;
11
18
  /** A voice's elapsed at an earlier mix time, by the controls it had then. */
12
19
  export declare function elapsedThen<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, t: number): number;
13
20
  /** Records a change to a voice's controls under `history`, and lets go of what it no longer reaches. */
@@ -25,11 +32,8 @@ export declare function remember<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, sub
25
32
  * for a seek or a read back to before then; without history, keeps nothing.
26
33
  */
27
34
  export declare function leave<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I, held: Subject<unknown> | undefined): void;
28
- /** The record a subject had at mix time `t` and left a voice with after it, if it left after `t`. */
29
- export declare function leftAt<I>(left: readonly {
30
- subject: I;
31
- at: number;
32
- held: Subject<unknown>;
33
- }[] | null, subject: I, t: number): Subject<unknown> | undefined;
34
- /** The mix time a motion patch keeps a released subject from, and how far back, under history. */
35
- export declare function releasing<I, O>(mix: Mixer<I, O>): [at?: number, reach?: number];
35
+ /**
36
+ * The mix time a motion patch keeps a released subject from, how far back, and the frame, under
37
+ * history.
38
+ */
39
+ export declare function releasing<I, O>(mix: Mixer<I, O>): [at?: number, reach?: number, seq?: number];
package/dist/history.js CHANGED
@@ -1,14 +1,13 @@
1
1
  import { elapsedWith } from './clock.js';
2
2
  import { clone } from './clone.js';
3
3
  import { schedule } from './due.js';
4
+ import { Leavings } from './leavings.js';
4
5
  import { packHeld } from './pack.js';
5
6
  import { pageOut } from './paging.js';
7
+ import { scoreTouched } from './scored.js';
8
+ import { within } from './transport.js';
6
9
  import { none } from './voice.js';
7
- /**
8
- * The last entry taken before `t`, or at it where `inclusive` says so, as it does for a change a
9
- * sync made. A host's change between frames is taken strictly: it shows from the next frame on, as
10
- * it did live.
11
- */
10
+ /** The last entry taken before `t`, or at it where `inclusive` says so. */
12
11
  export function last(list, t, inclusive) {
13
12
  let lo = 0;
14
13
  let hi = list.length - 1;
@@ -25,8 +24,28 @@ export function last(list, t, inclusive) {
25
24
  }
26
25
  return found;
27
26
  }
27
+ /**
28
+ * The last entry of a list kept in the order it was made that falls inside `cut`, an entry made
29
+ * before its frame was read counting as `early`.
30
+ */
31
+ export function lastWithin(list, cut) {
32
+ let lo = 0;
33
+ let hi = list.length - 1;
34
+ let found;
35
+ while (lo <= hi) {
36
+ const mid = (lo + hi) >> 1;
37
+ const e = list[mid];
38
+ if (within(cut, e.seq, e.sync)) {
39
+ found = e;
40
+ lo = mid + 1;
41
+ }
42
+ else
43
+ hi = mid - 1;
44
+ }
45
+ return found;
46
+ }
28
47
  /** Deep equality over plain data, for telling whether a host field changed. */
29
- function same(a, b) {
48
+ export function same(a, b) {
30
49
  if (a === b)
31
50
  return true;
32
51
  if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
@@ -52,12 +71,13 @@ function ownerThen(mix, owner, t) {
52
71
  }
53
72
  /** Records a change to a voice's controls under `history`, and lets go of what it no longer reaches. */
54
73
  export function noted(mix, voice) {
74
+ scoreTouched(mix, voice, true);
55
75
  mix.stir();
56
76
  schedule(mix, voice);
57
77
  const log = voice.log;
58
78
  if (log === null)
59
79
  return;
60
- voice.note(Number.isNaN(mix.now) ? Number.NEGATIVE_INFINITY : mix.now, mix.moving);
80
+ voice.note(Number.isNaN(mix.now) ? Number.NEGATIVE_INFINITY : mix.now, mix.transport.seq, mix.moving || !mix.looked);
61
81
  const reach = mix.now - mix.opts.history.ms;
62
82
  let drop = 0;
63
83
  while (drop + 1 < log.length && log[drop + 1].at <= reach)
@@ -106,7 +126,8 @@ export function copyHeld(voice, h) {
106
126
  export function recordHost(mix, now) {
107
127
  const log = mix.hostLog;
108
128
  const prev = log[log.length - 1];
109
- if (prev !== undefined && prev.at === now)
129
+ const seq = mix.transport.seq;
130
+ if (prev !== undefined && prev.seq === seq)
110
131
  return;
111
132
  const host = mix.opts.host;
112
133
  const fields = {};
@@ -116,7 +137,7 @@ export function recordHost(mix, now) {
116
137
  fields[f] = clone(host[f]);
117
138
  if (prev !== undefined && same(prev.fields, fields))
118
139
  return;
119
- log.push({ at: now, fields });
140
+ log.push({ at: now, seq, fields });
120
141
  const reach = now - mix.opts.history.ms;
121
142
  pageOut(mix, 'host', undefined, undefined, older(log, reach), (e) => e.fields, reach);
122
143
  }
@@ -133,7 +154,7 @@ export function record(mix, voice, subject, held, value) {
133
154
  prev.value = value;
134
155
  return;
135
156
  }
136
- inputs.push({ at: mix.now, value });
157
+ inputs.push({ at: mix.now, seq: mix.transport.seq, value });
137
158
  const reach = mix.now - history.ms;
138
159
  const out = older(inputs, reach);
139
160
  if (out.length > 0)
@@ -153,7 +174,7 @@ export function remember(mix, voice, subject, held) {
153
174
  const prev = snaps[snaps.length - 1];
154
175
  if (prev !== undefined && mix.now - prev.at < (history.every ?? 200))
155
176
  return;
156
- snaps.push({ at: mix.now, held: copyHeld(voice, held) });
177
+ snaps.push({ at: mix.now, seq: mix.transport.seq, held: copyHeld(voice, held) });
157
178
  const reach = mix.now - history.ms;
158
179
  const out = older(snaps, reach);
159
180
  if (out.length > 0)
@@ -168,27 +189,27 @@ export function leave(mix, voice, subject, held) {
168
189
  if (history === undefined || held === undefined)
169
190
  return;
170
191
  const reach = mix.now - history.ms;
171
- const left = [];
172
- for (const e of voice.left ?? [])
173
- if (e.at >= reach)
174
- left.push(e);
175
- else if (mix.keys !== null)
176
- pageOut(mix, 'left', voice.id, mix.keys.key(e.subject), [e], (x) => packHeld(voice, x.held), reach);
177
- left.push({ subject, at: Number.isNaN(mix.now) ? Number.NEGATIVE_INFINITY : mix.now, held });
192
+ const left = voice.left ?? new Leavings();
178
193
  voice.left = left;
194
+ const keys = mix.keys;
195
+ left.expire(reach, keys === null
196
+ ? null
197
+ : (e) => pageOut(mix, 'left', voice.id, keys.key(e.subject), [e], (x) => [packHeld(voice, x.held), x.sync], reach));
198
+ left.push({
199
+ subject,
200
+ at: Number.isNaN(mix.now) ? Number.NEGATIVE_INFINITY : mix.now,
201
+ seq: mix.transport.seq,
202
+ sync: mix.syncing,
203
+ held,
204
+ });
179
205
  }
180
- /** The record a subject had at mix time `t` and left a voice with after it, if it left after `t`. */
181
- export function leftAt(left, subject, t) {
182
- let found;
183
- for (const e of left ?? [])
184
- if (e.at > t && Object.is(e.subject, subject) && (found === undefined || e.at < found.at))
185
- found = e;
186
- return found?.held;
187
- }
188
- /** The mix time a motion patch keeps a released subject from, and how far back, under history. */
206
+ /**
207
+ * The mix time a motion patch keeps a released subject from, how far back, and the frame, under
208
+ * history.
209
+ */
189
210
  export function releasing(mix) {
190
211
  const history = mix.opts.history;
191
212
  if (history === undefined || Number.isNaN(mix.now))
192
213
  return [];
193
- return [mix.now, mix.now - history.ms];
214
+ return [mix.now, mix.now - history.ms, mix.transport.seq];
194
215
  }