@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/project.js CHANGED
@@ -1,14 +1,16 @@
1
1
  import { ownBlends } from './blend.js';
2
- import { index } from './chain.js';
3
- import { copyHeld, last, leftAt } from './history.js';
2
+ import { open, sighted, weightAt } from './everyone.js';
3
+ import { copyHeld, last, lastWithin } from './history.js';
4
4
  import { Mixer } from './mixer.js';
5
5
  import { move } from './move.js';
6
6
  import { heldByInput, ownerReading, relink } from './owner.js';
7
7
  import { pin } from './place.js';
8
8
  import { reading } from './reading.js';
9
- import { Store } from './store.js';
9
+ import { record } from './record.js';
10
+ import { recount } from './roster.js';
11
+ import { copyStanding, stands } from './standing.js';
10
12
  import { hostAt } from './tape.js';
11
- import { Transport } from './transport.js';
13
+ import { Transport, within } from './transport.js';
12
14
  import { cover } from './unpage.js';
13
15
  import { unreached } from './unreached.js';
14
16
  /**
@@ -18,24 +20,36 @@ import { unreached } from './unreached.js';
18
20
  export function projectAll(transport, t) {
19
21
  const ahead = Number.isNaN(transport.now) || t >= transport.now;
20
22
  const tape = transport.tape;
21
- const next = ahead && tape !== undefined ? tape.timestampAt(tape.undoDepth()) : undefined;
23
+ const stamp = ahead && tape !== undefined ? tape.timestampAt(tape.undoDepth()) : undefined;
24
+ const pace = transport.pace;
25
+ const next = stamp === undefined || pace === null ? stamp : pace.reading(stamp);
22
26
  if (next !== undefined && next <= t)
23
27
  throw new Error(`blits: the tape holds calls at ${next} that a read ahead to ${t} cannot play; seek there to see them`);
24
- if (!ahead) {
25
- const history = transport.history;
26
- if (!history)
27
- throw new Error('blits: reading back needs a mix made with history');
28
+ // Behind, the last frame at or before `t`, read as it showed when `t` is that frame's own time.
29
+ let cut = { seq: transport.seq, strict: false };
30
+ let u = hostAt(transport, t);
31
+ const kept = transport.history !== undefined;
32
+ if (!ahead && kept) {
28
33
  cover(transport, t);
34
+ const frame = transport.frameAt(t);
35
+ if (frame !== undefined) {
36
+ cut = { seq: frame.seq, strict: frame.at === t };
37
+ if (frame.at === t)
38
+ u = frame.u;
39
+ }
29
40
  }
30
- const u = hostAt(transport, t);
31
- const pace = transport.pace;
41
+ else if (!ahead)
42
+ stands(transport, t);
32
43
  const c = new Transport(undefined, true);
33
44
  c.offset = transport.offset;
34
45
  c.u = u;
35
46
  c.nextId = transport.nextId;
36
47
  c.now = ahead ? transport.now : t;
37
- c.pace = pace === null ? null : pace.until(ahead ? Number.POSITIVE_INFINITY : u);
38
- c.announced = (ahead ? transport.announced : transport.announced.filter((a) => a.made < t)).map((a) => ({ ...a }));
48
+ c.pace =
49
+ pace === null || !within(cut, transport.pacedSeq)
50
+ ? null
51
+ : pace.until(ahead ? Number.POSITIVE_INFINITY : u);
52
+ c.announced = (ahead ? transport.announced : transport.announced.filter((a) => within(cut, a.seq))).map((a) => ({ ...a }));
39
53
  const copies = new Map();
40
54
  for (const mix of transport.members) {
41
55
  const copy = new Mixer(mix.kit, {
@@ -49,8 +63,10 @@ export function projectAll(transport, t) {
49
63
  copies.set(mix, copy);
50
64
  if (ahead)
51
65
  copyAhead(mix, copy);
66
+ else if (kept)
67
+ copyBack(mix, copy, t, cut);
52
68
  else
53
- copyBack(mix, copy, t);
69
+ copyStanding(mix, copy, t, cut);
54
70
  }
55
71
  if (ahead)
56
72
  for (const copy of copies.values())
@@ -83,13 +99,11 @@ export function projectAll(transport, t) {
83
99
  }
84
100
  /** A mix's copy for a read ahead: its voices as they stand, each subject carried from the live one. */
