@msb235/blits 0.10.0 → 0.12.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.
- package/CHANGELOG.md +248 -0
- package/README.md +15 -4
- package/dist/arcs.d.ts +40 -0
- package/dist/arcs.js +239 -0
- package/dist/blend.js +1 -5
- package/dist/book.d.ts +2 -0
- package/dist/book.js +27 -3
- package/dist/chain.d.ts +17 -1
- package/dist/chain.js +57 -15
- package/dist/channels.d.ts +2 -0
- package/dist/channels.js +12 -1
- package/dist/clock.d.ts +14 -0
- package/dist/clock.js +9 -1
- package/dist/clone.js +16 -4
- package/dist/columns.d.ts +4 -5
- package/dist/columns.js +11 -5
- package/dist/crowd.d.ts +1 -1
- package/dist/crowd.js +4 -1
- package/dist/cue.js +63 -7
- package/dist/departed.d.ts +8 -3
- package/dist/departed.js +76 -29
- package/dist/due.d.ts +4 -0
- package/dist/due.js +16 -3
- package/dist/easing.d.ts +12 -1
- package/dist/easing.js +7 -1
- package/dist/everyone.d.ts +14 -1
- package/dist/everyone.js +27 -2
- package/dist/fade.d.ts +12 -5
- package/dist/fade.js +57 -19
- package/dist/fill.js +16 -9
- package/dist/fit.d.ts +8 -1
- package/dist/fit.js +7 -3
- package/dist/fold.d.ts +1 -1
- package/dist/fold.js +112 -9
- package/dist/frames.d.ts +2 -0
- package/dist/frames.js +17 -0
- package/dist/gather.js +38 -7
- package/dist/handle.d.ts +7 -2
- package/dist/handle.js +28 -12
- package/dist/held.d.ts +13 -2
- package/dist/held.js +43 -9
- package/dist/history.d.ts +25 -2
- package/dist/history.js +163 -35
- package/dist/hosts.js +45 -17
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/keyfill.js +3 -1
- package/dist/lane.d.ts +1 -1
- package/dist/lane.js +3 -1
- package/dist/lanes.d.ts +31 -7
- package/dist/lanes.js +57 -6
- package/dist/lean.d.ts +15 -0
- package/dist/lean.js +23 -0
- package/dist/leavings.d.ts +6 -1
- package/dist/leavings.js +16 -2
- package/dist/marks.d.ts +7 -3
- package/dist/marks.js +105 -27
- package/dist/meet.js +1 -0
- package/dist/mixer.d.ts +35 -6
- package/dist/mixer.js +113 -45
- package/dist/motion.d.ts +4 -1
- package/dist/motion.js +16 -2
- package/dist/motions.d.ts +4 -0
- package/dist/motions.js +45 -11
- package/dist/move.js +35 -12
- package/dist/numbers.d.ts +5 -1
- package/dist/numbers.js +4 -0
- package/dist/owner.d.ts +18 -2
- package/dist/owner.js +44 -3
- package/dist/pack.d.ts +1 -0
- package/dist/pack.js +3 -0
- package/dist/place.d.ts +8 -0
- package/dist/place.js +16 -8
- package/dist/placed.d.ts +19 -0
- package/dist/placed.js +54 -0
- package/dist/project.d.ts +5 -2
- package/dist/project.js +238 -34
- package/dist/pull.js +37 -8
- package/dist/qualify.d.ts +10 -0
- package/dist/qualify.js +60 -3
- package/dist/reading.d.ts +8 -1
- package/dist/reading.js +8 -1
- package/dist/record.d.ts +6 -0
- package/dist/record.js +2 -0
- package/dist/restored.d.ts +69 -17
- package/dist/restored.js +163 -50
- package/dist/revive.d.ts +6 -3
- package/dist/revive.js +11 -3
- package/dist/roster.js +2 -1
- package/dist/rows.js +15 -5
- package/dist/seek.d.ts +7 -2
- package/dist/seek.js +152 -43
- package/dist/shared.d.ts +5 -0
- package/dist/shared.js +5 -0
- package/dist/signals.d.ts +10 -0
- package/dist/signals.js +17 -0
- package/dist/spans.d.ts +5 -0
- package/dist/spans.js +75 -5
- package/dist/store.d.ts +13 -0
- package/dist/store.js +23 -0
- package/dist/strays.js +50 -2
- package/dist/tape.d.ts +29 -4
- package/dist/tape.js +49 -11
- package/dist/touch.js +3 -2
- package/dist/transport.d.ts +11 -2
- package/dist/transport.js +26 -10
- package/dist/types/arc.d.ts +55 -0
- package/dist/types/arc.js +1 -0
- package/dist/types/mix.d.ts +57 -14
- package/dist/types/patch.d.ts +5 -1
- package/dist/types/place.d.ts +11 -4
- package/dist/types/transport.d.ts +12 -3
- package/dist/types/voice.d.ts +72 -5
- package/dist/types/watch.d.ts +126 -0
- package/dist/types/watch.js +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/unpage.d.ts +6 -1
- package/dist/unpage.js +34 -2
- package/dist/voice.d.ts +93 -16
- package/dist/voice.js +60 -3
- package/dist/watch.d.ts +26 -0
- package/dist/watch.js +441 -0
- package/dist/weigh.d.ts +13 -2
- package/dist/weigh.js +10 -3
- package/docs/schema.html +106 -38
- package/package.json +10 -4
package/dist/voice.js
CHANGED
|
@@ -6,6 +6,7 @@ import { childPlayed, Holding, mixTime, ownedElapsed, ownerPatch, ownerReading,
|
|
|
6
6
|
import { builtOf, intosOf } from './patch.js';
|
|
7
7
|
import { reading } from './reading.js';
|
|
8
8
|
import { Store } from './store.js';
|
|
9
|
+
import { warnOnce } from './warn.js';
|
|
9
10
|
export const none = Object.freeze([]);
|
|
10
11
|
/**
|
|
11
12
|
* A voice's `setting.keep`, one per voice: a setting is valid only during its call, so the state
|
|
@@ -62,19 +63,26 @@ class VoiceSetting {
|
|
|
62
63
|
/** A store that fills a missing entry from `make` on first ask. */
|
|
63
64
|
class Filled extends Store {
|
|
64
65
|
make;
|
|
66
|
+
/** Keys deleted here, which are made afresh by whoever asks and never again by `make`. */
|
|
67
|
+
gone = null;
|
|
65
68
|
constructor(make) {
|
|
66
69
|
super();
|
|
67
70
|
this.make = make;
|
|
68
71
|
}
|
|
69
72
|
get(key) {
|
|
70
73
|
let v = super.get(key);
|
|
71
|
-
if (v === undefined) {
|
|
74
|
+
if (v === undefined && this.gone?.get(key) !== true) {
|
|
72
75
|
v = this.make(key);
|
|
73
76
|
if (v !== undefined)
|
|
74
77
|
super.set(key, v);
|
|
75
78
|
}
|
|
76
79
|
return v;
|
|
77
80
|
}
|
|
81
|
+
delete(key) {
|
|
82
|
+
super.delete(key);
|
|
83
|
+
this.gone ??= new Store();
|
|
84
|
+
this.gone.set(key, true);
|
|
85
|
+
}
|
|
78
86
|
}
|
|
79
87
|
const noSend = () => { };
|
|
80
88
|
export class Voice {
|
|
@@ -135,11 +143,16 @@ export class Voice {
|
|
|
135
143
|
rebuilds = 0;
|
|
136
144
|
/** Whether its span's last fit jumped it to its end, which leaves it no time in that fit. */
|
|
137
145
|
skipped = false;
|
|
146
|
+
/** What its `earliest` answered at its span's last fit, span ms; NaN while unknown or absent. */
|
|
147
|
+
floorSeen = Number.NaN;
|
|
138
148
|
/** Whether its patch has kept state on a record through `setting.keep`, which makes it stateful. */
|
|
139
149
|
keeping = false;
|
|
140
150
|
/** Subjects fading out of this voice alone, by the ramp each started; null while none are. */
|
|
141
151
|
parts = null;
|
|
142
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* Subjects faded out of this voice, by the mix time and frame each left at, and the ramp that
|
|
154
|
+
* took it out, which a seek or a read back into it puts back; null while none have.
|
|
155
|
+
*/
|
|
143
156
|
parted = null;
|
|
144
157
|
/**
|
|
145
158
|
* Under history, the records of subjects that left this voice, by the mix time and frame each
|
|
@@ -165,6 +178,8 @@ export class Voice {
|
|
|
165
178
|
freezesAfter;
|
|
166
179
|
/** The mix time it first showed: when it was cued, or the first sync after. */
|
|
167
180
|
opened = Number.NaN;
|
|
181
|
+
/** Whether no delta its patch's `at` returned has been checked against `writes` yet. */
|
|
182
|
+
unchecked = true;
|
|
168
183
|
/** Which of its entries in the mix's due queue is current; older ones are skipped when popped. */
|
|
169
184
|
dueToken = 0;
|
|
170
185
|
/**
|
|
@@ -183,6 +198,8 @@ export class Voice {
|
|
|
183
198
|
/** The least stagger of any subject seen, 0 or below, which a voice running back ends at. */
|
|
184
199
|
soonest = 0;
|
|
185
200
|
restedCount = 0;
|
|
201
|
+
/** Whether a seek back to before its cue has it out of the mix, until the mix plays the cue again. */
|
|
202
|
+
parked = false;
|
|
186
203
|
/** The mix time it was cued at; -Infinity before the first sync. */
|
|
187
204
|
cuedAt = Number.NEGATIVE_INFINITY;
|
|
188
205
|
/** The frame it was cued in. */
|
|
@@ -221,12 +238,19 @@ export class Voice {
|
|
|
221
238
|
ownKept = null;
|
|
222
239
|
/** The blend it is a member of, and which. */
|
|
223
240
|
blend = null;
|
|
241
|
+
/**
|
|
242
|
+
* Copies the latest seek back cut from its records' lists that no other list holds, for the
|
|
243
|
+
* copies its probes keep next to be written over; null until a seek cuts one.
|
|
244
|
+
*/
|
|
245
|
+
spares = null;
|
|
224
246
|
/** The one record of every subject it does not reach. */
|
|
225
247
|
unreached = null;
|
|
226
248
|
/** By subject number, a bit set where `unreached` stands for the subject; null until one is. */
|
|
227
249
|
unreachedBits = null;
|
|
228
250
|
/** Whether one record stands for every subject: see `shares` in `everyone.ts`. */
|
|
229
251
|
sharing = false;
|
|
252
|
+
/** The frames it has played sharing it. */
|
|
253
|
+
shared = 0;
|
|
230
254
|
/** That record, null until a chain first asks and once the voice shares no more. */
|
|
231
255
|
everyone = null;
|
|
232
256
|
/** While sharing, by subject number: a bit where a chain has asked about the subject. */
|
|
@@ -247,6 +271,12 @@ export class Voice {
|
|
|
247
271
|
holding;
|
|
248
272
|
/** For a span, its budget and how its children were last fitted; null for any other voice. */
|
|
249
273
|
fitting = null;
|
|
274
|
+
/** For an arc, how its parts are laid out; null for any other voice. */
|
|
275
|
+
arc = null;
|
|
276
|
+
/** For an arc, how long its hold lasts, ms on its clock: Infinity while open. */
|
|
277
|
+
hold = 0;
|
|
278
|
+
/** For a voice an arc holds, which part it plays: 0 enter, 1 active, 2 exit; -1 for any other. */
|
|
279
|
+
part = -1;
|
|
250
280
|
/**
|
|
251
281
|
* `done` and `played`, made when first asked for, already settled if the voice is: most hosts
|
|
252
282
|
* never await either, and a mix may hold tens of thousands of voices.
|
|
@@ -350,6 +380,7 @@ export class Voice {
|
|
|
350
380
|
this.outOver = c.outOver;
|
|
351
381
|
this.outSet = c.outSet;
|
|
352
382
|
this.rebuilds = c.rebuilds;
|
|
383
|
+
this.hold = c.hold;
|
|
353
384
|
const clocks = this.clocks;
|
|
354
385
|
if (clocks !== null && clocks.length !== c.past)
|
|
355
386
|
this.clocks = c.past === 0 ? null : clocks.slice(0, c.past);
|
|
@@ -362,6 +393,7 @@ export class Voice {
|
|
|
362
393
|
park() {
|
|
363
394
|
this.reopen(Number.NEGATIVE_INFINITY);
|
|
364
395
|
this.take(this.first);
|
|
396
|
+
this.parked = true;
|
|
365
397
|
this.log = null;
|
|
366
398
|
this.state = 'pending';
|
|
367
399
|
this.subjects = new Store();
|
|
@@ -529,9 +561,20 @@ export class Voice {
|
|
|
529
561
|
get holdsFirst() {
|
|
530
562
|
return this.freezesBefore || this.out?.back === true;
|
|
531
563
|
}
|
|
564
|
+
/**
|
|
565
|
+
* Whether a subject past the end of its passes shows nothing: its clock runs back, so it has yet
|
|
566
|
+
* to reach them, and it does not freeze after.
|
|
567
|
+
*/
|
|
568
|
+
get hidesPast() {
|
|
569
|
+
return !this.freezesAfter && this.backs !== (this.owner !== null && turnedBack(this));
|
|
570
|
+
}
|
|
532
571
|
/** A subject's voice time at `raw` once the voice's freezes apply; NaN where it shows nothing. */
|
|
533
572
|
shownAt(raw) {
|
|
534
|
-
|
|
573
|
+
if (this.beginless)
|
|
574
|
+
return raw;
|
|
575
|
+
if (raw > this.span && this.hidesPast)
|
|
576
|
+
return Number.NaN;
|
|
577
|
+
return frozenTime(raw, this.holdsFirst, this.freezesAfter, this.span);
|
|
535
578
|
}
|
|
536
579
|
/** Records this voice's controls as they stand, from mix time `at` in frame `seq`. */
|
|
537
580
|
note(at, seq, sync = false) {
|
|
@@ -551,6 +594,7 @@ export class Voice {
|
|
|
551
594
|
outOver: this.outOver,
|
|
552
595
|
outSet: this.outSet,
|
|
553
596
|
rebuilds: this.rebuilds,
|
|
597
|
+
hold: this.hold,
|
|
554
598
|
past: this.clocks?.length ?? 0,
|
|
555
599
|
});
|
|
556
600
|
}
|
|
@@ -600,3 +644,16 @@ export class Voice {
|
|
|
600
644
|
rebaseWith(this, now);
|
|
601
645
|
}
|
|
602
646
|
}
|
|
647
|
+
/**
|
|
648
|
+
* Warns, once a patch outside production, of a key the first delta `voice` read returns outside its
|
|
649
|
+
* patch's `writes`, which the mix drops.
|
|
650
|
+
*/
|
|
651
|
+
export function checkWrites(voice, delta) {
|
|
652
|
+
voice.unchecked = false;
|
|
653
|
+
if (delta === null || typeof delta !== 'object')
|
|
654
|
+
return;
|
|
655
|
+
const writes = voice.patch.writes;
|
|
656
|
+
for (const key in delta)
|
|
657
|
+
if (!writes.includes(key))
|
|
658
|
+
warnOnce(voice.patch, `${voice.spec.name ?? `voice ${voice.id}`}'s patch returns ${key} from at(), which is not in its writes, so the mix drops it`);
|
|
659
|
+
}
|
package/dist/watch.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Mixer } from './mixer.js';
|
|
2
|
+
import type { MixOptions, Registry } from './types.js';
|
|
3
|
+
type Any = Mixer<unknown, unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* Every mix made on the page, shared by every copy of blits loaded: what a debugger reads to find
|
|
6
|
+
* them. See `Registry`.
|
|
7
|
+
*
|
|
8
|
+
* @category mix
|
|
9
|
+
*/
|
|
10
|
+
export declare function watched(): Registry;
|
|
11
|
+
/**
|
|
12
|
+
* The history a mix or transport made with `opts` keeps: its own, or with none named and no
|
|
13
|
+
* transport, the registry's default. Reads the registry only where one was made.
|
|
14
|
+
*/
|
|
15
|
+
export declare function historyFor(opts: {
|
|
16
|
+
history?: MixOptions['history'];
|
|
17
|
+
transport?: unknown;
|
|
18
|
+
}): {
|
|
19
|
+
history: MixOptions['history'];
|
|
20
|
+
added: boolean;
|
|
21
|
+
};
|
|
22
|
+
/** Marks a transport whose history the registry's default gave. */
|
|
23
|
+
export declare function gave(transport: object): void;
|
|
24
|
+
/** Lists a mix the host made, under an entry this copy builds. */
|
|
25
|
+
export declare function register(mix: Any): void;
|
|
26
|
+
export {};
|
package/dist/watch.js
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { seek } from './seek.js';
|
|
2
|
+
import { release } from './shared.js';
|
|
3
|
+
import { hostAhead } from './tape.js';
|
|
4
|
+
const KEY = Symbol.for('@msb235/blits/watch/1');
|
|
5
|
+
const slot = globalThis;
|
|
6
|
+
/**
|
|
7
|
+
* Every mix made on the page, shared by every copy of blits loaded: what a debugger reads to find
|
|
8
|
+
* them. See `Registry`.
|
|
9
|
+
*
|
|
10
|
+
* @category mix
|
|
11
|
+
*/
|
|
12
|
+
export function watched() {
|
|
13
|
+
return registry();
|
|
14
|
+
}
|
|
15
|
+
function registry() {
|
|
16
|
+
let r = slot[KEY];
|
|
17
|
+
if (r !== undefined)
|
|
18
|
+
return r;
|
|
19
|
+
const refs = new Map();
|
|
20
|
+
const fns = new Set();
|
|
21
|
+
const entries_ = new WeakMap();
|
|
22
|
+
r = {
|
|
23
|
+
refs,
|
|
24
|
+
entries_,
|
|
25
|
+
fns,
|
|
26
|
+
labels: new WeakMap(),
|
|
27
|
+
next: 1,
|
|
28
|
+
counted: 0,
|
|
29
|
+
history: undefined,
|
|
30
|
+
gone: new FinalizationRegistry((id) => {
|
|
31
|
+
refs.delete(id);
|
|
32
|
+
for (const fn of [...fns])
|
|
33
|
+
fn({ kind: 'gone', id });
|
|
34
|
+
}),
|
|
35
|
+
entries() {
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const ref of refs.values()) {
|
|
38
|
+
const mix = ref.deref();
|
|
39
|
+
const entry = mix === undefined ? undefined : entries_.get(mix);
|
|
40
|
+
if (entry !== undefined)
|
|
41
|
+
out.push(entry);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
},
|
|
45
|
+
on(fn) {
|
|
46
|
+
fns.add(fn);
|
|
47
|
+
return () => {
|
|
48
|
+
fns.delete(fn);
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
slot[KEY] = r;
|
|
53
|
+
return r;
|
|
54
|
+
}
|
|
55
|
+
/** The transports whose history the registry's default gave. */
|
|
56
|
+
const added = new WeakSet();
|
|
57
|
+
/**
|
|
58
|
+
* The history a mix or transport made with `opts` keeps: its own, or with none named and no
|
|
59
|
+
* transport, the registry's default. Reads the registry only where one was made.
|
|
60
|
+
*/
|
|
61
|
+
export function historyFor(opts) {
|
|
62
|
+
const fallback = slot[KEY]?.history;
|
|
63
|
+
if (opts.history !== undefined || opts.transport !== undefined || fallback === undefined)
|
|
64
|
+
return { history: opts.history, added: false };
|
|
65
|
+
return { history: fallback, added: true };
|
|
66
|
+
}
|
|
67
|
+
/** Marks a transport whose history the registry's default gave. */
|
|
68
|
+
export function gave(transport) {
|
|
69
|
+
added.add(transport);
|
|
70
|
+
}
|
|
71
|
+
/** Lists a mix the host made, under an entry this copy builds. */
|
|
72
|
+
export function register(mix) {
|
|
73
|
+
const r = registry();
|
|
74
|
+
const id = r.next++;
|
|
75
|
+
const transport = mix.transport;
|
|
76
|
+
const entry = {
|
|
77
|
+
id,
|
|
78
|
+
release,
|
|
79
|
+
name: mix.name,
|
|
80
|
+
mix,
|
|
81
|
+
get history() {
|
|
82
|
+
const h = transport.history;
|
|
83
|
+
return h === undefined
|
|
84
|
+
? undefined
|
|
85
|
+
: { ms: h.ms, tape: transport.tape !== undefined, added: added.has(transport) };
|
|
86
|
+
},
|
|
87
|
+
get reach() {
|
|
88
|
+
return reachOf(transport, false);
|
|
89
|
+
},
|
|
90
|
+
channels: mix.names,
|
|
91
|
+
voice: (handle) => {
|
|
92
|
+
const v = mix.cued.find((c) => c.handle === handle) ?? mix.gone.find((c) => c.handle === handle);
|
|
93
|
+
return v === undefined ? undefined : { name: v.spec.name, tags: v.spec.tags ?? [] };
|
|
94
|
+
},
|
|
95
|
+
label: (subject) => labelOf(mix, subject),
|
|
96
|
+
watch: (fn) => tap(mix, fn),
|
|
97
|
+
subjects: () => subjectsOf(mix),
|
|
98
|
+
read: (subject, at = mix.now) => mix.project(at).probe(subject),
|
|
99
|
+
hold: () => hold(transport),
|
|
100
|
+
};
|
|
101
|
+
r.entries_.set(mix, entry);
|
|
102
|
+
r.refs.set(id, new WeakRef(mix));
|
|
103
|
+
r.gone.register(mix, id);
|
|
104
|
+
for (const fn of [...r.fns])
|
|
105
|
+
fn({ kind: 'add', entry });
|
|
106
|
+
}
|
|
107
|
+
function labelOf(mix, subject) {
|
|
108
|
+
const own = mix.opts.labelOf?.(subject);
|
|
109
|
+
return own ?? guess(subject);
|
|
110
|
+
}
|
|
111
|
+
/** A name for a subject from the subject itself, numbered by class where nothing names it. */
|
|
112
|
+
function guess(subject) {
|
|
113
|
+
if (typeof subject !== 'object' || subject === null)
|
|
114
|
+
return typeof subject === 'function' ? subject.name || 'function' : String(subject);
|
|
115
|
+
const Element = globalThis.Element;
|
|
116
|
+
if (Element !== undefined && subject instanceof Element) {
|
|
117
|
+
const id = subject.id ? `#${subject.id}` : '';
|
|
118
|
+
const classes = [...subject.classList].map((c) => `.${c}`).join('');
|
|
119
|
+
return `${subject.tagName.toLowerCase()}${id}${classes}`;
|
|
120
|
+
}
|
|
121
|
+
const o = subject;
|
|
122
|
+
for (const k of ['name', 'id']) {
|
|
123
|
+
const v = o[k];
|
|
124
|
+
if ((typeof v === 'string' && v !== '') || typeof v === 'number')
|
|
125
|
+
return String(v);
|
|
126
|
+
}
|
|
127
|
+
const r = registry();
|
|
128
|
+
let label = r.labels.get(subject);
|
|
129
|
+
if (label === undefined) {
|
|
130
|
+
label = `${subject.constructor?.name || 'Object'} #${++r.counted}`;
|
|
131
|
+
r.labels.set(subject, label);
|
|
132
|
+
}
|
|
133
|
+
return label;
|
|
134
|
+
}
|
|
135
|
+
function subjectsOf(mix) {
|
|
136
|
+
const t = taps.get(mix);
|
|
137
|
+
const out = new Set(t?.was);
|
|
138
|
+
for (const s of t?.seen ?? [])
|
|
139
|
+
out.add(s);
|
|
140
|
+
for (const v of mix.cued)
|
|
141
|
+
if (v.state !== 'done')
|
|
142
|
+
for (const s of v.spec.subjects ?? [])
|
|
143
|
+
out.add(s);
|
|
144
|
+
return [...out];
|
|
145
|
+
}
|
|
146
|
+
const ctls = new WeakMap();
|
|
147
|
+
function ctlOf(t) {
|
|
148
|
+
let c = ctls.get(t);
|
|
149
|
+
if (c === undefined) {
|
|
150
|
+
c = { tapped: 0, held: null, unsync: null };
|
|
151
|
+
ctls.set(t, c);
|
|
152
|
+
}
|
|
153
|
+
return c;
|
|
154
|
+
}
|
|
155
|
+
/** Puts `sync` on the transport while anything needs it, and takes it off when nothing does. */
|
|
156
|
+
function fit(t) {
|
|
157
|
+
const c = ctlOf(t);
|
|
158
|
+
const wants = c.tapped > 0 || c.held !== null;
|
|
159
|
+
if (wants && c.unsync === null) {
|
|
160
|
+
const sync = proto(t).sync;
|
|
161
|
+
c.unsync = swap(t, 'sync', (ts) => {
|
|
162
|
+
for (const m of t.members)
|
|
163
|
+
flush(m);
|
|
164
|
+
const held = c.held;
|
|
165
|
+
if (held === null)
|
|
166
|
+
sync.call(t, ts);
|
|
167
|
+
else
|
|
168
|
+
held.sync(ts);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
else if (!wants && c.unsync !== null) {
|
|
172
|
+
c.unsync();
|
|
173
|
+
c.unsync = null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Sets `key` on `on` itself, and returns what puts back the function it had. That stays an own
|
|
178
|
+
* property: deleting one added before another drops the mix or transport into dictionary mode
|
|
179
|
+
* for good, which slows every later call on it.
|
|
180
|
+
*/
|
|
181
|
+
function swap(on, key, fn) {
|
|
182
|
+
const o = on;
|
|
183
|
+
const was = o[key];
|
|
184
|
+
o[key] = fn;
|
|
185
|
+
return () => {
|
|
186
|
+
o[key] = was;
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function proto(o) {
|
|
190
|
+
return Object.getPrototypeOf(o);
|
|
191
|
+
}
|
|
192
|
+
const taps = new WeakMap();
|
|
193
|
+
function tap(mix, fn) {
|
|
194
|
+
let t = taps.get(mix);
|
|
195
|
+
if (t === undefined) {
|
|
196
|
+
const p = proto(mix);
|
|
197
|
+
const probe = p.probe;
|
|
198
|
+
const pull = p.pull;
|
|
199
|
+
const drain = p.drain;
|
|
200
|
+
const puts = [
|
|
201
|
+
swap(mix, 'probe', (subject, out) => {
|
|
202
|
+
const pose = probe.call(mix, subject, out);
|
|
203
|
+
const f = frameOf(mix);
|
|
204
|
+
f.probed.push({ subject, pose: copy(pose) });
|
|
205
|
+
taps.get(mix).seen.add(subject);
|
|
206
|
+
return pose;
|
|
207
|
+
}),
|
|
208
|
+
swap(mix, 'pull', (subjects, into) => {
|
|
209
|
+
const list = Array.isArray(subjects) ? subjects : [...subjects];
|
|
210
|
+
pull.call(mix, list, into);
|
|
211
|
+
const columns = {};
|
|
212
|
+
for (const [k, v] of Object.entries(into))
|
|
213
|
+
if (v !== undefined)
|
|
214
|
+
columns[k] = v.slice();
|
|
215
|
+
frameOf(mix).pulled.push({ subjects: [...list], columns });
|
|
216
|
+
const seen = taps.get(mix).seen;
|
|
217
|
+
for (const s of list)
|
|
218
|
+
seen.add(s);
|
|
219
|
+
}),
|
|
220
|
+
swap(mix, 'drain', (tag) => {
|
|
221
|
+
const out = drain.call(mix, tag);
|
|
222
|
+
if (out.length > 0)
|
|
223
|
+
frameOf(mix).sent.push(...out);
|
|
224
|
+
return out;
|
|
225
|
+
}),
|
|
226
|
+
];
|
|
227
|
+
t = {
|
|
228
|
+
fns: new Set(),
|
|
229
|
+
frame: null,
|
|
230
|
+
seen: new Set(),
|
|
231
|
+
was: new Set(),
|
|
232
|
+
untap: () => {
|
|
233
|
+
for (const put of puts)
|
|
234
|
+
put();
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
taps.set(mix, t);
|
|
238
|
+
ctlOf(mix.transport).tapped++;
|
|
239
|
+
fit(mix.transport);
|
|
240
|
+
}
|
|
241
|
+
t.fns.add(fn);
|
|
242
|
+
return stopper(new WeakRef(mix), fn);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* What stops one watcher, holding the mix weakly so a debugger that keeps it does not keep the
|
|
246
|
+
* mix alive. Made out here: a closure made inside `tap` would share the scope that holds the mix.
|
|
247
|
+
*/
|
|
248
|
+
function stopper(ref, fn) {
|
|
249
|
+
return () => {
|
|
250
|
+
const m = ref.deref();
|
|
251
|
+
const own = m === undefined ? undefined : taps.get(m);
|
|
252
|
+
if (m === undefined || own === undefined || !own.fns.delete(fn) || own.fns.size > 0)
|
|
253
|
+
return;
|
|
254
|
+
own.untap();
|
|
255
|
+
taps.delete(m);
|
|
256
|
+
ctlOf(m.transport).tapped--;
|
|
257
|
+
fit(m.transport);
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/** The frame a read lands in, handing the last one over where the mix has moved on since. */
|
|
261
|
+
function frameOf(mix) {
|
|
262
|
+
const t = taps.get(mix);
|
|
263
|
+
if (t.frame !== null && t.frame.seq === mix.transport.seq)
|
|
264
|
+
return t.frame;
|
|
265
|
+
flush(mix);
|
|
266
|
+
t.was = t.seen;
|
|
267
|
+
t.seen = new Set();
|
|
268
|
+
t.frame = {
|
|
269
|
+
seq: mix.transport.seq,
|
|
270
|
+
now: mix.now,
|
|
271
|
+
timestamp: mix.timestamp,
|
|
272
|
+
probed: [],
|
|
273
|
+
pulled: [],
|
|
274
|
+
sent: [],
|
|
275
|
+
voices: mix.voices(),
|
|
276
|
+
};
|
|
277
|
+
return t.frame;
|
|
278
|
+
}
|
|
279
|
+
function flush(mix) {
|
|
280
|
+
const t = taps.get(mix);
|
|
281
|
+
const frame = t?.frame;
|
|
282
|
+
if (t === undefined || frame == null)
|
|
283
|
+
return;
|
|
284
|
+
t.frame = null;
|
|
285
|
+
for (const fn of [...t.fns])
|
|
286
|
+
fn(frame);
|
|
287
|
+
}
|
|
288
|
+
function copy(pose) {
|
|
289
|
+
try {
|
|
290
|
+
return globalThis.structuredClone(pose);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return { ...pose };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/** The earliest mix time history reaches back to; a seek needs a tape besides. */
|
|
297
|
+
function reachOf(t, seeking) {
|
|
298
|
+
const h = t.history;
|
|
299
|
+
if (h === undefined || (seeking && t.tape === undefined))
|
|
300
|
+
return t.now;
|
|
301
|
+
const kept = t.pager !== null ? Number.NEGATIVE_INFINITY : t.now - h.ms;
|
|
302
|
+
return Math.max(t.born, t.floor, kept);
|
|
303
|
+
}
|
|
304
|
+
function hold(t) {
|
|
305
|
+
const c = ctlOf(t);
|
|
306
|
+
if (c.held !== null)
|
|
307
|
+
throw new Error('blits: that mix is held already; free it first');
|
|
308
|
+
const p = proto(t);
|
|
309
|
+
const ramp = p.ramp;
|
|
310
|
+
const rebase = p.rebase;
|
|
311
|
+
const sync = p.sync;
|
|
312
|
+
const seekT = p.seek;
|
|
313
|
+
const queue = [];
|
|
314
|
+
let rate = 0;
|
|
315
|
+
let quiet = false;
|
|
316
|
+
let freed = false;
|
|
317
|
+
// The host time the hold syncs the transport at, moved by the host's gaps at `rate`; the
|
|
318
|
+
// transport's own rate is left alone, so nothing the hold does lands in its history.
|
|
319
|
+
let line = t.last;
|
|
320
|
+
let host = t.last;
|
|
321
|
+
const hushed = (fn) => {
|
|
322
|
+
const was = quiet;
|
|
323
|
+
quiet = true;
|
|
324
|
+
try {
|
|
325
|
+
fn();
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
quiet = was;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
const later = (fn) => {
|
|
332
|
+
if (t.replaying || quiet)
|
|
333
|
+
fn();
|
|
334
|
+
else
|
|
335
|
+
queue.push(fn);
|
|
336
|
+
};
|
|
337
|
+
/** Syncs at `line`, where a rebase the host asked for before the hold takes out no time of it. */
|
|
338
|
+
const at = () => {
|
|
339
|
+
if (t.rebasing && !Number.isNaN(t.last))
|
|
340
|
+
sync.call(t, t.last);
|
|
341
|
+
sync.call(t, line);
|
|
342
|
+
};
|
|
343
|
+
const puts = [];
|
|
344
|
+
const tape = t.tape;
|
|
345
|
+
if (tape !== undefined) {
|
|
346
|
+
const record = tape.recordEntry;
|
|
347
|
+
// Behind the tape's last call, a call the host makes would cut the calls after it; it acts
|
|
348
|
+
// on the mix but goes unrecorded, so they play on.
|
|
349
|
+
puts.push(swap(tape, 'recordEntry', (...args) => {
|
|
350
|
+
if (quiet || tape.timestampAt(tape.undoDepth()) !== undefined)
|
|
351
|
+
return;
|
|
352
|
+
record.apply(tape, args);
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
puts.push(swap(t, 'ramp', (r, over) => {
|
|
356
|
+
if (!(r >= 0 && r < Number.POSITIVE_INFINITY))
|
|
357
|
+
throw new RangeError(`blits: a mix's rate is a finite number, 0 or more, not ${r}`);
|
|
358
|
+
later(() => ramp.call(t, r, over));
|
|
359
|
+
}), swap(t, 'rebase', () => later(() => rebase.call(t))), swap(t, 'seek', (to) => later(() => seekT.call(t, to))));
|
|
360
|
+
for (const m of t.members) {
|
|
361
|
+
const mixSeek = proto(m).seek;
|
|
362
|
+
puts.push(swap(m, 'seek', (to) => {
|
|
363
|
+
if (t.shared)
|
|
364
|
+
mixSeek.call(m, to);
|
|
365
|
+
else
|
|
366
|
+
later(() => mixSeek.call(m, to));
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
c.held = {
|
|
370
|
+
sync(ts) {
|
|
371
|
+
if (Number.isNaN(line))
|
|
372
|
+
line = ts;
|
|
373
|
+
else if (!Number.isNaN(host) && ts > host)
|
|
374
|
+
line += (ts - host) * rate;
|
|
375
|
+
host = ts;
|
|
376
|
+
at();
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
fit(t);
|
|
380
|
+
t.woke();
|
|
381
|
+
const live = () => {
|
|
382
|
+
if (freed)
|
|
383
|
+
throw new Error('blits: that hold was freed');
|
|
384
|
+
};
|
|
385
|
+
return {
|
|
386
|
+
get mixes() {
|
|
387
|
+
return [...t.members];
|
|
388
|
+
},
|
|
389
|
+
get now() {
|
|
390
|
+
return t.now;
|
|
391
|
+
},
|
|
392
|
+
get reach() {
|
|
393
|
+
return reachOf(t, true);
|
|
394
|
+
},
|
|
395
|
+
get rate() {
|
|
396
|
+
return rate;
|
|
397
|
+
},
|
|
398
|
+
set rate(r) {
|
|
399
|
+
live();
|
|
400
|
+
if (!(r >= 0 && r < Number.POSITIVE_INFINITY))
|
|
401
|
+
throw new RangeError(`blits: a hold's rate is a finite number, 0 or more, not ${r}`);
|
|
402
|
+
rate = r;
|
|
403
|
+
},
|
|
404
|
+
seek(to) {
|
|
405
|
+
live();
|
|
406
|
+
hushed(() => seek(t, to));
|
|
407
|
+
},
|
|
408
|
+
step(ms) {
|
|
409
|
+
live();
|
|
410
|
+
if (!(ms > 0))
|
|
411
|
+
return;
|
|
412
|
+
if (Number.isNaN(line))
|
|
413
|
+
throw new Error('blits: a mix that has never synced has nothing to step');
|
|
414
|
+
const u = hostAhead(t, t.now + ms);
|
|
415
|
+
if (!Number.isFinite(u))
|
|
416
|
+
throw new RangeError("blits: the mix's own rate is 0, so it never moves");
|
|
417
|
+
line = Math.max(line, u + t.offset);
|
|
418
|
+
at();
|
|
419
|
+
for (const m of t.members)
|
|
420
|
+
m.stir();
|
|
421
|
+
},
|
|
422
|
+
free() {
|
|
423
|
+
if (freed)
|
|
424
|
+
return;
|
|
425
|
+
freed = true;
|
|
426
|
+
try {
|
|
427
|
+
rebase.call(t);
|
|
428
|
+
// Still behind the guard on the tape, so a call made behind its last one cuts nothing.
|
|
429
|
+
for (const fn of queue)
|
|
430
|
+
fn();
|
|
431
|
+
}
|
|
432
|
+
finally {
|
|
433
|
+
for (const put of puts.reverse())
|
|
434
|
+
put();
|
|
435
|
+
c.held = null;
|
|
436
|
+
fit(t);
|
|
437
|
+
t.woke();
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
package/dist/weigh.d.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import type { Mixer } from './mixer.js';
|
|
2
2
|
import type { Subject, Voice } from './voice.js';
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Where `prime` reads its numbers in `Mixer.primed`: as arguments, each would be boxed wherever V8
|
|
5
|
+
* does not inline the call, which turns on the order its compiles ran (bench/frame.mjs `glow`,
|
|
6
|
+
* 2026-10-20).
|
|
7
|
+
*/
|
|
8
|
+
export declare const enum Primed {
|
|
9
|
+
NOW = 0,
|
|
10
|
+
ELAPSED = 1,
|
|
11
|
+
PASS = 2,
|
|
12
|
+
SIZE = 3
|
|
13
|
+
}
|
|
14
|
+
/** Fills a voice's setting for a call to its patch, the weight aside, from `primed`. */
|
|
15
|
+
export declare function prime<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, held: Subject<unknown>): void;
|
|
5
16
|
/** A gap as a `dt`: Infinity under reduced motion, and no more than `maxDt`. */
|
|
6
17
|
export declare function capped<I, O>(this: Mixer<I, O>, gap: number): number;
|
|
7
18
|
/** The earliest voice time a read may still ask for, for a subject delayed `delay`. */
|
package/dist/weigh.js
CHANGED
|
@@ -2,8 +2,11 @@ import { envelope, weighed } from './clock.js';
|
|
|
2
2
|
import { elapsedThen, last, record } from './history.js';
|
|
3
3
|
import { ownersWeight } from './owner.js';
|
|
4
4
|
import { reading } from './reading.js';
|
|
5
|
-
/** Fills a voice's setting for a call to its patch, the weight aside
|
|
6
|
-
export function prime(voice, held
|
|
5
|
+
/** Fills a voice's setting for a call to its patch, the weight aside, from `primed`. */
|
|
6
|
+
export function prime(voice, held) {
|
|
7
|
+
const now = this.primed[0 /* Primed.NOW */];
|
|
8
|
+
const elapsed = this.primed[1 /* Primed.ELAPSED */];
|
|
9
|
+
const pass = this.primed[2 /* Primed.PASS */];
|
|
7
10
|
const setting = voice.setting;
|
|
8
11
|
setting.timestamp = now;
|
|
9
12
|
// With no step to move it, `stepped` follows the call before this frame's.
|
|
@@ -48,7 +51,11 @@ export function ownerBase(owner, subject) {
|
|
|
48
51
|
const kept = reading.kept;
|
|
49
52
|
// An owner's record keeps a frame in `probed`, not a time, so `prime` leaves its `stepped` be.
|
|
50
53
|
held.probed = Number.NaN;
|
|
51
|
-
|
|
54
|
+
const primed = this.primed;
|
|
55
|
+
primed[0 /* Primed.NOW */] = now;
|
|
56
|
+
primed[1 /* Primed.ELAPSED */] = owner.elapsedAt(now);
|
|
57
|
+
primed[2 /* Primed.PASS */] = 0;
|
|
58
|
+
this.prime(owner, held);
|
|
52
59
|
const base = this.base(owner, subject, now, held);
|
|
53
60
|
reading.kept = kept;
|
|
54
61
|
held.stepped = now;
|