@glissade/core 0.9.1-pre.0 → 0.10.0-pre.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/dist/index.d.ts CHANGED
@@ -9,6 +9,13 @@ import { $ as validateTrack, A as GainEnvelope, At as pathType, B as emitDevWarn
9
9
  * transitive dependents CHECK. A CHECK node re-validates dependency versions on
10
10
  * read and only recomputes if one actually changed; a recompute that produces an
11
11
  * equal value keeps its version, so dirtiness stops propagating at that node.
12
+ *
13
+ * Subscriber NOTIFICATION (only) is routed through the ticker (`./ticker.ts`,
14
+ * DESIGN.md §6.1): a write enqueues affected subscribers and the active
15
+ * scheduler decides when to flush. The default scheduler is synchronous and
16
+ * flushes at the end of the outermost write, reproducing the pre-ticker timing
17
+ * byte-for-byte. The read path (get/peek/updateIfNecessary) is untouched and
18
+ * stays fully synchronous.
12
19
  */
13
20
  type Equals<T> = (a: T, b: T) => boolean;
14
21
  declare class WriteDuringEvaluationError extends Error {
@@ -45,6 +52,52 @@ declare function computed<T>(fn: () => T, options?: SignalOptions<T>): ReadonlyS
45
52
  /** Run `fn` without registering dependencies on the active consumer. */
46
53
  declare function untracked<T>(fn: () => T): T;
47
54
  //#endregion
55
+ //#region src/ticker.d.ts
56
+ /**
57
+ * Per-tick subscriber-notification coalescer (DESIGN.md §6.1).
58
+ *
59
+ * Signals stay pull-based and synchronous on the READ path: peek/get/evaluate
60
+ * return the new value immediately after set() — the DIRTY/CHECK staleness
61
+ * cascade is untouched (§2.1). This module times only *subscriber notification*:
62
+ * a write enqueues each affected subscriber, and a scheduler decides when the
63
+ * queue is flushed.
64
+ *
65
+ * The default scheduler is **synchronous**: it flushes at the end of the
66
+ * outermost write (or batch), so a single set() outside any batch notifies its
67
+ * subscribers inline — byte-for-byte the timing the framework had before this
68
+ * module existed. `batch(fn)` coalesces every write inside `fn` into one flush;
69
+ * `setScheduler` lets a consumer defer the flush to a microtask / rAF so that a
70
+ * scrub frame dirtying N signals produces one observer pass (Theatre's
71
+ * `dataverse` Ticker pattern).
72
+ *
73
+ * Determinism: nothing here is reachable from evaluate(). No Date.now /
74
+ * performance.now / Math.random / setTimeout lives on this path; the default
75
+ * scheduler is pure and synchronous, and a write *during* a flush is drained by
76
+ * a bounded loop rather than recursion.
77
+ */
78
+ /**
79
+ * A scheduler receives the coalesced `flush` and decides when to run it. It must
80
+ * call `flush` exactly once per request (calling it synchronously reproduces the
81
+ * default, eager behavior). A microtask/rAF scheduler defers the call.
82
+ */
83
+ type Scheduler = (flush: () => void) => void;
84
+ /** The default scheduler: flush synchronously, preserving pre-ticker timing. */
85
+ declare const synchronousScheduler: Scheduler;
86
+ /**
87
+ * Install a notification scheduler. Pass the synchronous default back (or call
88
+ * with no argument) to restore eager flushing. Returns the previous scheduler.
89
+ */
90
+ declare function setScheduler(next?: Scheduler): Scheduler;
91
+ /** The synchronous default scheduler, exported so callers can restore it. */
92
+
93
+ /**
94
+ * Coalesce every signal write inside `fn` into a single subscriber-notification
95
+ * pass. Reads inside `fn` stay synchronous and see each write immediately; only
96
+ * the *notification* is deferred to the end. Nested batches flush once, at the
97
+ * outermost exit. Returns whatever `fn` returns.
98
+ */
99
+ declare function batch<T>(fn: () => T): T;
100
+ //#endregion
48
101
  //#region src/vec2Signal.d.ts
49
102
  interface Vec2Signal extends ReadonlySignal<Vec2> {
50
103
  readonly x: BindableSignal<number>;
@@ -124,7 +177,7 @@ declare function buildFontRegistry(assets?: Record<string, AssetRef> | undefined
124
177
  * - format 12 (segmented coverage — astral planes, e.g. emoji)
125
178
  * Malformed or unsupported input yields an empty set; it never throws or hangs.
126
179
  */
127
- declare function parseCmap(bytes: ArrayBuffer): Set<number>;
180
+ declare function parseCmap(bytes: ArrayBuffer | ArrayBufferView): Set<number>;
128
181
  //#endregion
129
182
  //#region src/fontValidation.d.ts
130
183
  type FontMode = 'strict' | 'dev';
@@ -304,4 +357,4 @@ interface CheckpointedSim {
304
357
  }
305
358
  declare function bakeCheckpointed<W, S = W>(cfg: CheckpointedBakeConfig<W, S>): CheckpointedSim;
306
359
  //#endregion
307
- export { type AssetRef, type AudioClip, type BakeConfig, BakeError, type BindTarget, type BindableSignal, type BoundTimeline, type CheckpointedBakeConfig, type CheckpointedSim, type ChildEntry, CircularDependencyError, ColorParseError, type CompiledTimeline, type CoverageReport, type CurveSampler, DEFAULT_EASE, type DevWarning, type EaseSpec, type EasingFn, type Equals, type FontFaceRef, type FontMode, type FontRegistry, type FontUsage, FontValidationError, type GainEnvelope, type HandoffKind, type Json, type Key, type KeyOpts, type Marker, type MergeResult, type MissingGlyphs, type OkLab, type OrphanReason, type PathContour, type PathValue, type Playhead, type Position, PositionError, type ReadonlySignal, type ResolvedFace, type RetargetSpring, type Rgba, type Rng, type SidecarDoc, type SidecarDocV1, type SidecarOrphan, type SidecarTimelineEntry, type SidecarTrackEntry, SidecarVersionError, type Signal, type SignalOptions, type SpringConfig, type SpringEase, TARGET_PATH, type TargetCarrier, type Timeline, type TimelineBuilder, type TimelineInit, TimelineValidationError, type Track, TrackValidationError, type TweenOpts, type TweenTarget, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, type ValidateFontsOptions, type ValueType, type ValueTypeId, ValueTypeInferenceError, type Vec2, type Vec2Signal, WriteDuringEvaluationError, assignKeyIds, audioOffsetSamples, bake, bakeCheckpointed, beginReadPhase, bindTimeline, booleanType, buildFontRegistry, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, cubicBezierDerivative, deleteSidecarTrack, easingDerivatives, easings, emitDevWarning, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, hashKeys, inReadPhase, inferValueType, isDurationEditable, isEditableNodeId, isExemptFamily, key, lerpColor, mergeSidecar, mergeSidecarDetailed, migrateSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseCmap, parseColor, pathType, random, registerValueType, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, setSidecarTrack, signal, spring, springEasing, springEasingDerivative, springPresets, springTo, stagger, stringType, targetNodeId, timeline, track, untracked, validateFonts, validateTrack, vec2ArcType, vec2Equals, vec2Signal, vec2Type, velocityAt };
360
+ export { type AssetRef, type AudioClip, type BakeConfig, BakeError, type BindTarget, type BindableSignal, type BoundTimeline, type CheckpointedBakeConfig, type CheckpointedSim, type ChildEntry, CircularDependencyError, ColorParseError, type CompiledTimeline, type CoverageReport, type CurveSampler, DEFAULT_EASE, type DevWarning, type EaseSpec, type EasingFn, type Equals, type FontFaceRef, type FontMode, type FontRegistry, type FontUsage, FontValidationError, type GainEnvelope, type HandoffKind, type Json, type Key, type KeyOpts, type Marker, type MergeResult, type MissingGlyphs, type OkLab, type OrphanReason, type PathContour, type PathValue, type Playhead, type Position, PositionError, type ReadonlySignal, type ResolvedFace, type RetargetSpring, type Rgba, type Rng, type Scheduler, type SidecarDoc, type SidecarDocV1, type SidecarOrphan, type SidecarTimelineEntry, type SidecarTrackEntry, SidecarVersionError, type Signal, type SignalOptions, type SpringConfig, type SpringEase, TARGET_PATH, type TargetCarrier, type Timeline, type TimelineBuilder, type TimelineInit, TimelineValidationError, type Track, TrackValidationError, type TweenOpts, type TweenTarget, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, type ValidateFontsOptions, type ValueType, type ValueTypeId, ValueTypeInferenceError, type Vec2, type Vec2Signal, WriteDuringEvaluationError, assignKeyIds, audioOffsetSamples, bake, bakeCheckpointed, batch, beginReadPhase, bindTimeline, booleanType, buildFontRegistry, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, cubicBezierDerivative, deleteSidecarTrack, easingDerivatives, easings, emitDevWarning, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, hashKeys, inReadPhase, inferValueType, isDurationEditable, isEditableNodeId, isExemptFamily, key, lerpColor, mergeSidecar, mergeSidecarDetailed, migrateSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseCmap, parseColor, pathType, random, registerValueType, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, setScheduler, setSidecarTrack, signal, spring, springEasing, springEasingDerivative, springPresets, springTo, stagger, stringType, synchronousScheduler, targetNodeId, timeline, track, untracked, validateFonts, validateTrack, vec2ArcType, vec2Equals, vec2Signal, vec2Type, velocityAt };
package/dist/index.js CHANGED
@@ -1,5 +1,125 @@
1
1
  import { $ as vec2Equals, A as velocityAt, B as easings, C as resolveEase, D as stagger, E as springTo, F as DEFAULT_EASE, G as colorType, H as UnknownValueTypeError, I as UnknownEasingError, J as numberType, K as getValueType, L as cubicBezier, M as springEasing, N as springEasingDerivative, O as track, P as springPresets, Q as vec2ArcType, R as cubicBezierDerivative, S as key, T as sampleTrack, U as ValueTypeInferenceError, V as namedEasing, W as booleanType, X as registerValueType, Y as pathType, Z as stringType, _ as audioOffsetSamples, a as hashKeys, at as lerpColor, b as timeline$1, c as migrateSidecar, ct as rgbaToOklab, d as TARGET_PATH, et as vec2Type, f as UnresolvableTargetError, g as TimelineValidationError, h as targetNodeId, i as emptySidecar, it as formatColor, j as spring, k as validateTrack, l as normalizeEditedKeys, m as resolveTweenTarget, n as assignKeyIds, nt as setDevWarning, o as mergeSidecar, ot as oklabToRgba, p as isEditableNodeId, q as inferValueType, r as deleteSidecarTrack, rt as ColorParseError, s as mergeSidecarDetailed, st as parseColor, t as SidecarVersionError, tt as emitDevWarning, u as setSidecarTrack, v as compileTimeline, w as resolveEaseDerivative, x as TrackValidationError, y as isDurationEditable, z as easingDerivatives } from "./sidecar.js";
2
+ //#region src/ticker.ts
3
+ /** The default scheduler: flush synchronously, preserving pre-ticker timing. */
4
+ const synchronousScheduler = (flush) => flush();
5
+ let scheduler = synchronousScheduler;
6
+ /** Pending subscriber callbacks for the current tick (deduped, insertion-ordered). */
7
+ const pending = /* @__PURE__ */ new Set();
8
+ /**
9
+ * Depth of active `batch()` calls *plus* in-flight top-level writes; >0
10
+ * suppresses flushing so a write's whole staleness cascade completes before any
11
+ * subscriber runs, and `batch()` coalesces across writes.
12
+ */
13
+ let batchDepth = 0;
14
+ /** True while a flush is in progress, so writes mid-flush enqueue, not re-flush. */
15
+ let flushing = false;
16
+ /** True once a flush has been requested from the scheduler but not yet run. */
17
+ let flushScheduled = false;
18
+ /** Guard against an unbounded write-during-flush cascade. */
19
+ const MAX_FLUSH_PASSES = 1e3;
20
+ /**
21
+ * Install a notification scheduler. Pass the synchronous default back (or call
22
+ * with no argument) to restore eager flushing. Returns the previous scheduler.
23
+ */
24
+ function setScheduler(next) {
25
+ const prev = scheduler;
26
+ scheduler = next ?? synchronousScheduler;
27
+ if (flushScheduled && !flushing) {
28
+ flushScheduled = false;
29
+ if (pending.size > 0) requestFlush();
30
+ }
31
+ return prev;
32
+ }
33
+ /**
34
+ * Enqueue a subscriber notification. Called by the signal layer in place of
35
+ * invoking the callback directly. Honors the active batch and scheduler.
36
+ */
37
+ function enqueueNotification(cb) {
38
+ pending.add(cb);
39
+ requestFlush();
40
+ }
41
+ function requestFlush() {
42
+ if (batchDepth > 0 || flushing || flushScheduled) return;
43
+ flushScheduled = true;
44
+ scheduler(runFlush);
45
+ }
46
+ function runFlush() {
47
+ flushScheduled = false;
48
+ if (flushing) return;
49
+ drain();
50
+ }
51
+ /**
52
+ * Drain the pending queue. A subscriber may write synchronously (e.g.
53
+ * useSyncExternalStore re-reads and a derived store updates), enqueuing more
54
+ * callbacks; the bounded loop catches those in subsequent passes within the
55
+ * same flush so each tick settles in one synchronous burst.
56
+ */
57
+ function drain() {
58
+ if (flushing) return;
59
+ flushing = true;
60
+ try {
61
+ let passes = 0;
62
+ while (pending.size > 0) {
63
+ if (++passes > MAX_FLUSH_PASSES) {
64
+ pending.clear();
65
+ throw new Error("signal notification flush did not settle within 1000 passes: a subscriber is writing to a signal it observes (DESIGN.md §6.1).");
66
+ }
67
+ const batchCbs = [...pending];
68
+ pending.clear();
69
+ for (const cb of batchCbs) cb();
70
+ }
71
+ } finally {
72
+ flushing = false;
73
+ }
74
+ }
75
+ /**
76
+ * Bracket a single top-level write (the signal layer calls this around each
77
+ * set/forceSet/bindSource cascade). While open, enqueued notifications are
78
+ * held; on the outermost close — when no `batch()` and no other write is in
79
+ * flight — the queue is flushed exactly as before this module existed.
80
+ *
81
+ * Nesting these (a write whose flush triggers another write) or wrapping them in
82
+ * `batch()` defers the flush to the outermost boundary, the coalescing the
83
+ * ticker exists to provide.
84
+ */
85
+ function beginNotify() {
86
+ batchDepth++;
87
+ }
88
+ function endNotify() {
89
+ batchDepth--;
90
+ if (batchDepth === 0 && pending.size > 0) requestFlush();
91
+ }
92
+ /**
93
+ * Coalesce every signal write inside `fn` into a single subscriber-notification
94
+ * pass. Reads inside `fn` stay synchronous and see each write immediately; only
95
+ * the *notification* is deferred to the end. Nested batches flush once, at the
96
+ * outermost exit. Returns whatever `fn` returns.
97
+ */
98
+ function batch(fn) {
99
+ beginNotify();
100
+ try {
101
+ return fn();
102
+ } finally {
103
+ endNotify();
104
+ }
105
+ }
106
+ //#endregion
2
107
  //#region src/signal.ts
108
+ /**
109
+ * Pull-based reactive signals: lazy, cached, dependency-tracked (DESIGN.md §2.1).
110
+ *
111
+ * Staleness uses the two-flag scheme: a write marks direct dependents DIRTY and
112
+ * transitive dependents CHECK. A CHECK node re-validates dependency versions on
113
+ * read and only recomputes if one actually changed; a recompute that produces an
114
+ * equal value keeps its version, so dirtiness stops propagating at that node.
115
+ *
116
+ * Subscriber NOTIFICATION (only) is routed through the ticker (`./ticker.ts`,
117
+ * DESIGN.md §6.1): a write enqueues affected subscribers and the active
118
+ * scheduler decides when to flush. The default scheduler is synchronous and
119
+ * flushes at the end of the outermost write, reproducing the pre-ticker timing
120
+ * byte-for-byte. The read path (get/peek/updateIfNecessary) is untouched and
121
+ * stays fully synchronous.
122
+ */
3
123
  let activeConsumer = null;
4
124
  let readPhaseDepth = 0;
5
125
  var WriteDuringEvaluationError = class extends Error {
@@ -54,8 +174,13 @@ var SignalNode = class {
54
174
  }
55
175
  set(next) {
56
176
  if (readPhaseDepth > 0) throw new WriteDuringEvaluationError();
57
- if (this.fn) this.detachFn();
58
- this.writeValue(next);
177
+ beginNotify();
178
+ try {
179
+ if (this.fn) this.detachFn();
180
+ this.writeValue(next);
181
+ } finally {
182
+ endNotify();
183
+ }
59
184
  }
60
185
  /**
61
186
  * Sanctioned entry write (the playhead at evaluate() entry, DESIGN.md §2.5).
@@ -63,15 +188,25 @@ var SignalNode = class {
63
188
  * signal surface.
64
189
  */
65
190
  forceSet(next) {
66
- if (this.fn) this.detachFn();
67
- this.writeValue(next);
191
+ beginNotify();
192
+ try {
193
+ if (this.fn) this.detachFn();
194
+ this.writeValue(next);
195
+ } finally {
196
+ endNotify();
197
+ }
68
198
  }
69
199
  /** Rewire this signal's source to a computation (timeline binding, §2.4). */
70
200
  bindSource(fn) {
71
201
  if (readPhaseDepth > 0) throw new WriteDuringEvaluationError();
72
- this.fn = fn;
73
- this.state = 2;
74
- this.invalidateDependents(2);
202
+ beginNotify();
203
+ try {
204
+ this.fn = fn;
205
+ this.state = 2;
206
+ this.invalidateDependents(2);
207
+ } finally {
208
+ endNotify();
209
+ }
75
210
  }
76
211
  /** Remove a bound source, freezing the signal at its last value. */
77
212
  unbindSource() {
@@ -109,7 +244,7 @@ var SignalNode = class {
109
244
  }
110
245
  invalidateDependents(level) {
111
246
  for (const d of [...this.dependents]) d.markStale(level);
112
- if (this.subscribers.size > 0) for (const cb of [...this.subscribers]) cb();
247
+ if (this.subscribers.size > 0) for (const cb of [...this.subscribers]) enqueueNotification(cb);
113
248
  }
114
249
  markStale(level) {
115
250
  if (this.state >= level) return;
@@ -380,7 +515,7 @@ function parseFormat12(dv, base, into) {
380
515
  function parseCmap(bytes) {
381
516
  const out = /* @__PURE__ */ new Set();
382
517
  try {
383
- const dv = new DataView(bytes);
518
+ const dv = ArrayBuffer.isView(bytes) ? new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength) : new DataView(bytes);
384
519
  if (dv.byteLength < 12) return out;
385
520
  let sfntOff = 0;
386
521
  if (u32(dv, 0) === 1953784678) sfntOff = u32(dv, 12);
@@ -942,4 +1077,4 @@ function bakeCheckpointed(cfg) {
942
1077
  };
943
1078
  }
944
1079
  //#endregion
945
- export { BakeError, CircularDependencyError, ColorParseError, DEFAULT_EASE, FontValidationError, PositionError, SidecarVersionError, TARGET_PATH, TimelineValidationError, TrackValidationError, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, ValueTypeInferenceError, WriteDuringEvaluationError, assignKeyIds, audioOffsetSamples, bake, bakeCheckpointed, beginReadPhase, bindTimeline, booleanType, buildFontRegistry, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, cubicBezierDerivative, deleteSidecarTrack, easingDerivatives, easings, emitDevWarning, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, hashKeys, inReadPhase, inferValueType, isDurationEditable, isEditableNodeId, isExemptFamily, key, lerpColor, mergeSidecar, mergeSidecarDetailed, migrateSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseCmap, parseColor, pathType, random, registerValueType, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, setSidecarTrack, signal, spring, springEasing, springEasingDerivative, springPresets, springTo, stagger, stringType, targetNodeId, timeline, track, untracked, validateFonts, validateTrack, vec2ArcType, vec2Equals, vec2Signal, vec2Type, velocityAt };
1080
+ export { BakeError, CircularDependencyError, ColorParseError, DEFAULT_EASE, FontValidationError, PositionError, SidecarVersionError, TARGET_PATH, TimelineValidationError, TrackValidationError, UnboundTargetError, UnknownEasingError, UnknownValueTypeError, UnresolvableTargetError, ValueTypeInferenceError, WriteDuringEvaluationError, assignKeyIds, audioOffsetSamples, bake, bakeCheckpointed, batch, beginReadPhase, bindTimeline, booleanType, buildFontRegistry, buildTimeline, colorType, compileTimeline, computed, createPlayhead, cubicBezier, cubicBezierDerivative, deleteSidecarTrack, easingDerivatives, easings, emitDevWarning, emptySidecar, endReadPhase, evaluateAt, formatColor, getTimelineCallbacks, getValueType, hashKeys, inReadPhase, inferValueType, isDurationEditable, isEditableNodeId, isExemptFamily, key, lerpColor, mergeSidecar, mergeSidecarDetailed, migrateSidecar, namedEasing, normalizeEditedKeys, numberType, oklabToRgba, parseCmap, parseColor, pathType, random, registerValueType, resolveEase, resolveEaseDerivative, resolveTweenTarget, rgbaToOklab, sampleTrack, setDevWarning, setScheduler, setSidecarTrack, signal, spring, springEasing, springEasingDerivative, springPresets, springTo, stagger, stringType, synchronousScheduler, targetNodeId, timeline, track, untracked, validateFonts, validateTrack, vec2ArcType, vec2Equals, vec2Signal, vec2Type, velocityAt };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/core",
3
- "version": "0.9.1-pre.0",
3
+ "version": "0.10.0-pre.0",
4
4
  "description": "glissade core: signals, tracks, timeline document, evaluation, easing, springs, seeded RNG. Zero DOM/Node dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",