@msb235/blits 0.7.1 → 0.8.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 (156) hide show
  1. package/CHANGELOG.md +193 -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 +8 -0
  11. package/dist/clone.js +22 -0
  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 +31 -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 +34 -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 +216 -31
  36. package/dist/frames.d.ts +21 -0
  37. package/dist/frames.js +59 -0
  38. package/dist/gather.d.ts +15 -9
  39. package/dist/gather.js +60 -25
  40. package/dist/gone.js +10 -0
  41. package/dist/handle.d.ts +16 -5
  42. package/dist/handle.js +56 -6
  43. package/dist/held.d.ts +8 -5
  44. package/dist/held.js +56 -58
  45. package/dist/history.d.ts +17 -13
  46. package/dist/history.js +47 -28
  47. package/dist/hosts.js +55 -8
  48. package/dist/index.d.ts +9 -4
  49. package/dist/index.js +5 -2
  50. package/dist/keyfill.d.ts +7 -0
  51. package/dist/keyfill.js +124 -0
  52. package/dist/keyrows.d.ts +1 -0
  53. package/dist/keyrows.js +1 -1
  54. package/dist/lane.d.ts +32 -2
  55. package/dist/lane.js +5 -0
  56. package/dist/lanes.d.ts +32 -8
  57. package/dist/lanes.js +69 -37
  58. package/dist/leavings.d.ts +26 -0
  59. package/dist/leavings.js +70 -0
  60. package/dist/locus.js +2 -2
  61. package/dist/meet.js +14 -8
  62. package/dist/mixer.d.ts +39 -7
  63. package/dist/mixer.js +50 -10
  64. package/dist/motion.d.ts +2 -0
  65. package/dist/motion.js +4 -1
  66. package/dist/motions.d.ts +22 -15
  67. package/dist/motions.js +40 -34
  68. package/dist/move.js +23 -16
  69. package/dist/moved.d.ts +11 -0
  70. package/dist/moved.js +29 -0
  71. package/dist/origin.d.ts +19 -0
  72. package/dist/origin.js +68 -0
  73. package/dist/owed.d.ts +4 -4
  74. package/dist/owed.js +8 -8
  75. package/dist/owner.d.ts +1 -1
  76. package/dist/owner.js +2 -2
  77. package/dist/pace.d.ts +13 -5
  78. package/dist/pace.js +21 -8
  79. package/dist/pack.d.ts +3 -1
  80. package/dist/pack.js +15 -24
  81. package/dist/paging.d.ts +8 -2
  82. package/dist/paging.js +39 -4
  83. package/dist/patch.d.ts +14 -9
  84. package/dist/patch.js +83 -21
  85. package/dist/place.js +18 -3
  86. package/dist/project.d.ts +12 -1
  87. package/dist/project.js +101 -83
  88. package/dist/pull.d.ts +2 -0
  89. package/dist/pull.js +8 -0
  90. package/dist/qualify.js +10 -2
  91. package/dist/reading.d.ts +5 -0
  92. package/dist/reading.js +5 -0
  93. package/dist/record.d.ts +9 -0
  94. package/dist/record.js +42 -0
  95. package/dist/relink.d.ts +11 -5
  96. package/dist/relink.js +7 -5
  97. package/dist/restored.d.ts +41 -0
  98. package/dist/restored.js +91 -0
  99. package/dist/revive.d.ts +10 -3
  100. package/dist/revive.js +31 -23
  101. package/dist/roster.d.ts +11 -0
  102. package/dist/roster.js +46 -0
  103. package/dist/rotation.d.ts +29 -0
  104. package/dist/rotation.js +91 -0
  105. package/dist/rows.d.ts +5 -5
  106. package/dist/rows.js +5 -5
  107. package/dist/sample.d.ts +3 -2
  108. package/dist/sample.js +32 -15
  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 +45 -6
  129. package/dist/transport.js +64 -19
  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 +53 -25
  150. package/dist/voice.d.ts +90 -22
  151. package/dist/voice.js +105 -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/spans.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { elapsedWith, retime } from './clock.js';
2
2
  import { retire } from './fade.js';
3
3
  import { defaultFit, layout, settle } from './fit.js';
4
+ import { reorigin } from './held.js';
4
5
  import { noted } from './history.js';
5
6
  import { nextFrame } from './move.js';
