@msb235/blits 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/arcs.d.ts +40 -0
  3. package/dist/arcs.js +241 -0
  4. package/dist/chain.d.ts +6 -1
  5. package/dist/chain.js +33 -13
  6. package/dist/channels.d.ts +2 -0
  7. package/dist/channels.js +12 -1
  8. package/dist/clone.js +16 -4
  9. package/dist/cue.js +11 -2
  10. package/dist/departed.d.ts +5 -1
  11. package/dist/departed.js +71 -27
  12. package/dist/easing.d.ts +12 -1
  13. package/dist/easing.js +7 -1
  14. package/dist/everyone.d.ts +14 -1
  15. package/dist/everyone.js +27 -2
  16. package/dist/fade.js +3 -3
  17. package/dist/fill.js +16 -9
  18. package/dist/fit.d.ts +8 -1
  19. package/dist/fit.js +7 -3
  20. package/dist/fold.js +69 -13
  21. package/dist/gather.js +38 -7
  22. package/dist/handle.d.ts +2 -0
  23. package/dist/handle.js +8 -0
  24. package/dist/held.d.ts +5 -2
  25. package/dist/held.js +6 -3
  26. package/dist/history.d.ts +19 -2
  27. package/dist/history.js +149 -31
  28. package/dist/hosts.js +27 -11
  29. package/dist/index.d.ts +4 -1
  30. package/dist/index.js +2 -0
  31. package/dist/lanes.d.ts +23 -7
  32. package/dist/lanes.js +36 -2
  33. package/dist/lean.js +2 -2
  34. package/dist/marks.js +3 -0
  35. package/dist/mixer.d.ts +16 -1
  36. package/dist/mixer.js +60 -30
  37. package/dist/move.js +7 -2
  38. package/dist/numbers.d.ts +5 -1
  39. package/dist/numbers.js +4 -0
  40. package/dist/owner.js +6 -1
  41. package/dist/place.d.ts +8 -0
  42. package/dist/place.js +16 -8
  43. package/dist/project.js +3 -3
  44. package/dist/qualify.d.ts +10 -0
  45. package/dist/qualify.js +52 -1
  46. package/dist/reading.d.ts +2 -1
  47. package/dist/reading.js +2 -1
  48. package/dist/record.d.ts +6 -0
  49. package/dist/record.js +1 -0
  50. package/dist/restored.d.ts +67 -18
  51. package/dist/restored.js +159 -49
  52. package/dist/revive.d.ts +1 -0
  53. package/dist/revive.js +1 -0
  54. package/dist/roster.js +2 -1
  55. package/dist/seek.js +72 -18
  56. package/dist/shared.d.ts +1 -1
  57. package/dist/shared.js +1 -1
  58. package/dist/spans.d.ts +5 -0
  59. package/dist/spans.js +75 -5
  60. package/dist/store.d.ts +2 -0
  61. package/dist/store.js +4 -0
  62. package/dist/touch.js +3 -2
  63. package/dist/transport.js +6 -1
  64. package/dist/types/arc.d.ts +55 -0
  65. package/dist/types/arc.js +1 -0
  66. package/dist/types/mix.d.ts +24 -0
  67. package/dist/types/voice.d.ts +10 -1
  68. package/dist/types/watch.d.ts +126 -0
  69. package/dist/types/watch.js +1 -0
  70. package/dist/types.d.ts +2 -0
  71. package/dist/voice.d.ts +37 -6
  72. package/dist/voice.js +17 -0
  73. package/dist/watch.d.ts +28 -0
  74. package/dist/watch.js +467 -0
  75. package/dist/weigh.d.ts +13 -2
  76. package/dist/weigh.js +10 -3
  77. package/docs/schema.html +39 -3
  78. package/package.json +5 -4
package/dist/restored.js CHANGED
@@ -1,33 +1,70 @@
1
1
  import { Store } from './store.js';