85
101
  function copyAhead(mix, c) {
86
- c.pose = mix.pose;
87
- c.wantsPose = mix.wantsPose;
88
- c.reducedNow = mix.reducedNow;
102
+ mirror(mix, c);
89
103
  c.cued = mix.cued
90
104
  .filter((v) => v.state !== 'done')
91
105
  .map((v) => {
92
- const copy = v.copy((subject) => carry(mix, v, subject));
106
+ const copy = v.copy((subject) => carry(mix, v, subject, c, copy), undefined, true);
93
107
  const at = mix.pins?.get(v);
94
108
  if (at !== undefined)
95
109
  pin(c, copy, at);
@@ -98,15 +112,14 @@ function copyAhead(mix, c) {
98
112
  ownBlends(c.cued);
99
113
  if (mix.owners !== null)
100
114
  relink(c.cued);
101
- count(c);
115
+ recount(c);
102
116
  }
103
- /** A mix's copy for a read back to `t`: its voices then, from the controls and copies kept. */
104
- function copyBack(mix, c, t) {
105
- c.pose = mix.pose;
106
- c.wantsPose = mix.wantsPose;
107
- c.reducedNow = mix.reducedNow;
117
+ /** A mix's copy for a read back to `t`, cut at `cut`: its voices then, from the controls and copies kept. */
118
+ function copyBack(mix, c, t, cut) {
119
+ mirror(mix, c);
108
120
  c.backward = true;
109
- const was = last(mix.hostLog, t, true);
121
+ // Copied as the frame's first probe began, so its own frame shows it.
122
+ const was = lastWithin(mix.hostLog, { seq: cut.seq, strict: false });
110
123
  const host = mix.opts.host;
111
124
  const then = was && typeof host === 'object' && host !== null
112
125
  ? Object.assign(Object.create(host), was.fields)
@@ -114,11 +127,11 @@ function copyBack(mix, c, t) {
114
127
  if (was)
115
128
  c.hostThen = was;
116
129
  c.cued = [...mix.cued, ...mix.gone]
117
- .filter((v) => v.cuedAt <= t && v.doneAt > t)
130
+ .filter((v) => v.cuedSeq <= cut.seq && !within(cut, v.doneSeq, true))
118
131
  .sort((a, b) => a.id - b.id)
119
132
  .map((v) => {
120
133
  const log = v.log;
121
- const copy = v.copy((subject) => recall(mix, v, subject, t), last(log, t, (e) => e.sync) ?? log[0]);
134
+ const copy = v.copy((subject) => recall(mix, v, subject, t, cut), lastWithin(log, cut) ?? log[0]);
122
135
  if (then !== undefined)
123
136
  copy.setting.host = then;
124
137
  return copy;
@@ -126,30 +139,39 @@ function copyBack(mix, c, t) {
126
139
  ownBlends(c.cued);
127
140
  if (mix.owners !== null)
128
141
  relink(c.cued);
142
+ stateAt(c, t);
143
+ recount(c);
144
+ }
145
+ /** Sets each copied voice's state to what its clock and fade make it at `t`. */
146
+ export function stateAt(c, t) {
129
147
  for (const copy of c.cued)
130
- copy.state =
131
- (copy.owner === null ? t : ownerReading(copy.owner, t)) < copy.start
148
+ copy.state = copy.out
149
+ ? 'fading'
150
+ : (copy.owner === null ? t : ownerReading(copy.owner, t)) < copy.start
132
151
  ? 'pending'
133
- : copy.out && copy.out.at <= t
134
- ? 'fading'
135
- : copy.freezesAfter && copy.elapsedAt(t) >= copy.span + copy.latest
136
- ? 'frozen'
137
- : 'live';
138
- count(c);
152
+ : copy.freezesAfter && copy.elapsedAt(t) >= copy.span + copy.latest
153
+ ? 'frozen'
154
+ : 'live';
139
155
  }
140
- /** Recounts what the fold's shortcuts depend on, for a projection's freshly copied voices. */
141
- function count(mix) {
142
- mix.named = new Store();
143
- mix.naming = 0;
144
- mix.general = [];
145
- for (const voice of mix.cued)
146
- index(mix, voice);
147
- mix.loci = mix.cued.filter((v) => v.spec.locus !== undefined).length;
148
- mix.anchored = mix.cued.filter((v) => v.spec.anchor !== undefined).length;
149
- mix.steps.push(null, ++mix.version);
156
+ /** Gives a projection's copy what it reads of the live mix as it is, before its voices are copied. */
157
+ export function mirror(mix, c) {
158
+ c.pose = mix.pose;
159
+ c.wantsPose = mix.wantsPose;
160
+ c.reducedNow = mix.reducedNow;
150
161
  }
151
162
  /** A voice's record of a subject, the one it shares among those it does not reach included. */
152
163
  function recordOf(mix, voice, subject) {
164
+ // A voice sharing one record has that for every subject a chain has asked it about, pointed at
165
+ // the subject for the caller to copy from.
166
+ if (voice.sharing) {
167
+ const slot = mix.chains.get(subject)?.slot ?? -1;
168
+ if (!sighted(voice, slot))
169
+ return undefined;
170
+ const all = voice.everyone;
171
+ open(voice, all, slot);
172
+ all.weight = weightAt(voice, slot);
173
+ return all;
174
+ }
153
175
  const held = voice.subjects.get(subject);
154
176
  if (held !== undefined || voice.unreachedBits === null)
155
177
  return held;
@@ -157,12 +179,19 @@ function recordOf(mix, voice, subject) {
157
179
  ? voice.unreached
158
180
  : undefined;
159
181
  }
160
- /** A projection ahead starts each subject from where the live mix holds it. */
161
- function carry(mix, voice, subject) {
182
+ /**
183
+ * A projection ahead starts each subject from where the live mix holds it, its `since` worked out
184
+ * again where it was still ahead, since the copy's clock may have moved it.
185
+ */
186
+ function carry(mix, voice, subject, c, copy) {
162
187
  const live = recordOf(mix, voice, subject);
163
188
  if (live === undefined)
164
189
  return undefined;
165
190
  const h = copyHeld(voice, live);
191
+ if (h.reaches && !(h.since <= mix.now)) {
192
+ h.since = c.sinceOf(copy, h.delay);
193
+ h.shown = c.shownOf(copy, h.since);
194
+ }
166
195
  h.from = h.stepped;
167
196
  return h;
168
197
  }
@@ -170,8 +199,8 @@ function carry(mix, voice, subject) {
170
199
  * A projection back starts each subject from the latest copy kept at or before `t`, else from its
171
200
  * voice's start with fresh state, which is exact for a voice stepped at a fixed interval.
172
201
  */
173
- function recall(mix, voice, subject, t) {
174
- const live = leftAt(voice.left, subject, t) ?? recordOf(mix, voice, subject);
202
+ export function recall(mix, voice, subject, t, cut) {
203
+ const live = voice.left?.at(subject, cut) ?? recordOf(mix, voice, subject);
175
204
  if (live === undefined)
176
205
  return undefined;
177
206
  const snap = live.snaps && last(live.snaps, t, true);
@@ -182,33 +211,18 @@ function recall(mix, voice, subject, t) {
182
211
  return h;
183
212
  }
184
213
  const stepped = live.since < t ? live.since : t;
185
- return {
186
- reaches: live.reaches,
187
- delay: live.delay,
188
- since: live.since,
189
- shown: live.shown,
190
- weight: 0,
191
- rested: false,
192
- bands: null,
193
- state: live.reaches && voice.patch.state
194
- ? voice.patch.state(subject)
195
- : undefined,
196
- stepped,
197
- ticks: 0,
198
- probed: Number.NaN,
199
- delta: null,
200
- phase: 0,
201
- seeks: 0,
202
- kept: null,
203
- voice,
204
- next: null,
205
- version: Number.NaN,
206
- loci: null,
207
- slot: -1,
208
- from: stepped,
209
- unknown: voice.spec.from === 'current',
210
- replay: live.inputs,
211
- };
214
+ const h = record(voice, live.reaches, live.reaches && voice.patch.state
215
+ ? voice.patch.state(subject)
216
+ : undefined);
217
+ h.delay = live.delay;
218
+ h.since = live.since;
219
+ h.shown = live.shown;
220
+ h.stepped = stepped;
221
+ h.rebuilt = voice.rebuilds;
222
+ h.from = stepped;
223
+ h.unknown = voice.spec.from === 'current';
224
+ h.replay = live.inputs;
225
+ return h;
212
226
  }
213
227
  /** Per channel, the least sure voice that fed it this frame. */
214
228
  function doubts(mix, subject) {
@@ -231,19 +245,23 @@ function doubts(mix, subject) {
231
245
  for (const slot of voice.slots)
232
246
  out[mix.names[slot]] = 'held';
233
247
  }
234
- for (let held = mix.chain(subject); held !== null; held = held.next) {
235
- const voice = held.voice;
236
- if (voice === null || voice.state === 'done')
237
- continue;
238
- if (held.weight <= 0)
239
- continue;
248
+ const feed = (voice, held) => {
249
+ if (voice.state === 'done' || held.weight <= 0)
250
+ return;
240
251
  const d = doubtOf(mix, voice, held);
241
252
  for (const slot of voice.slots) {
242
253
  const name = mix.names[slot];
243
254
  if (rank[d] > rank[out[name]])
244
255
  out[name] = d;
245
256
  }
246
- }
257
+ };
258
+ for (let held = mix.chain(subject); held !== null; held = held.next)
259
+ if (held.voice !== null)
260
+ feed(held.voice, held);
261
+ // A voice sharing one record is in no chain; its record holds the weight the fold just gave.
262
+ for (const voice of mix.sharers)
263
+ if (voice.everyone !== null && voice.state !== 'pending')
264
+ feed(voice, voice.everyone);
247
265
  return out;
248
266
  }
249
267
  function doubtOf(mix, voice, held) {
package/dist/pull.d.ts CHANGED
@@ -3,3 +3,5 @@ import type { Columns } from './types.js';
3
3
  /** After a probe, keeps the pose a retarget from `'current'` reads as the subject's last. */
4
4
  export declare function keep<I, O>(this: Mixer<I, O>, subject: I, pose: O, out: O | undefined): void;
5
5
  export declare function pull<I, O>(mix: Mixer<I, O>, subjects: Iterable<I>, into: Columns<O>): void;
6
+ /** Forgets the array `pull` last read, whose heads a seek has made stale. */
7
+ export declare function unpull<I, O>(mix: Mixer<I, O>): void;
package/dist/pull.js CHANGED
@@ -174,3 +174,11 @@ function columnsOf(mix, into) {
174
174
  }
175
175
  return columns;
176
176
  }
177
+ /** Forgets the array `pull` last read, whose heads a seek has made stale. */
178
+ export function unpull(mix) {
179
+ mix.pulled = [];
180
+ mix.pulledHeads = [];
181
+ mix.pulledSlots = new Int32Array(0);
182
+ mix.pulledVersion = Number.NaN;
183
+ mix.pulledRelinks = -1;
184
+ }
package/dist/qualify.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { numericOf } from './channels.js';
2
2
  import { flush } from './columns.js';
3
3
  import { crowdable } from './crowd.js';
4
+ import { weighAt } from './everyone.js';
4
5
  import { wake } from './fill.js';
5
6
  import { Lane } from './lane.js';
6
7
  import { bury, compactSparse, join, regroup, retouchRow, sameCrowds } from './rows.js';
@@ -169,6 +170,9 @@ export function requalify(lanes, version) {
169
170
  const ch = {
170
171
  name: host.names[slot],
171
172
  op: n.op,
173
+ plain: n.op !== 'last' && n.op !== 'own' && n.turn === undefined,
174
+ turn: n.turn ?? 0,
175
+ channel,
172
176
  scalar,
173
177
  rest: typeof rest === 'number' ? rest : rest[0],
174
178
  axes: n.axes,
@@ -332,8 +336,12 @@ function leave(lanes, lane) {
332
336
  const rec = lane.records[p];
333
337
  const slot = lane.list[p];
334
338
  const w = done ? undefined : lanes.reported(lane, slot);
335
- if (rec !== undefined && w !== undefined)
336
- rec.weight = w;
339
+ if (rec !== undefined && w !== undefined) {
340
+ if (lane.voice.sharing)
341
+ weighAt(lane.voice, slot, w, lanes.cap);
342
+ else
343
+ rec.weight = w;
344
+ }
337
345
  if (rec !== undefined && lane.motion !== undefined)
338
346
  settle(lanes, lane, p, slot, rec);
339
347
  }
package/dist/reading.d.ts CHANGED
@@ -20,4 +20,9 @@ export declare const reading: {
20
20
  * One count for every mix: a change to a spring in one mix refills the lanes of all of them.
21
21
  */
22
22
  moved: number;
23
+ /**
24
+ * How many changes an `input` signal has reported, in any mix, so a fill can tell an input moved
25
+ * since it read it. One count for every mix, as `moved` is.
26
+ */
27
+ inputs: number;
23
28
  };
package/dist/reading.js CHANGED
@@ -20,4 +20,9 @@ export const reading = {
20
20
  * One count for every mix: a change to a spring in one mix refills the lanes of all of them.
21
21
  */
22
22
  moved: 0,
23
+ /**
24
+ * How many changes an `input` signal has reported, in any mix, so a fill can tell an input moved
25
+ * since it read it. One count for every mix, as `moved` is.
26
+ */
27
+ inputs: 0,
23
28
  };
@@ -0,0 +1,9 @@
1
+ import type { Subject } from './voice.js';
2
+ /**
3
+ * A subject's record for `voice` holding every field one can come to hold, in one order, so every
4
+ * record in a mix shares one hidden class: a record built elsewhere, or given `base`, `snaps` or
5
+ * `from` later, took a class of its own, and a fold walking a chain of several kinds reads each
6
+ * field the slow way. The caller sets what it knows; nothing here takes a number, which a call V8
7
+ * does not inline would box.
8
+ */
9
+ export declare function record(voice: object | null, reaches: boolean, state: unknown): Subject<unknown>;
package/dist/record.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * A subject's record for `voice` holding every field one can come to hold, in one order, so every
3
+ * record in a mix shares one hidden class: a record built elsewhere, or given `base`, `snaps` or
4
+ * `from` later, took a class of its own, and a fold walking a chain of several kinds reads each
5
+ * field the slow way. The caller sets what it knows; nothing here takes a number, which a call V8
6
+ * does not inline would box.
7
+ */
8
+ export function record(voice, reaches, state) {
9
+ return {
10
+ reaches,
11
+ delay: 0,
12
+ since: 0,
13
+ shown: 0,
14
+ weight: 0,
15
+ rested: false,
16
+ bands: null,
17
+ state,
18
+ stepped: 0,
19
+ ticks: 0,
20
+ probed: Number.NaN,
21
+ delta: null,
22
+ phase: 0,
23
+ seeks: 0,
24
+ rebuilt: 0,
25
+ base: undefined,
26
+ slope: undefined,
27
+ kept: null,
28
+ unkept: undefined,
29
+ snaps: undefined,
30
+ from: undefined,
31
+ unknown: undefined,
32
+ inputs: undefined,
33
+ replay: undefined,
34
+ voice,
35
+ next: null,
36
+ version: Number.NaN,
37
+ loci: null,
38
+ slot: -1,
39
+ run: -1,
40
+ freed: -1,
41
+ };
42
+ }
package/dist/relink.d.ts CHANGED
@@ -3,12 +3,16 @@ import type { Subject } from './voice.js';
3
3
  interface Ordered {
4
4
  readonly id: number;
5
5
  }
6
+ /** What `patch` asks a mix: a voice's record for a subject where a full relink would link it. */
7
+ interface Linker<V, I> {
8
+ linkable(voice: V, subject: I, slot: number): Subject<unknown> | null;
9
+ }
6
10
  /**
7
11
  * The voices over every subject behind each step of a mix's `version`, so a chain linked a few
8
12
  * steps back can be brought up to date by those voices alone; null for a step that changed
9
13
  * anything else, after which every chain links afresh.
10
14
  */
11
- export declare class Steps<V extends Ordered> {
15
+ export declare class Steps<V extends Ordered, I> {
12
16
  private list;
13
17
  /** The version before `list[0]`. */
14
18
  private from;
@@ -16,10 +20,12 @@ export declare class Steps<V extends Ordered> {
16
20
  push(voice: V | null, version: number): void;
17
21
  /**
18
22
  * A chain linked at `version`, its first record `head` (null where it holds none), with each voice
19
- * stepped since taken off it or put on at its place in voice order by `reach`, which gives the
20
- * voice's record where a full relink would link it and null where it would not. undefined where
21
- * the steps since do not say, so the chain must link afresh.
23
+ * stepped since taken off it or put on at its place in voice order by `mix.linkable`, which gives
24
+ * the voice's record where a full relink would link it and null where it would not. undefined
25
+ * where the steps since do not say, so the chain must link afresh. It takes the mix and the
26
+ * subject, not a function closed over them: a closure and its context a subject cost the `swap`
27
+ * row about 138 B each (bench/allocs.mjs, 2026-10-09).
22
28
  */
23
- patch(head: Subject<unknown> | null, version: number, reach: (voice: V) => Subject<unknown> | null): Subject<unknown> | null | undefined;
29
+ patch(head: Subject<unknown> | null, version: number, mix: Linker<V, I>, subject: I, slot: number): Subject<unknown> | null | undefined;
24
30
  }
25
31
  export {};
package/dist/relink.js CHANGED
@@ -21,11 +21,13 @@ export class Steps {
21
21
  }
22
22
  /**
23
23
  * A chain linked at `version`, its first record `head` (null where it holds none), with each voice
24
- * stepped since taken off it or put on at its place in voice order by `reach`, which gives the
25
- * voice's record where a full relink would link it and null where it would not. undefined where
26
- * the steps since do not say, so the chain must link afresh.
24
+ * stepped since taken off it or put on at its place in voice order by `mix.linkable`, which gives
25
+ * the voice's record where a full relink would link it and null where it would not. undefined
26
+ * where the steps since do not say, so the chain must link afresh. It takes the mix and the
27
+ * subject, not a function closed over them: a closure and its context a subject cost the `swap`
28
+ * row about 138 B each (bench/allocs.mjs, 2026-10-09).
27
29
  */
28
- patch(head, version, reach) {
30
+ patch(head, version, mix, subject, slot) {
29
31
  if (!(version >= this.from))
30
32
  return undefined;
31
33
  const list = this.list;
@@ -40,7 +42,7 @@ export class Steps {
40
42
  at = at.next;
41
43
  }
42
44
  const here = at !== null && at.voice === voice ? at : null;
43
- const want = reach(voice);
45
+ const want = mix.linkable(voice, subject, slot);
44
46
  if (want === here)
45
47
  continue;
46
48
  const after = here === null ? at : here.next;
@@ -0,0 +1,41 @@
1
+ import type { Leavings } from './leavings.js';
2
+ import { Store } from './store.js';
3
+ import type { Cut } from './transport.js';
4
+ import type { Subject } from './voice.js';
5
+ /**
6
+ * A voice's records once a seek has gone back: each subject's is put back from the record it had,
7
+ * the first time it is asked for after a seek, since the subjects a store holds cannot be listed.
8
+ * It is one store however many seeks there are: by subject it keeps how many seeks its record has
9
+ * been put back for, and a record behind is put back to the earliest moment sought since, as a
10
+ * seek back cuts what came after it and a later seek ahead of that moment must not read past it.
11
+ */
12
+ export declare class Restored<I> extends Store<I, Subject<unknown>> {
13
+ private readonly own;
14
+ private readonly make;
15
+ /** Seeks back so far. */
16
+ private n;
17
+ /** By subject, the `n` its record was last put back, written or deleted at; none reads as 0. */
18
+ private readonly at;
19
+ /**
20
+ * Each seek back sought to an earlier moment than every one after it, oldest first: the earliest
21
+ * moment sought since seek `n` is that of the first here at or after it.
22
+ */
23
+ private readonly least;
24
+ /** By seek, the records subjects left with after its moment, and where it cut; null until one has any. */
25
+ private futures;
26
+ /** The latest seek's moment. */
27
+ private t;
28
+ /**
29
+ * `own` holds the records as they stood before the first seek. `make` puts a record back as it
30
+ * stood at `t`, undefined where it had none then; `top` is the latest seek's moment.
31
+ */
32
+ constructor(own: Store<I, Subject<unknown>>, make: (live: Subject<unknown>, t: number, top: number) => Subject<unknown> | undefined);
33
+ /** A seek back to `t`: `left` has the records subjects left with after `cut`, null for none. */
34
+ back(t: number, cut: Cut, left: Leavings<I> | null): void;
35
+ /** The earliest moment sought by seek `from` or any after it. */
36
+ private since;
37
+ get(key: I): Subject<unknown> | undefined;
38
+ set(key: I, value: Subject<unknown>): void;
39
+ delete(key: I): void;
40
+ clear(): void;
41
+ }
@@ -0,0 +1,91 @@
1
+ import { Store } from './store.js';
2
+ /**
3
+ * A voice's records once a seek has gone back: each subject's is put back from the record it had,
4
+ * the first time it is asked for after a seek, since the subjects a store holds cannot be listed.
5
+ * It is one store however many seeks there are: by subject it keeps how many seeks its record has
6
+ * been put back for, and a record behind is put back to the earliest moment sought since, as a
7
+ * seek back cuts what came after it and a later seek ahead of that moment must not read past it.
8
+ */
9
+ export class Restored extends Store {
10
+ own;
11
+ make;
12
+ /** Seeks back so far. */
13
+ n = 0;
14
+ /** By subject, the `n` its record was last put back, written or deleted at; none reads as 0. */
15
+ at = new Store();
16
+ /**
17
+ * Each seek back sought to an earlier moment than every one after it, oldest first: the earliest
18
+ * moment sought since seek `n` is that of the first here at or after it.
19
+ */
20
+ least = [];
21
+ /** By seek, the records subjects left with after its moment, and where it cut; null until one has any. */
22
+ futures = null;
23
+ /** The latest seek's moment. */
24
+ t = Number.NaN;
25
+ /**
26
+ * `own` holds the records as they stood before the first seek. `make` puts a record back as it
27
+ * stood at `t`, undefined where it had none then; `top` is the latest seek's moment.
28
+ */
29
+ constructor(own, make) {
30
+ super();
31
+ this.own = own;
32
+ this.make = make;
33
+ }
34
+ /** A seek back to `t`: `left` has the records subjects left with after `cut`, null for none. */
35
+ back(t, cut, left) {
36
+ const n = ++this.n;
37
+ const least = this.least;
38
+ while (least.length > 0 && least[least.length - 1].t >= t)
39
+ least.pop();
40
+ least.push({ n, t });
41
+ this.t = t;
42
+ if (left !== null) {
43
+ this.futures ??= new Map();
44
+ this.futures.set(n, { left, cut });
45
+ }
46
+ }
47
+ /** The earliest moment sought by seek `from` or any after it. */
48
+ since(from) {
49
+ const least = this.least;
50
+ let lo = 0;
51
+ let hi = least.length - 1;
52
+ while (lo < hi) {
53
+ const mid = (lo + hi) >> 1;
54
+ if (least[mid].n >= from)
55
+ hi = mid;
56
+ else
57
+ lo = mid + 1;
58
+ }
59
+ return least[lo].t;
60
+ }
61
+ get(key) {
62
+ const at = this.at.get(key) ?? 0;
63
+ if (at === this.n)
64
+ return this.own.get(key);
65
+ this.at.set(key, this.n);
66
+ // A subject that left after the moment the next seek sought had the record it left with,
67
+ // whatever it has now. Leaving is a write, so no later seek's leavings can hold it.
68
+ const future = this.futures?.get(at + 1);
69
+ const live = future?.left.at(key, future.cut) ?? this.own.get(key);
70
+ if (live === undefined)
71
+ return undefined;
72
+ const made = this.make(live, this.since(at + 1), this.t);
73
+ if (made === undefined)
74
+ this.own.delete(key);
75
+ else
76
+ this.own.set(key, made);
77
+ return made;
78
+ }
79
+ set(key, value) {
80
+ this.own.set(key, value);
81
+ this.at.set(key, this.n);
82
+ }
83
+ delete(key) {
84
+ this.own.delete(key);
85
+ this.at.set(key, this.n);
86
+ }
87
+ clear() {
88
+ this.own.clear();
89
+ this.at.clear();
90
+ }
91
+ }
package/dist/revive.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Mixer } from './mixer.js';
2
2
  import type { Run } from './motions.js';
3
+ import type { Past } from './origin.js';
3
4
  import { type PackedRecord } from './pack.js';
4
5
  import { type Key } from './paging.js';
5
6
  import type { Described } from './types.js';
@@ -22,8 +23,13 @@ interface PackedVoice {
22
23
  outAt: number;
23
24
  outOver: number;
24
25
  outSet: boolean;
26
+ rebuilds: number;
27
+ past: number;
28
+ clocks: Past[] | null;
25
29
  cuedAt: number;
30
+ cuedSeq: number;
26
31
  doneAt: number;
32
+ doneSeq: number;
27
33
  opened: number;
28
34
  pinned: number;
29
35
  latest: number;
@@ -32,11 +38,12 @@ interface PackedVoice {
32
38
  log: Controls[] | null;
33
39
  played: boolean | undefined;
34
40
  playedAt: number;
41
+ playedSeq: number;
35
42
  records: [Key, PackedRecord][];
36
43
  runs: [Key, Run][];
37
- left: [Key, number, PackedRecord][];
38
- parts: [Key, number, number][];
39
- parted: [Key, number][];
44
+ left: [Key, number, number, PackedRecord, boolean][];
45
+ parts: [Key, number, number, number][];
46
+ parted: [Key, number, number][];
40
47
  }
41
48
  /**
42
49
  * Voices a history store holds whose handles something still holds, by id: a seek that revives