7
+ import { leave } from './origin.js';
6
8
  import { ownerReading } from './owner.js';
7
9
  /** What a span keeps beside its owner: its claim, its fit, and how the last fit came out. */
8
10
  export class Fitting {
@@ -89,6 +91,7 @@ export function refit(mix, span) {
89
91
  kids.forEach((c, i) => {
90
92
  const rate = (c.spec.rate ?? 1) * plan.rate[i];
91
93
  const skipped = plan.skip[i] && c.holding === null;
94
+ c.skipped = skipped;
92
95
  let moved = false;
93
96
  if (c.state === 'pending') {
94
97
  const start = local + at[i];
@@ -99,23 +102,27 @@ export function refit(mix, span) {
99
102
  c.anchorElapsed = elapsed;
100
103
  c.rate = rate;
101
104
  c.ramp = null;
105
+ c.clocks = null;
102
106
  moved = true;
103
107
  }
104
108
  }
105
109
  else {
106
110
  if (skipped) {
111
+ leave(c, local);
107
112
  c.rebase(local);
108
113
  c.anchorElapsed = c.span;
109
114
  c.seeks++;
110
115
  moved = true;
111
116
  }
112
117
  if (c.rate !== rate || c.ramp !== null) {
118
+ leave(c, local);
113
119
  retime(c, local, rate, 0);
114
120
  moved = true;
115
121
  }
116
122
  }
117
123
  if (moved) {
118
124
  mix.frame = nextFrame();
125
+ reorigin(mix, c, mix.now);
119
126
  noted(mix, c);
120
127
  }
121
128
  });
@@ -0,0 +1,11 @@
1
+ import type { Kit } from './types.js';
2
+ /**
3
+ * Folds deltas into one pose by a kit's arithmetic, in order, with no mix and no clock: what a mix
4
+ * gives for voices contributing these deltas at these weights. Each channel starts at its rest, a
5
+ * weight (default 1, held to 0..1) fades a delta toward rest, and the pose is clamped to each
6
+ * channel's bounds. A channel with no rest takes the last delta whose weight is 0.6 or more, where
7
+ * a mix's default band first switches one on, and stays undefined where none is.
8
+ *
9
+ * @category channel
10
+ */
11
+ export declare function fold<O>(kit: Kit<O>, deltas: readonly Partial<O>[], weights?: readonly number[]): O;
package/dist/stack.js ADDED
@@ -0,0 +1,47 @@
1
+ import { copy, first } from './clone.js';
2
+ import { bound, merged } from './fold.js';
3
+ /** The weight a mix's default band first switches a rest-less channel on at. */
4
+ const ON = 0.6;
5
+ /**
6
+ * Folds deltas into one pose by a kit's arithmetic, in order, with no mix and no clock: what a mix
7
+ * gives for voices contributing these deltas at these weights. Each channel starts at its rest, a
8
+ * weight (default 1, held to 0..1) fades a delta toward rest, and the pose is clamped to each
9
+ * channel's bounds. A channel with no rest takes the last delta whose weight is 0.6 or more, where
10
+ * a mix's default band first switches one on, and stays undefined where none is.
11
+ *
12
+ * @category channel
13
+ */
14
+ export function fold(kit, deltas, weights) {
15
+ const pose = {};
16
+ const names = Object.keys(kit);
17
+ for (const key of names) {
18
+ const channel = kit[key];
19
+ const rest = channel.rest;
20
+ pose[key] = rest === undefined ? undefined : channel.copy ? channel.copy(rest) : copy(rest);
21
+ }
22
+ for (let i = 0; i < deltas.length; i++) {
23
+ const given = weights?.[i] ?? 1;
24
+ const w = given > 1 ? 1 : given;
25
+ if (!(w > 0))
26
+ continue;
27
+ const delta = deltas[i];
28
+ for (const key of names) {
29
+ const value = delta[key];
30
+ if (value === undefined)
31
+ continue;
32
+ const channel = kit[key];
33
+ if (channel.rest !== undefined && channel.scale)
34
+ pose[key] = channel.fold
35
+ ? channel.fold(pose[key], value, w)
36
+ : merged(channel, pose[key], channel.scale(value, w));
37
+ else if (w >= ON)
38
+ pose[key] = pose[key] === undefined ? first(value) : merged(channel, pose[key], value);
39
+ }
40
+ }
41
+ for (const key of names) {
42
+ const channel = kit[key];
43
+ if (channel.bounds)
44
+ pose[key] = bound(channel, pose[key]);
45
+ }
46
+ return pose;
47
+ }
@@ -0,0 +1,10 @@
1
+ import type { Mixer } from './mixer.js';
2
+ import type { Cut, Transport } from './transport.js';
3
+ /**
4
+ * Throws unless a read back to mix time `t` is exact with no history: every voice plays as it was
5
+ * cued and reads the same whenever it is asked, and no mix on the transport has changed since `t`.
6
+ * A mix keeps only how it stands now, which is how it has stood since its last change.
7
+ */
8
+ export declare function stands(transport: Transport, t: number): void;
9
+ /** A mix's copy for a read back to `t` with no history: its voices as they stand, each at `t`. */
10
+ export declare function copyStanding<I, O>(mix: Mixer<I, O>, c: Mixer<I, O>, t: number, cut: Cut): void;
@@ -0,0 +1,79 @@
1
+ import { ownBlends } from './blend.js';
2
+ import { relink } from './owner.js';
3
+ import { pin } from './place.js';
4
+ import { mirror, recall, stateAt } from './project.js';
5
+ import { recount } from './roster.js';
6
+ /**
7
+ * Why a voice cannot be read at an earlier time from how it stands now, or undefined where it can:
8
+ * what it shows depends on more than its clock as cued, so the frames it has played decided it.
9
+ */
10
+ function unfit(mix, voice) {
11
+ const patch = voice.patch;
12
+ const spec = voice.spec;
13
+ if (patch.state !== undefined || patch.step !== undefined)
14
+ return 'its patch keeps state';
15
+ if (voice.motion !== undefined)
16
+ return 'it plays a motion';
17
+ if (voice.keeping)
18
+ return 'its patch or its weight keeps state';
19
+ if (spec.from === 'current')
20
+ return 'it starts from the pose its subjects had';
21
+ if (voice.written)
22
+ return 'its handle has been written to';
23
+ if (spec.anchor !== undefined)
24
+ return 'an anchor places it';
25
+ if (voice.holding !== null)
26
+ return 'it is an owner';
27
+ if (voice.owner !== null)
28
+ return 'it plays under an owner';
29
+ // A rest-less channel switches on across a band, by the weights its voice has passed through.
30
+ const steady = typeof spec.weight !== 'function' &&
31
+ spec.locus === undefined &&
32
+ !((voice.fade.in ?? 0) > 0) &&
33
+ !((voice.fade.out ?? 0) > 0);
34
+ if (!steady && voice.slots.some((s) => mix.channels[s]?.rest === undefined))
35
+ return 'it writes a channel with no rest at a weight that moves';
36
+ return undefined;
37
+ }
38
+ /**
39
+ * Throws unless a read back to mix time `t` is exact with no history: every voice plays as it was
40
+ * cued and reads the same whenever it is asked, and no mix on the transport has changed since `t`.
41
+ * A mix keeps only how it stands now, which is how it has stood since its last change.
42
+ */
43
+ export function stands(transport, t) {
44
+ const need = `blits: reading back to ${t} needs a mix made with history`;
45
+ if (transport.pace !== null)
46
+ throw new Error(`${need}: the mix's rate has been set`);
47
+ if (t < transport.settled)
48
+ throw new Error(`${need}: a mix changed or a voice left at ${transport.settled}, and without history a mix keeps only how it stands now`);
49
+ for (const mix of transport.members)
50
+ for (const voice of mix.cued) {
51
+ if (voice.state === 'done')
52
+ continue;
53
+ const why = unfit(mix, voice);
54
+ if (why !== undefined)
55
+ throw new Error(`${need}: voice ${voice.id} is cued, and ${why}`);
56
+ }
57
+ }
58
+ /** A mix's copy for a read back to `t` with no history: its voices as they stand, each at `t`. */
59
+ export function copyStanding(mix, c, t, cut) {
60
+ mirror(mix, c);
61
+ c.backward = true;
62
+ c.cued = mix.cued
63
+ .filter((v) => v.state !== 'done')
64
+ .map((v) => {
65
+ const copy = v.copy((subject) => recall(mix, v, subject, t, cut));
66
+ // A fade its own end began after `t` had not begun then.
67
+ if (copy.out !== null && copy.out.at > t)
68
+ copy.out = null;
69
+ const at = mix.pins?.get(v);
70
+ if (at !== undefined)
71
+ pin(c, copy, at);
72
+ return copy;
73
+ });
74
+ ownBlends(c.cued);
75
+ if (mix.owners !== null)
76
+ relink(c.cued);
77
+ stateAt(c, t);
78
+ recount(c);
79
+ }
package/dist/store.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Per-subject storage keyed by identity. Objects go in a WeakMap and are forgotten when the host
3
3
  * drops them; anything else — a string id, the unit subject of a mix with no subject dimension —
4
- * goes in a Map that only `drop` and `clear` empty, which is why a host keying by value should
5
- * call `drop`. Each map is made when first written, since a mix holds a store per voice and patch;
6
- * until then it is a shared empty one, so the field always holds a map.
4
+ * goes in a Map that only `delete` and `clear` empty, which is why a host keying by value should
5
+ * call `Mix.drop`. Each map is made when first written, since a mix holds a store per voice and
6
+ * patch; until then it is a shared empty one, so the field always holds a map.
7
7
  */
8
8
  export declare class Store<K, V> {
9
9
  private weak;
package/dist/store.js CHANGED
@@ -4,9 +4,9 @@ const noStrong = new Map();
4
4
  /**
5
5
  * Per-subject storage keyed by identity. Objects go in a WeakMap and are forgotten when the host
6
6
  * drops them; anything else — a string id, the unit subject of a mix with no subject dimension —
7
- * goes in a Map that only `drop` and `clear` empty, which is why a host keying by value should
8
- * call `drop`. Each map is made when first written, since a mix holds a store per voice and patch;
9
- * until then it is a shared empty one, so the field always holds a map.
7
+ * goes in a Map that only `delete` and `clear` empty, which is why a host keying by value should
8
+ * call `Mix.drop`. Each map is made when first written, since a mix holds a store per voice and
9
+ * patch; until then it is a shared empty one, so the field always holds a map.
10
10
  */
11
11
  export class Store {
12
12
  weak = noWeak;
package/dist/strays.js CHANGED
@@ -42,6 +42,9 @@ export function motionOwner(mix) {
42
42
  now() {
43
43
  return mix.deref()?.now ?? Number.NaN;
44
44
  },
45
+ seq() {
46
+ return mix.deref()?.transport.seq ?? Number.NaN;
47
+ },
45
48
  changed(id, motion, subject, change) {
46
49
  const m = mix.deref();
47
50
  if (m === undefined)
package/dist/tape.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import type { Pace } from './pace.js';
2
2
  import type { Transport } from './transport.js';
3
3
  import type { Tape } from './types.js';
4
- /** The tape `history.tape` makes for a transport, stamping each call with the mix time it was made at. */
4
+ /**
5
+ * The tape `history.tape` makes for a transport, stamping each call with the host time it was made
6
+ * at: rate 0 holds mix time still across frames, and host time tells them apart.
7
+ */
5
8
  export declare function tapeOf(transport: Transport): Tape | undefined;
6
9
  /** Records a host call, which `again` makes once more when the mix plays past it after a seek. */
7
10
  export declare function record(on: {
@@ -14,8 +17,8 @@ export declare function hostAt(clock: {
14
17
  readonly pace: Pace | null;
15
18
  }, t: number): number;
16
19
  /**
17
- * Makes the recorded calls due by mix time `to()` again, each with every mix on the transport
18
- * moved to the time it was made at, as the host made it then. `to` is asked again after every
19
- * call, since a call may change the rate and so where a host time lands.
20
+ * Makes the recorded calls `due` by the host time each was made at again, each host time's in a
21
+ * frame of its own with every mix on the transport moved to where it stood then, as the host made
22
+ * them. Answers the host time of the last frame it played, NaN for none.
20
23
  */
21
- export declare function replay(transport: Transport, to: () => number): void;
24
+ export declare function replay(transport: Transport, due: (u: number) => boolean): number;
package/dist/tape.js CHANGED
@@ -1,13 +1,16 @@
1
1
  import { move } from './move.js';
2
2
  /** What a recorded call's `invert` gives: a mix goes back by restoring, never by undoing a call. */
3
3
  const undone = { apply() { }, invert: () => undone };
4
- /** The tape `history.tape` makes for a transport, stamping each call with the mix time it was made at. */
4
+ /**
5
+ * The tape `history.tape` makes for a transport, stamping each call with the host time it was made
6
+ * at: rate 0 holds mix time still across frames, and host time tells them apart.
7
+ */
5
8
  export function tapeOf(transport) {
6
9
  const make = transport.history?.tape;
7
10
  if (make === undefined)
8
11
  return undefined;
9
12
  return make(transport, {
10
- now: () => (Number.isNaN(transport.now) ? Number.NEGATIVE_INFINITY : transport.now),
13
+ now: () => (Number.isNaN(transport.u) ? Number.NEGATIVE_INFINITY : transport.u),
11
14
  branching: true,
12
15
  coalesceWindowMs: 0,
13
16
  });
@@ -31,24 +34,29 @@ export function hostAt(clock, t) {
31
34
  return clock.pace === null ? t : clock.pace.timeOf(t);
32
35
  }
33
36
  /**
34
- * Makes the recorded calls due by mix time `to()` again, each with every mix on the transport
35
- * moved to the time it was made at, as the host made it then. `to` is asked again after every
36
- * call, since a call may change the rate and so where a host time lands.
37
+ * Makes the recorded calls `due` by the host time each was made at again, each host time's in a
38
+ * frame of its own with every mix on the transport moved to where it stood then, as the host made
39
+ * them. Answers the host time of the last frame it played, NaN for none.
37
40
  */
38
- export function replay(transport, to) {
41
+ export function replay(transport, due) {
39
42
  const tape = transport.tape;
40
43
  if (tape === undefined)
41
- return;
44
+ return Number.NaN;
42
45
  const u = transport.u;
46
+ let last = Number.NaN;
43
47
  try {
44
48
  for (;;) {
45
49
  const at = tape.timestampAt(tape.undoDepth());
46
- if (at === undefined || !(at <= to()))
47
- return;
48
- if (at > transport.now) {
49
- transport.u = hostAt(transport, at);
50
- for (const m of transport.members)
51
- move(m, at);
50
+ if (at === undefined || !due(at))
51
+ return last;
52
+ if (at !== last) {
53
+ last = at;
54
+ transport.u = at;
55
+ const now = transport.pace === null ? at : transport.pace.reading(at);
56
+ if (now > transport.now)
57
+ for (const m of transport.members)
58
+ move(m, now);
59
+ transport.tick(transport.now);
52
60
  }
53
61
  transport.replaying = true;
54
62
  try {
@@ -0,0 +1,35 @@
1
+ import type { Kit, Patch, Setting } from './types.js';
2
+ /**
3
+ * A `Setting` for calling a patch's `at` or `step` outside a mix: `keep` holds per owner as a mix's
4
+ * does, and `send` keeps what is sent in `sent`. Any field may be given; the rest read a voice at
5
+ * full weight at the start of its first pass, 1/60 s after the last frame.
6
+ *
7
+ * @category patch
8
+ */
9
+ export declare function setting<S = void, H = unknown>(fields?: Partial<Omit<Setting<S, H>, 'keep' | 'send'>>): Setting<S, H> & {
10
+ sent: unknown[];
11
+ };
12
+ /** @category patch */
13
+ export interface CheckOptions<I, O, H = unknown> {
14
+ /** The subject to read the patch for. */
15
+ subject: I;
16
+ /** A kit the patch should play in: each channel it writes, of the kind it was written for. */
17
+ kit?: Kit<O>;
18
+ /** What the patch's `reads` find on `setting.host`. */
19
+ host?: H;
20
+ /** The interval `step` is given, as under `stepMs`. Default 1000 / 60. */
21
+ stepMs?: number;
22
+ /** How many steps to run before reading state. Default 30. */
23
+ steps?: number;
24
+ }
25
+ /**
26
+ * What a patch does that a mix cannot rely on, one sentence per problem; empty when none. It reads
27
+ * `at` across its phase, twice from fresh state, and checks that it writes only its `writes`, gives
28
+ * the same delta each time, and leaves the subject alone; that `state` makes a new value per call,
29
+ * that `step` lands the same from the same start, and that `clone` and `pack`/`unpack` give back an
30
+ * equal state; that every field it `reads` is on `host`; and, given a kit, that `cue` would
31
+ * take it. Meant for a library's own tests: `expect(checkPatch(glow, { subject })).toEqual([])`.
32
+ *
33
+ * @category patch
34
+ */
35
+ export declare function checkPatch<I, O, S = void, H = unknown>(patch: Patch<I, O, S, H>, opts: CheckOptions<I, O, H>): string[];
@@ -0,0 +1,116 @@
1
+ import { clone } from './clone.js';
2
+ import { fitsKit } from './cue.js';
3
+ import { same } from './history.js';
4
+ /**
5
+ * A `Setting` for calling a patch's `at` or `step` outside a mix: `keep` holds per owner as a mix's
6
+ * does, and `send` keeps what is sent in `sent`. Any field may be given; the rest read a voice at
7
+ * full weight at the start of its first pass, 1/60 s after the last frame.
8
+ *
9
+ * @category patch
10
+ */
11
+ export function setting(fields = {}) {
12
+ const kept = new Map();
13
+ const sent = [];
14
+ return {
15
+ timestamp: 0,
16
+ dt: 1000 / 60,
17
+ elapsed: 0,
18
+ pass: 0,
19
+ weight: 1,
20
+ state: undefined,
21
+ host: undefined,
22
+ ...fields,
23
+ keep(owner, init) {
24
+ if (!kept.has(owner))
25
+ kept.set(owner, init());
26
+ return kept.get(owner);
27
+ },
28
+ send(event) {
29
+ sent.push(event);
30
+ },
31
+ sent,
32
+ };
33
+ }
34
+ /**
35
+ * What a patch does that a mix cannot rely on, one sentence per problem; empty when none. It reads
36
+ * `at` across its phase, twice from fresh state, and checks that it writes only its `writes`, gives
37
+ * the same delta each time, and leaves the subject alone; that `state` makes a new value per call,
38
+ * that `step` lands the same from the same start, and that `clone` and `pack`/`unpack` give back an
39
+ * equal state; that every field it `reads` is on `host`; and, given a kit, that `cue` would
40
+ * take it. Meant for a library's own tests: `expect(checkPatch(glow, { subject })).toEqual([])`.
41
+ *
42
+ * @category patch
43
+ */
44
+ export function checkPatch(patch, opts) {
45
+ const problems = [];
46
+ const { subject, host } = opts;
47
+ const tick = opts.stepMs ?? 1000 / 60;
48
+ const steps = opts.steps ?? 30;
49
+ const writes = new Set(patch.writes);
50
+ if (opts.kit !== undefined)
51
+ try {
52
+ fitsKit(patch, opts.kit);
53
+ }
54
+ catch (e) {
55
+ problems.push(e.message);
56
+ }
57
+ for (const field of patch.reads ?? [])
58
+ if (typeof host !== 'object' || host === null || !(field in host))
59
+ problems.push(`reads host.${field}, which the host given lacks`);
60
+ const fresh = () => (patch.state ? patch.state(subject) : undefined);
61
+ if (patch.state) {
62
+ const a = fresh();
63
+ if (typeof a === 'object' && a !== null && a === fresh())
64
+ problems.push('state() hands every subject the same object, so they share state');
65
+ }
66
+ /** The state after `steps` steps from fresh. */
67
+ const stepped = () => {
68
+ const state = fresh();
69
+ if (patch.step)
70
+ for (let n = 1; n <= steps; n++)
71
+ patch.step(state, tick, subject, setting({ state, host, dt: tick, timestamp: n * tick }));
72
+ return state;
73
+ };
74
+ const before = clone(subject);
75
+ const phases = [0, 0.25, 0.5, 0.75, 1];
76
+ const read = (state) => phases.map((phase) => clone(patch.at(phase, subject, setting({ state, host, elapsed: phase * patch.duration }))));
77
+ let first = [];
78
+ try {
79
+ first = read(stepped());
80
+ const second = read(stepped());
81
+ const stray = new Set();
82
+ for (const delta of first)
83
+ for (const key of Object.keys(delta))
84
+ if (!writes.has(key))
85
+ stray.add(key);
86
+ for (const key of stray)
87
+ problems.push(`at() writes ${key}, which is not in writes`);
88
+ if (!same(first, second))
89
+ problems.push('at() gives a different delta for the same phase, subject and state');
90
+ }
91
+ catch (e) {
92
+ problems.push(`at() or step() threw: ${e.message}`);
93
+ }
94
+ if (!same(subject, before))
95
+ problems.push('at() or step() changed the subject');
96
+ if (patch.step || patch.state) {
97
+ const state = stepped();
98
+ if (!same(state, stepped()))
99
+ problems.push('step() lands somewhere else from the same start and steps');
100
+ if (patch.clone) {
101
+ const c = patch.clone(state);
102
+ if (!same(c, state))
103
+ problems.push('clone() gives a state unequal to the one it was given');
104
+ else if (typeof state === 'object' && state !== null && c === state)
105
+ problems.push('clone() hands back the state itself, not a copy');
106
+ }
107
+ if ((patch.pack === undefined) !== (patch.unpack === undefined))
108
+ problems.push('pack and unpack come as a pair');
109
+ else if (patch.pack && patch.unpack) {
110
+ const back = patch.unpack(JSON.parse(JSON.stringify(patch.pack(state))));
111
+ if (!same(back, state))
112
+ problems.push('unpack(pack(state)) through JSON gives another state');
113
+ }
114
+ }
115
+ return problems;
116
+ }
@@ -0,0 +1,6 @@
1
+ import type { Mixer } from './mixer.js';
2
+ /**
3
+ * What `Mix.touch` does: makes stale every delta read this frame, for one subject or for all, so
4
+ * the next probe calls each patch again, on either path.
5
+ */
6
+ export declare function touch<I, O>(mix: Mixer<I, O>, subject?: I): void;
package/dist/touch.js ADDED
@@ -0,0 +1,28 @@
1
+ import { restamp } from './everyone.js';
2
+ import { nextFrame } from './move.js';
3
+ /**
4
+ * What `Mix.touch` does: makes stale every delta read this frame, for one subject or for all, so
5
+ * the next probe calls each patch again, on either path.
6
+ */
7
+ export function touch(mix, subject) {
8
+ mix.frame = nextFrame();
9
+ if (subject === undefined) {
10
+ // A motion patch reads nothing from outside the mix, and an owner has no delta.
11
+ for (const voice of mix.cued)
12
+ if (voice.motion === undefined && voice.holding === null) {
13
+ voice.seeks++;
14
+ voice.touches++;
15
+ }
16
+ }
17
+ else {
18
+ // 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
+ for (; held !== null; held = held.next)
21
+ held.seeks = -1;
22
+ const slot = mix.chains.get(subject)?.slot ?? -1;
23
+ for (const voice of mix.sharers)
24
+ restamp(voice, slot);
25
+ }
26
+ mix.lanes?.refill();
27
+ mix.stir();
28
+ }
@@ -1,3 +1,4 @@
1
+ import { Frames } from './frames.js';
1
2
  import type { Mixer } from './mixer.js';
2
3
  import { Pace } from './pace.js';
3
4
  import { Pager } from './paging.js';
@@ -5,22 +6,39 @@ import { type ScoreHolders } from './scored.js';
5
6
  import type { Marked, Mix, MixOptions, Tape, Transport as TransportApi, TransportOptions, TransportProjection } from './types.js';
6
7
  /**
7
8
  * A mark the host announced on a score: `at` in host time, so the rate never moves one, and NaN
8
- * until the next sync for one announced as now before any sync; `made` is the mix time it was
9
- * announced, for a read back to know what was known.
9
+ * until the next sync for one announced as now before any sync.
10
10
  */
11
11
  export interface Announced {
12
12
  name: string;
13
13
  score: string | undefined;
14
14
  tags: readonly string[];
15
15
  at: number;
16
- made: number;
17
16
  order: number;
17
+ /** The frame it was announced in, for a read back to know what was known. */
18
+ seq: number;
18
19
  /** The slot of the mix that announced it, -1 for the transport; only it sees an unnamed score. */
19
20
  slot: number;
20
21
  /** That mix's name, for `Marked.mix`. */
21
22
  mix: string | undefined;
22
23
  }
23
24
  type Member = Mixer<unknown, unknown>;
25
+ /**
26
+ * One frame on the transport: its number, the mix time it stood at and the host time it was made
27
+ * at. Rate 0 holds mix time still across frames, so only the number tells two of them apart.
28
+ */
29
+ export interface Frame {
30
+ seq: number;
31
+ at: number;
32
+ u: number;
33
+ }
34
+ /** Where history is cut for a seek or read: what frame `seq` made, and before it, is kept. */
35
+ export interface Cut {
36
+ seq: number;
37
+ /** For a read of what a frame showed: of that frame's own changes, only those made before it was read. */
38
+ strict: boolean;
39
+ }
40
+ /** Whether a change made in frame `seq` falls inside `cut`; `early` for one made before the frame was read. */
41
+ export declare function within(cut: Cut, seq: number, early?: boolean): boolean;
24
42
  /**
25
43
  * The clock its mixes play on: host time less what `rebase` took out, at the rate its `pace`
26
44
  * sets, moved by `sync` and `seek`, and the tape of calls a seek plays again. A mix made alone
@@ -35,6 +53,7 @@ export declare class Transport implements TransportApi {
35
53
  dropped: {
36
54
  mix: Member;
37
55
  at: number;
56
+ seq: number;
38
57
  }[];
39
58
  /** Numbers the mixes as they join, for their order and for whose unnamed score a mark is on. */
40
59
  slots: number;
@@ -44,6 +63,13 @@ export declare class Transport implements TransportApi {
44
63
  private woken;
45
64
  /** The mix clock at the last sync or seek. */
46
65
  now: number;
66
+ /**
67
+ * The number of the frame being played: each sync and seek begins one, and so does each host
68
+ * time the tape plays calls again at.
69
+ */
70
+ seq: number;
71
+ /** Under history, the frames a seek or read back may reach, oldest first. */
72
+ frames: Frames;
47
73
  /** Host time at the last sync: the host's timestamp less every gap `rebase` took out. */
48
74
  u: number;
49
75
  offset: number;
@@ -57,6 +83,17 @@ export declare class Transport implements TransportApi {
57
83
  taped: number;
58
84
  /** The rate; null while it has never been set, when mix time is host time. */
59
85
  pace: Pace | null;
86
+ /** The latest mix time a voice any mix on it let go of from `gone` had left at. */
87
+ forgotTo: number;
88
+ /**
89
+ * The latest mix time a host changed a mix on it, or a voice or a subject left one: without
90
+ * history, the earliest time a read back still finds the mixes as they stand now.
91
+ */
92
+ settled: number;
93
+ /** Records a change or a leaving at the mix time now; before the first sync, nothing. */
94
+ settle(): void;
95
+ /** The frame the rate was first set in, which a seek or read back to before it finds unset. */
96
+ pacedSeq: number;
60
97
  /** True while the tape makes a recorded call again, so it is not recorded twice. */
61
98
  replaying: boolean;
62
99
  announced: Announced[];
@@ -72,11 +109,11 @@ export declare class Transport implements TransportApi {
72
109
  constructor(history: MixOptions['history'], shared: boolean);
73
110
  /** Whether this is a projection's own transport, which records and seeks nothing. */
74
111
  get projecting(): boolean;
75
- /** Takes a mix on, after every mix that joined before it; `slot` keeps a copy's in place. */
112
+ /** Takes a mix on, after every mix that joined before it; `slot` keeps one a seek puts back in place. */
76
113
  join(mix: Member, slot?: number): void;
77
114
  drop<I, O, H>(mix: Mix<I, O, H>): void;
78
- /** A seek back to `t`: every mix dropped after it is back on, as it stood then. */
79
- undrop(t: number): void;
115
+ /** A seek back to frame `seq`: every mix dropped after it is back on, as it stood then. */
116
+ undrop(seq: number): void;
80
117
  seek(time: number): void;
81
118
  prepare(time: number): Promise<void>;
82
119
  announce(name: string, opts: {
@@ -93,6 +130,8 @@ export declare class Transport implements TransportApi {
93
130
  woke(): void;
94
131
  sync(timestamp: number): void;
95
132
  private syncAt;
133
+ /** Begins a frame at mix time `at` and the host time the transport stands at. */
134
+ tick(at: number): void;
96
135
  rebase(): void;
97
136
  /**
98
137
  * Moves `floor` past what the mix clock standing at `now` has let go of. With a store nothing is