2
+ const stamp = (of) => ({
3
+ t: Number.NaN,
4
+ seq: 0,
5
+ up: null,
6
+ rank: 0,
7
+ left: null,
8
+ cut: null,
9
+ of,
10
+ });
11
+ /** What a reaching record a store has let go of is stamped with, so setting it again is seen. */
12
+ const gone = stamp(null);
13
+ /** The root `s` was joined to, halving the path there for the next read. */
14
+ function rootOf(s) {
15
+ let x = s;
16
+ let up = x.up;
17
+ while (up !== null) {
18
+ const next = up.up;
19
+ if (next === null)
20
+ return up;
21
+ x.up = next;
22
+ x = next;
23
+ up = x.up;
24
+ }
25
+ return x;
26
+ }
2
27
  /**
3
28
  * A voice's records once a seek has gone back: each subject's is put back from the record it had,
4
29
  * 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.
30
+ * It is one store however many seeks there are: by subject it keeps the stamp of the seek its
31
+ * record was last put back at, and a record behind is put back to the earliest moment sought
32
+ * since, as a seek back cuts what came after it and a later seek ahead of that moment must not
33
+ * read past it.
8
34
  */
9
35
  export class Restored extends Store {
10
36
  own;
11
37
  now;
12
38
  make;
13
- /** Seeks back so far. */
14
- n = 0;
15
- /** By subject, the `n` its record was last put back, written or deleted at; none reads as 0. */
39
+ /** What a record put back now is stamped with, filled by the next seek. */
40
+ stamp = stamp(this);
41
+ /** What a record never stamped reads as: the first seek's. */
42
+ first = this.stamp;
43
+ /**
44
+ * By subject, the stamp its record was last put back, written or deleted at; none reads as
45
+ * `first`. A record that reaches its subject is its own, and holds it as `restored` instead,
46
+ * sparing a write here per subject a seek puts back; one that does not may stand for many.
47
+ */
16
48
  at = new Store();
17
49
  /**
18
- * Each seek back sought to an earlier moment than every one after it, oldest first: the earliest
19
- * moment sought since seek `n` is that of the first here at or after it.
50
+ * The roots, each moment earlier than the next, held weakly: one no record can reach is never
51
+ * read again, and those are cleared out once the list doubles.
52
+ */
53
+ roots = [];
54
+ kept = 0;
55
+ /**
56
+ * Stamps made a batch ahead of their seeks: one made at its seek is young when every old record
57
+ * is written with it, and V8 notes each such write for its next scavenge.
20
58
  */
21
- least = [];
22
- /** By seek, the records subjects left with after its moment, and where it cut; null until one has any. */
23
- futures = null;
59
+ ready = [];
60
+ aging = [];
24
61
  /** The latest seek's moment. */
25
- t = Number.NaN;
62
+ top = Number.NaN;
26
63
  /**
27
64
  * `own` holds the records as they stood before the first seek, and `now` gives the one a subject
28
- * that has not left since has there, as a seek to `t`, the end of frame `seq`, takes it. `make`
29
- * puts a record back as it stood at `t`, undefined where it had none then; `top` is the latest
30
- * seek's moment.
65
+ * that has not left since and is not reached has there, as a seek to `at` takes it. `make` puts
66
+ * a record back as it stood at `at`, undefined where it had none then; `mine` says `live` is the
67
+ * record this store held, which nothing else does.
31
68
  */
32
69
  constructor(own, now, make) {
33
70
  super();
@@ -37,57 +74,130 @@ export class Restored extends Store {
37
74
  }
38
75
  /** A seek back to `t`: `left` has the records subjects left with after `cut`, null for none. */
39
76
  back(t, cut, left) {
40
- const n = ++this.n;
41
- const least = this.least;
42
- while (least.length > 0 && least[least.length - 1].t >= t)
43
- least.pop();
44
- least.push({ n, t, seq: cut.seq });
45
- this.t = t;
77
+ const s = this.stamp;
46
78
  if (left !== null) {
47
- this.futures ??= new Map();
48
- this.futures.set(n, { left, cut });
79
+ s.left = left;
80
+ s.cut = cut;
81
+ }
82
+ // Every root at or after `t` now reads `t`, so they and `s` are joined under one.
83
+ const roots = this.roots;
84
+ let root = s;
85
+ let ref = null;
86
+ while (roots.length > 0) {
87
+ const r = roots[roots.length - 1].deref();
88
+ if (r !== undefined && r.t < t)
89
+ break;
90
+ const w = roots.pop();
91
+ if (r === undefined)
92
+ continue;
93
+ if (r.rank > root.rank) {
94
+ root.up = r;
95
+ root = r;
96
+ ref = w;
97
+ }
98
+ else {
99
+ r.up = root;
100
+ if (r.rank === root.rank)
101
+ root.rank++;
102
+ }
103
+ }
104
+ root.t = t;
105
+ root.seq = cut.seq;
106
+ roots.push(ref ?? new WeakRef(root));
107
+ if (roots.length > 2 * this.kept + 16) {
108
+ this.roots = roots.filter((w) => w.deref() !== undefined);
109
+ this.kept = this.roots.length;
49
110
  }
111
+ this.stamp = this.fresh();
112
+ this.top = t;
50
113
  }
51
- /** The earliest moment sought by seek `from` or any after it, and the frame it ended. */
52
- since(from) {
53
- const least = this.least;
54
- let lo = 0;
55
- let hi = least.length - 1;
56
- while (lo < hi) {
57
- const mid = (lo + hi) >> 1;
58
- if (least[mid].n >= from)
59
- hi = mid;
60
- else
61
- lo = mid + 1;
114
+ /** The next seek's stamp, from a batch made a batch of seeks ago, twice as many each time to 64. */
115
+ fresh() {
116
+ if (this.ready.length === 0) {
117
+ const n = Math.min(64, Math.max(1, this.aging.length * 2));
118
+ this.ready = this.aging.reverse();
119
+ this.aging = [];
120
+ for (let i = 0; i < n; i++)
121
+ this.aging.push(stamp(this));
62
122
  }
63
- return least[lo];
123
+ return this.ready.pop() ?? stamp(this);
64
124
  }
125
+ /**
126
+ * Whether a reaching record stamped by this store is still its record for the key it was found
127
+ * under: every record let go is stamped `gone`, copies are stamped with nothing, and this turns
128
+ * false if a stamped record is set again, which could put it under a second key.
129
+ */
130
+ sure = true;
65
131
  get(key) {
66
- const at = this.at.get(key) ?? 0;
67
- if (at === this.n)
68
- return this.own.get(key);
69
- this.at.set(key, this.n);
132
+ return this.from(key, this.own.get(key));
133
+ }
134
+ /** A chain's record for a subject, found again without a lookup where this store stamped it. */
135
+ find(key, guess) {
136
+ const s = guess.restored;
137
+ return s !== null && s.of === this && this.sure ? this.from(key, guess) : this.get(key);
138
+ }
139
+ /** `get`, given what `own` holds for `key`. */
140
+ from(key, own) {
141
+ const mark = own !== undefined && own.reaches;
142
+ const at = (mark ? own.restored : this.at.get(key)) ?? this.first;
143
+ const now = this.stamp;
144
+ if (at === now)
145
+ return own;
146
+ if (mark)
147
+ own.restored = now;
148
+ else
149
+ this.at.set(key, now);
70
150
  // A subject that left after the moment the next seek sought had the record it left with,
71
151
  // whatever it has now. Leaving is a write, so no later seek's leavings can hold it.
72
- const future = this.futures?.get(at + 1);
73
- const { t, seq } = this.since(at + 1);
74
- const live = future?.left.at(key, future.cut) ?? this.now(key, this.own.get(key), t, seq);
75
- const made = live && this.make(live, t, this.t);
76
- if (made === undefined)
77
- this.own.delete(key);
78
- else
79
- this.own.set(key, made);
152
+ const root = rootOf(at);
153
+ const left = at.left?.at(key, at.cut);
154
+ const live = left ?? this.now(key, own, root);
155
+ const made = live && this.make(live, root, left === undefined);
156
+ // Looked up again, not taken from `own`: a call made while it was put back may have set it.
157
+ if (made === undefined) {
158
+ this.shed(key);
159
+ if (mark)
160
+ this.at.set(key, now);
161
+ }
162
+ else if (made !== own)
163
+ this.set(key, made);
164
+ else if (!made.reaches)
165
+ this.at.set(key, now);
80
166
  return made;
81
167
  }
82
168
  set(key, value) {
169
+ const old = this.own.get(key);
170
+ if (old === value) {
171
+ if (value.reaches)
172
+ value.restored = this.stamp;
173
+ else
174
+ this.at.set(key, this.stamp);
175
+ return;
176
+ }
177
+ if (old?.reaches)
178
+ old.restored = gone;
179
+ // Stamped already, it may be in a chain under another key.
180
+ if (value.reaches && value.restored !== null)
181
+ this.sure = false;
83
182
  this.own.set(key, value);
84
- this.at.set(key, this.n);
183
+ if (value.reaches)
184
+ value.restored = this.stamp;
185
+ else
186
+ this.at.set(key, this.stamp);
85
187
  }
86
188
  delete(key) {
189
+ this.shed(key);
190
+ this.at.set(key, this.stamp);
191
+ }
192
+ /** Lets go of the record held for `key`, stamping it `gone`. */
193
+ shed(key) {
194
+ const old = this.own.get(key);
195
+ if (old?.reaches)
196
+ old.restored = gone;
87
197
  this.own.delete(key);
88
- this.at.set(key, this.n);
89
198
  }
90
199
  clear() {
200
+ this.sure = false;
91
201
  this.own.clear();
92
202
  this.at.clear();
93
203
  }
package/dist/revive.d.ts CHANGED
@@ -24,6 +24,7 @@ interface PackedVoice {
24
24
  outOver: number;
25
25
  outSet: boolean;
26
26
  rebuilds: number;
27
+ hold: number;
27
28
  past: number;
28
29
  clocks: Past[] | null;
29
30
  cuedAt: number;
package/dist/revive.js CHANGED
@@ -83,6 +83,7 @@ function packVoice(voice, keys) {
83
83
  outOver: voice.outOver,
84
84
  outSet: voice.outSet,
85
85
  rebuilds: voice.rebuilds,
86
+ hold: voice.hold,
86
87
  past: voice.clocks?.length ?? 0,
87
88
  clocks: voice.clocks,
88
89
  cuedAt: voice.cuedAt,
package/dist/roster.js CHANGED
@@ -4,7 +4,8 @@ import { Store } from './store.js';
4
4
  function tally(mix, voice, by) {
5
5
  if (voice.spec.locus !== undefined)
6
6
  mix.loci += by;
7
- if (voice.spec.anchor !== undefined)
7
+ const e = voice.spec.earliest;
8
+ if (voice.spec.anchor !== undefined || (e !== undefined && typeof e !== 'number'))
8
9
  mix.anchored += by;
9
10
  }
10
11
  /** Counts a voice just put in the list into everything a sync and a fold skip work by. */
package/dist/seek.js CHANGED
@@ -2,7 +2,7 @@ import { heldAt } from './chain.js';
2
2
  import { schedule } from './due.js';
3
3
  import { fadesAt } from './fade.js';
4
4
  import { origin } from './held.js';
5
- import { apart, copyHeld, last, lastWithin } from './history.js';
5
+ import { apart, copyHeld, copyHeldInto, lastIndex, lastWithin, unread } from './history.js';
6
6
  import { laneHost } from './hosts.js';
7
7
  import { Leavings } from './leavings.js';
8
8
  import { move, nextFrame } from './move.js';
@@ -22,12 +22,15 @@ import { cover, coverFrame, cut } from './unpage.js';
22
22
  * probe by `t` read is not kept, and a lane holds it apart from the record, so the record reads 0
23
23
  * until the next probe.
24
24
  */
25
- function restore(mix, voice, live, t, every) {
25
+ function restore(mix, voice, live, at, every, mine) {
26
+ // Read here, not passed: a double passed to a call is boxed, once per record put back.
27
+ const t = at.t;
26
28
  const inputs = live.inputs?.filter((e) => e.at <= t);
27
29
  const snaps = live.snaps;
28
30
  if (snaps === undefined) {
29
- // Nothing a step or `setting.keep` holds: what it keeps does not move with time.
30
- const h = copyHeld(voice, live);
31
+ // Nothing a step or `setting.keep` holds: what it keeps does not move with time. One record
32
+ // stands for every subject a voice does not reach, so that one is copied.
33
+ const h = mine && live.reaches ? unread(live) : copyHeld(voice, live);
31
34
  if (h.stepped > t)
32
35
  h.stepped = t;
33
36
  // Whether it had rested by `t` is not kept; the next probe finds it again.
@@ -38,37 +41,84 @@ function restore(mix, voice, live, t, every) {
38
41
  origin(mix, voice, h);
39
42
  return h;
40
43
  }
41
- const snap = last(snaps, t, true);
42
- if (snap === undefined)
44
+ // A seek back to just before now finds its copy among the newest, looked at before searching.
45
+ const n = snaps.length;
46
+ const i = n > 0 && snaps[n - 1].at <= t
47
+ ? n - 1
48
+ : n > 1 && snaps[n - 2].at <= t
49
+ ? n - 2
50
+ : lastIndex(snaps, t, true);
51
+ if (i < 0)
43
52
  return undefined;
44
- const h = copyHeld(voice, snap.held);
53
+ const snap = snaps[i];
54
+ // What the record replaced held, which a record put back over it would lose.
55
+ const { ticks, since, rebuilt, state, slot, restored } = live;
56
+ let h;
57
+ if (mine && live.reaches) {
58
+ // Its chain may still read its number from it, and its store has just stamped it.
59
+ h = copyHeldInto(voice, snap.held, live, true);
60
+ h.slot = slot;
61
+ h.restored = restored;
62
+ }
63
+ else
64
+ h = copyHeld(voice, snap.held, true);
45
65
  h.weight = 0;
46
66
  if (snap.at < t)
47
67
  h.copied = true;
48
- h.snaps = snaps.slice(0, snaps.indexOf(snap) + 1);
68
+ const kept = i + 1;
69
+ // The record replaced is let go, so its list is cut where it stands.
70
+ if (mine)
71
+ trim(voice, snaps, kept);
72
+ else
73
+ for (let i = 0; i < kept; i++)
74
+ snaps[i].shared = true;
75
+ h.snaps = mine ? snaps : snaps.slice(0, kept);
49
76
  h.inputs = inputs;
50
77
  // A start still ahead when the copy was kept may have moved before `t`, with no copy between.
51
78
  if (!(h.since <= snap.at))
52
79
  origin(mix, voice, h);
53
- if (voice.patch.step !== undefined && (snap.ahead !== undefined || t - snap.at >= every)) {
54
- const ahead = snap.ahead;
80
+ // The record a seek replaces, stepped on from this copy with nothing since rebuilding or moving
81
+ // its start, holds the copy's state at its own `ticks`, which a probe there need not step to.
82
+ const was = mine && ticks > h.ticks && since === h.since && rebuilt === h.rebuilt;
83
+ if (voice.patch.step !== undefined && (snap.ahead !== undefined || t - snap.at >= every || was)) {
84
+ let ahead = snap.ahead;
55
85
  // Steps counted from a start that has moved since are not this record's.
56
- if (ahead === undefined || ahead.since !== h.since)
57
- snap.ahead = {
86
+ if (ahead === undefined || ahead.since !== h.since) {
87
+ ahead = {
58
88
  put: h,
59
89
  sought: t,
60
90
  since: h.since,
61
91
  at: snap.at,
62
92
  ticks: h.ticks,
63
93
  state: undefined,
94
+ leftTicks: 0,
95
+ left: undefined,
64
96
  };
97
+ snap.ahead = ahead;
98
+ }
65
99
  else {
66
100
  ahead.put = h;
67
101
  ahead.sought = t;
68
102
  }
103
+ if (was) {
104
+ // Nothing else holds the record replaced, so its state is taken, not copied.
105
+ ahead.leftTicks = ticks;
106
+ ahead.left = state;
107
+ }
69
108
  }
70
109
  return h;
71
110
  }
111
+ /** Cuts a record's list of copies to its first `kept`, keeping those no other list holds as spares. */
112
+ function trim(voice, snaps, kept) {
113
+ while (snaps.length > kept) {
114
+ const snap = snaps.pop();
115
+ if (snap.shared)
116
+ continue;
117
+ snap.ahead = undefined;
118
+ voice.spares ??= [];
119
+ voice.spares.push(snap);
120
+ }
121
+ }
72
122
  /** Takes a voice cued after the moment sought out of the mix, until the tape cues it again. */
73
123
  function park(mix, voice) {
74
124
  const owner = voice.owner;
@@ -122,6 +172,7 @@ function restoreVoice(mix, voice, t, seq) {
122
172
  voice.unreached = null;
123
173
  voice.unreachedBits = null;
124
174
  voice.seeks++;
175
+ voice.spares = null;
125
176
  const left = voice.left?.all() ?? [];
126
177
  if (voice.left !== null)
127
178
  fadesAt(voice, voice.left.after(cut), seq);
@@ -130,21 +181,22 @@ function restoreVoice(mix, voice, t, seq) {
130
181
  let records = voice.subjects;
131
182
  if (!(records instanceof Restored)) {
132
183
  const every = apart(mix.opts.history);
133
- const now = (subject, held, least, seq) => heldAt(voice, subject, held, least, seq);
134
- records = new Restored(records, now, (live, least, top) => {
135
- const h = restore(mix, voice, live, least, every);
184
+ const now = (subject, held, at) => heldAt(voice, subject, held, at.t, at.seq);
185
+ const restored = new Restored(records, now, (live, at, mine) => {
186
+ const h = restore(mix, voice, live, at, every, mine);
136
187
  if (h === undefined)
137
188
  return h;
138
189
  if (h.reaches)
139
190
  voice.seen++;
140
191
  if (h.rested)
141
192
  voice.restedCount++;
142
- if (!(h.since <= top)) {
193
+ if (!(h.since <= restored.top)) {
143
194
  voice.early ??= [];
144
195
  voice.early.push(h);
145
196
  }
146
197
  return h;
147
198
  });
199
+ records = restored;
148
200
  voice.subjects = records;
149
201
  }
150
202
  records.back(t, cut, future);
@@ -277,10 +329,12 @@ function back(mix, t, seq, on) {
277
329
  voice.holding.back(seq);
278
330
  }
279
331
  // Rebuilt from the voices that are back: the lanes, the chains and every count the fold reads.
332
+ // Subject numbers carry over, and with them the chains that hold them, each linked afresh.
280
333
  const oldLanes = mix.lanes;
281
334
  if (oldLanes !== null && mix.lanesOf !== null)
282
- mix.lanes = new mix.lanesOf(laneHost(mix));
283
- mix.chains = new Store();
335
+ mix.lanes = new mix.lanesOf(laneHost(mix), oldLanes);
336
+ else
337
+ mix.chains = new Store();
284
338
  mix.owners = kept.some((v) => v.holding !== null) ? kept.filter((v) => v.holding !== null) : null;
285
339
  recount(mix);
286
340
  mix.parters.clear();
package/dist/shared.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * The package version, for a fact whose shape follows the code too closely to version by hand:
3
3
  * copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
4
4
  */
5
- export declare const release = "0.11.0";
5
+ export declare const release = "0.12.1";
6
6
  /**
7
7
  * A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
8
8
  * patch or channel it made is read by another copy's mix. The name carries a version, bumped when
package/dist/shared.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * The package version, for a fact whose shape follows the code too closely to version by hand:
3
3
  * copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
4
4
  */
5
- export const release = '0.11.0';
5
+ export const release = '0.12.1';
6
6
  /**
7
7
  * A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
8
8
  * patch or channel it made is read by another copy's mix. The name carries a version, bumped when
package/dist/spans.d.ts CHANGED
@@ -22,6 +22,11 @@ export declare function checkChild<I, O>(spec: VoiceSpec<I, O>, child: Voice<I,
22
22
  * after.
23
23
  */
24
24
  export declare function refit<I, O>(mix: Mixer<I, O>, span: Voice<I, O>, from?: number): void;
25
+ /**
26
+ * Fits again each span with a child still to start whose anchored `earliest` answers differently
27
+ * than at its last fit: an anchor that has come to have an answer, or moved.
28
+ */
29
+ export declare function refloor<I, O>(mix: Mixer<I, O>): void;
25
30
  /** Whether a span whose last child just left at mix time `at` stays, its budget not yet out. */
26
31
  export declare function lingers<I, O>(span: Voice<I, O>, at: number): boolean;
27
32
  /** Lets the spans that are empty and out of budget leave, at the moment their budget ran out. */
package/dist/spans.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { arcEnd } from './arcs.js';
1
2
  import { elapsedWith, retime } from './clock.js';
2
3
  import { retire } from './fade.js';
3
4
  import { defaultFit, layout, settle } from './fit.js';
@@ -6,6 +7,7 @@ import { noted } from './history.js';
6
7
  import { nextFrame } from './move.js';
7
8
  import { leave } from './origin.js';
8
9
  import { ownerReading } from './owner.js';
10
+ import { resolve } from './place.js';
9
11
  /** What a span keeps beside its owner: its claim, its fit, and how the last fit came out. */
10
12
  export class Fitting {
11
13
  budget;
@@ -39,9 +41,13 @@ export class Fitting {
39
41
  export function checkChild(spec, child) {
40
42
  if (spec.start !== undefined || spec.anchor?.start !== undefined || spec.anchor?.in !== undefined)
41
43
  throw new Error('blits: a span places the voices it holds, so one takes no start of its own');
42
- if (child.holding !== null) {
44
+ if (child.arc !== null) {
45
+ if (!Number.isFinite(arcEnd(child)))
46
+ throw new Error('blits: a span cannot fit an arc held open, which never ends');
47
+ }
48
+ else if (child.holding !== null) {
43
49
  if (child.fitting === null)
44
- throw new Error('blits: a span holds voices and spans, not owners');
50
+ throw new Error('blits: a span holds voices, spans and arcs, not owners');
45
51
  }
46
52
  else if (!Number.isFinite(child.span))
47
53
  throw new Error('blits: a span cannot fit a voice that never ends');
@@ -59,7 +65,7 @@ function elapsedIn(child, local) {
59
65
  }
60
66
  /** How much of a child is left, voice ms at its own clock: what its span still has to fit. */
61
67
  function leftOf(child, local) {
62
- const length = child.fitting !== null ? child.fitting.end : child.span;
68
+ const length = child.fitting !== null ? child.fitting.end : child.arc !== null ? arcEnd(child) : child.span;
63
69
  return Math.max(0, length - elapsedIn(child, local));
64
70
  }
65
71
  /**
@@ -73,10 +79,11 @@ export function refit(mix, span, from = mix.now) {
73
79
  if (mix.projecting || span.state === 'done')
74
80
  return;
75
81
  const local = clockOf(span, from);
76
- const kids = (span.holding?.children ?? []).filter((c) => c.state !== 'done' && c.state !== 'frozen' && c.state !== 'fading' && leftOf(c, local) > 0);
77
- const claims = kids.map((c) => {
82
+ const { kids, floors, waiting } = lineUp(mix, span, local);
83
+ const claims = kids.map((c, i) => {
78
84
  const s = c.spec;
79
85
  return {
86
+ floor: floors[i],
80
87
  natural: leftOf(c, local) / (s.rate ?? 1),
81
88
  state: c.state === 'pending' ? 'pending' : 'playing',
82
89
  faster: Math.max(1, s.faster ?? 1),
@@ -127,6 +134,17 @@ export function refit(mix, span, from = mix.now) {
127
134
  noted(mix, c);
128
135
  }
129
136
  });
137
+ for (const c of waiting) {
138
+ if (c.start === Number.POSITIVE_INFINITY)
139
+ continue;
140
+ c.start = Number.POSITIVE_INFINITY;
141
+ c.anchorNow = Number.POSITIVE_INFINITY;
142
+ c.anchorElapsed = 0;
143
+ c.clocks = null;
144
+ mix.frame = nextFrame();
145
+ reorigin(mix, c, mix.now);
146
+ noted(mix, c);
147
+ }
130
148
  if (kids.length > 0)
131
149
  mix.lanes?.refill();
132
150
  const reach = local + length;
@@ -147,6 +165,58 @@ export function refit(mix, span, from = mix.now) {
147
165
  refit(mix, span.owner, from);
148
166
  }
149
167
  }
168
+ /**
169
+ * A span's children still to fit, in the order it lays them out: those playing first, as cued, then
170
+ * those still to start by rank. Each pending one's floor is where its `earliest` answers, span ms
171
+ * from `local`, and before `local` only in the frame it was cued. One whose anchor has no answer yet waits unplaced, and under `queue` or `stagger`
172
+ * so does every child after it, since each is laid out from the one before.
173
+ */
174
+ function lineUp(mix, span, local) {
175
+ const live = (span.holding?.children ?? []).filter((c) => c.state !== 'done' && c.state !== 'frozen' && c.state !== 'fading' && leftOf(c, local) > 0);
176
+ const kids = live.filter((c) => c.state !== 'pending');
177
+ const floors = kids.map(() => undefined);
178
+ const waiting = [];
179
+ const pending = live
180
+ .filter((c) => c.state === 'pending')
181
+ .sort((a, b) => (a.spec.rank ?? 0) - (b.spec.rank ?? 0));
182
+ const chained = span.fitting.claim.order !== 'together';
183
+ for (const c of pending) {
184
+ const e = c.spec.earliest;
185
+ const t = e === undefined ? undefined : resolve(mix, e, c, Number.NEGATIVE_INFINITY);
186
+ c.floorSeen = t ?? Number.NaN;
187
+ if ((e !== undefined && t === undefined) || (chained && waiting.length > 0)) {
188
+ waiting.push(c);
189
+ continue;
190
+ }
191
+ kids.push(c);
192
+ // An anchor that answered before the span began has no time on its clock: no bound.
193
+ const floor = t === undefined || t === Number.NEGATIVE_INFINITY ? undefined : t - local;
194
+ // A past floor places a child only as it is cued: one waiting since waits on those before it.
195
+ floors.push(floor === undefined || c.cuedSeq === mix.transport.seq ? floor : Math.max(0, floor));
196
+ }
197
+ return { kids, floors, waiting };
198
+ }
199
+ /**
200
+ * Fits again each span with a child still to start whose anchored `earliest` answers differently
201
+ * than at its last fit: an anchor that has come to have an answer, or moved.
202
+ */
203
+ export function refloor(mix) {
204
+ if (mix.projecting)
205
+ return;
206
+ for (const span of mix.owners ?? []) {
207
+ if (span.fitting === null || span.state === 'done')
208
+ continue;
209
+ const moved = (span.holding?.children ?? []).some((c) => {
210
+ const e = c.spec.earliest;
211
+ if (c.state !== 'pending' || e === undefined || typeof e === 'number')
212
+ return false;
213
+ const t = resolve(mix, e, c, Number.NEGATIVE_INFINITY) ?? Number.NaN;
214
+ return !Object.is(t, c.floorSeen);
215
+ });
216
+ if (moved)
217
+ refit(mix, span);
218
+ }
219
+ }
150
220
  /** Whether a span whose last child just left at mix time `at` stays, its budget not yet out. */
151
221
  export function lingers(span, at) {
152
222
  const f = span.fitting;
package/dist/store.d.ts CHANGED
@@ -16,6 +16,8 @@ export declare class Store<K, V> {
16
16
  private oneValue;
17
17
  get(key: K): V | undefined;
18
18
  set(key: K, value: V): void;
19
+ /** As `get`, where `guess` may be the value for `key`, which a store that can tell spares a lookup. */
20
+ find(key: K, _guess: V): V | undefined;
19
21
  has(key: K): boolean;
20
22
  delete(key: K): void;
21
23
  clear(): void;
package/dist/store.js CHANGED
@@ -43,6 +43,10 @@ export class Store {
43
43
  }
44
44
  this.strong.set(key, value);
45
45
  }
46
+ /** As `get`, where `guess` may be the value for `key`, which a store that can tell spares a lookup. */
47
+ find(key, _guess) {
48
+ return this.get(key);
49
+ }
46
50
  has(key) {
47
51
  return this.get(key) !== undefined;
48
52
  }
package/dist/touch.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { headOf } from './chain.js';
1
2
  import { restamp } from './everyone.js';
2
3
  import { nextFrame } from './move.js';
3
4
  /**
@@ -16,10 +17,10 @@ export function touch(mix, subject) {
16
17
  }
17
18
  else {
18
19
  // No voice's `seeks` is below 0, so no record stamped with it reads as current.
19
- let held = mix.chains.get(subject) ?? null;
20
+ let held = headOf(mix, subject) ?? null;
20
21
  for (; held !== null; held = held.next)
21
22
  held.seeks = -1;
22
- const slot = mix.chains.get(subject)?.slot ?? -1;
23
+ const slot = headOf(mix, subject)?.slot ?? -1;
23
24
  for (const voice of mix.sharers)
24
25
  restamp(voice, slot);
25
26
  }