@czap/core 0.1.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/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/addressed-digest.d.ts +15 -0
- package/dist/addressed-digest.d.ts.map +1 -0
- package/dist/addressed-digest.js +35 -0
- package/dist/addressed-digest.js.map +1 -0
- package/dist/animation.d.ts +46 -0
- package/dist/animation.d.ts.map +1 -0
- package/dist/animation.js +70 -0
- package/dist/animation.js.map +1 -0
- package/dist/assembly.d.ts +25 -0
- package/dist/assembly.d.ts.map +1 -0
- package/dist/assembly.js +58 -0
- package/dist/assembly.js.map +1 -0
- package/dist/av-bridge.d.ts +74 -0
- package/dist/av-bridge.d.ts.map +1 -0
- package/dist/av-bridge.js +107 -0
- package/dist/av-bridge.js.map +1 -0
- package/dist/av-renderer.d.ts +56 -0
- package/dist/av-renderer.d.ts.map +1 -0
- package/dist/av-renderer.js +65 -0
- package/dist/av-renderer.js.map +1 -0
- package/dist/blend.d.ts +61 -0
- package/dist/blend.d.ts.map +1 -0
- package/dist/blend.js +100 -0
- package/dist/blend.js.map +1 -0
- package/dist/boundary.d.ts +154 -0
- package/dist/boundary.d.ts.map +1 -0
- package/dist/boundary.js +269 -0
- package/dist/boundary.js.map +1 -0
- package/dist/brands.d.ts +63 -0
- package/dist/brands.d.ts.map +1 -0
- package/dist/brands.js +31 -0
- package/dist/brands.js.map +1 -0
- package/dist/caps.d.ts +49 -0
- package/dist/caps.d.ts.map +1 -0
- package/dist/caps.js +73 -0
- package/dist/caps.js.map +1 -0
- package/dist/capsule.d.ts +77 -0
- package/dist/capsule.d.ts.map +1 -0
- package/dist/capsule.js +18 -0
- package/dist/capsule.js.map +1 -0
- package/dist/capsules/boundary-evaluate.d.ts +28 -0
- package/dist/capsules/boundary-evaluate.d.ts.map +1 -0
- package/dist/capsules/boundary-evaluate.js +117 -0
- package/dist/capsules/boundary-evaluate.js.map +1 -0
- package/dist/capsules/canonical-cbor.d.ts +13 -0
- package/dist/capsules/canonical-cbor.d.ts.map +1 -0
- package/dist/capsules/canonical-cbor.js +60 -0
- package/dist/capsules/canonical-cbor.js.map +1 -0
- package/dist/capsules/token-buffer.d.ts +24 -0
- package/dist/capsules/token-buffer.d.ts.map +1 -0
- package/dist/capsules/token-buffer.js +53 -0
- package/dist/capsules/token-buffer.js.map +1 -0
- package/dist/capture.d.ts +40 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +10 -0
- package/dist/capture.js.map +1 -0
- package/dist/cbor.d.ts +33 -0
- package/dist/cbor.d.ts.map +1 -0
- package/dist/cbor.js +179 -0
- package/dist/cbor.js.map +1 -0
- package/dist/cell.d.ts +53 -0
- package/dist/cell.d.ts.map +1 -0
- package/dist/cell.js +83 -0
- package/dist/cell.js.map +1 -0
- package/dist/codec.d.ts +30 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +25 -0
- package/dist/codec.js.map +1 -0
- package/dist/component.d.ts +52 -0
- package/dist/component.d.ts.map +1 -0
- package/dist/component.js +44 -0
- package/dist/component.js.map +1 -0
- package/dist/composable.d.ts +76 -0
- package/dist/composable.d.ts.map +1 -0
- package/dist/composable.js +221 -0
- package/dist/composable.js.map +1 -0
- package/dist/compositor-pool.d.ts +74 -0
- package/dist/compositor-pool.d.ts.map +1 -0
- package/dist/compositor-pool.js +119 -0
- package/dist/compositor-pool.js.map +1 -0
- package/dist/compositor.d.ts +90 -0
- package/dist/compositor.d.ts.map +1 -0
- package/dist/compositor.js +278 -0
- package/dist/compositor.js.map +1 -0
- package/dist/config.d.ts +72 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +97 -0
- package/dist/config.js.map +1 -0
- package/dist/dag.d.ts +251 -0
- package/dist/dag.d.ts.map +1 -0
- package/dist/dag.js +450 -0
- package/dist/dag.js.map +1 -0
- package/dist/defaults.d.ts +45 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +45 -0
- package/dist/defaults.js.map +1 -0
- package/dist/derived.d.ts +34 -0
- package/dist/derived.d.ts.map +1 -0
- package/dist/derived.js +101 -0
- package/dist/derived.js.map +1 -0
- package/dist/diagnostics.d.ts +77 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +122 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/dirty.d.ts +55 -0
- package/dist/dirty.d.ts.map +1 -0
- package/dist/dirty.js +80 -0
- package/dist/dirty.js.map +1 -0
- package/dist/easing.d.ts +55 -0
- package/dist/easing.d.ts.map +1 -0
- package/dist/easing.js +291 -0
- package/dist/easing.js.map +1 -0
- package/dist/ecs.d.ts +105 -0
- package/dist/ecs.d.ts.map +1 -0
- package/dist/ecs.js +245 -0
- package/dist/ecs.js.map +1 -0
- package/dist/fnv.d.ts +14 -0
- package/dist/fnv.d.ts.map +1 -0
- package/dist/fnv.js +28 -0
- package/dist/fnv.js.map +1 -0
- package/dist/frame-budget.d.ts +73 -0
- package/dist/frame-budget.d.ts.map +1 -0
- package/dist/frame-budget.js +114 -0
- package/dist/frame-budget.js.map +1 -0
- package/dist/gen-frame.d.ts +102 -0
- package/dist/gen-frame.d.ts.map +1 -0
- package/dist/gen-frame.js +121 -0
- package/dist/gen-frame.js.map +1 -0
- package/dist/harness/arbitrary-from-schema.d.ts +28 -0
- package/dist/harness/arbitrary-from-schema.d.ts.map +1 -0
- package/dist/harness/arbitrary-from-schema.js +262 -0
- package/dist/harness/arbitrary-from-schema.js.map +1 -0
- package/dist/harness/cached-projection.d.ts +19 -0
- package/dist/harness/cached-projection.d.ts.map +1 -0
- package/dist/harness/cached-projection.js +39 -0
- package/dist/harness/cached-projection.js.map +1 -0
- package/dist/harness/index.d.ts +16 -0
- package/dist/harness/index.d.ts.map +1 -0
- package/dist/harness/index.js +15 -0
- package/dist/harness/index.js.map +1 -0
- package/dist/harness/policy-gate.d.ts +18 -0
- package/dist/harness/policy-gate.d.ts.map +1 -0
- package/dist/harness/policy-gate.js +46 -0
- package/dist/harness/policy-gate.js.map +1 -0
- package/dist/harness/pure-transform.d.ts +42 -0
- package/dist/harness/pure-transform.d.ts.map +1 -0
- package/dist/harness/pure-transform.js +76 -0
- package/dist/harness/pure-transform.js.map +1 -0
- package/dist/harness/receipted-mutation.d.ts +23 -0
- package/dist/harness/receipted-mutation.d.ts.map +1 -0
- package/dist/harness/receipted-mutation.js +52 -0
- package/dist/harness/receipted-mutation.js.map +1 -0
- package/dist/harness/scene-composition.d.ts +19 -0
- package/dist/harness/scene-composition.d.ts.map +1 -0
- package/dist/harness/scene-composition.js +47 -0
- package/dist/harness/scene-composition.js.map +1 -0
- package/dist/harness/site-adapter.d.ts +18 -0
- package/dist/harness/site-adapter.d.ts.map +1 -0
- package/dist/harness/site-adapter.js +38 -0
- package/dist/harness/site-adapter.js.map +1 -0
- package/dist/harness/state-machine.d.ts +19 -0
- package/dist/harness/state-machine.d.ts.map +1 -0
- package/dist/harness/state-machine.js +44 -0
- package/dist/harness/state-machine.js.map +1 -0
- package/dist/hlc.d.ts +99 -0
- package/dist/hlc.d.ts.map +1 -0
- package/dist/hlc.js +219 -0
- package/dist/hlc.js.map +1 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +137 -0
- package/dist/index.js.map +1 -0
- package/dist/interpolate.d.ts +14 -0
- package/dist/interpolate.d.ts.map +1 -0
- package/dist/interpolate.js +31 -0
- package/dist/interpolate.js.map +1 -0
- package/dist/live-cell.d.ts +46 -0
- package/dist/live-cell.d.ts.map +1 -0
- package/dist/live-cell.js +154 -0
- package/dist/live-cell.js.map +1 -0
- package/dist/op.d.ts +58 -0
- package/dist/op.d.ts.map +1 -0
- package/dist/op.js +171 -0
- package/dist/op.js.map +1 -0
- package/dist/plan.d.ts +195 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +211 -0
- package/dist/plan.js.map +1 -0
- package/dist/protocol.d.ts +33 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +10 -0
- package/dist/protocol.js.map +1 -0
- package/dist/quantizer-types.d.ts +28 -0
- package/dist/quantizer-types.d.ts.map +1 -0
- package/dist/quantizer-types.js +9 -0
- package/dist/quantizer-types.js.map +1 -0
- package/dist/receipt.d.ts +294 -0
- package/dist/receipt.d.ts.map +1 -0
- package/dist/receipt.js +352 -0
- package/dist/receipt.js.map +1 -0
- package/dist/runtime-coordinator.d.ts +75 -0
- package/dist/runtime-coordinator.d.ts.map +1 -0
- package/dist/runtime-coordinator.js +149 -0
- package/dist/runtime-coordinator.js.map +1 -0
- package/dist/scheduler.d.ts +58 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +109 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/ship-capsule.d.ts +54 -0
- package/dist/ship-capsule.d.ts.map +1 -0
- package/dist/ship-capsule.js +142 -0
- package/dist/ship-capsule.js.map +1 -0
- package/dist/ship-manifest.d.ts +45 -0
- package/dist/ship-manifest.d.ts.map +1 -0
- package/dist/ship-manifest.js +175 -0
- package/dist/ship-manifest.js.map +1 -0
- package/dist/signal.d.ts +149 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +277 -0
- package/dist/signal.js.map +1 -0
- package/dist/speculative.d.ts +67 -0
- package/dist/speculative.d.ts.map +1 -0
- package/dist/speculative.js +139 -0
- package/dist/speculative.js.map +1 -0
- package/dist/store.d.ts +39 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +42 -0
- package/dist/store.js.map +1 -0
- package/dist/style.d.ts +119 -0
- package/dist/style.d.ts.map +1 -0
- package/dist/style.js +168 -0
- package/dist/style.js.map +1 -0
- package/dist/testing.d.ts +14 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +14 -0
- package/dist/testing.js.map +1 -0
- package/dist/theme.d.ts +78 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +109 -0
- package/dist/theme.js.map +1 -0
- package/dist/timeline.d.ts +45 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +101 -0
- package/dist/timeline.js.map +1 -0
- package/dist/token-buffer.d.ts +43 -0
- package/dist/token-buffer.d.ts.map +1 -0
- package/dist/token-buffer.js +112 -0
- package/dist/token-buffer.js.map +1 -0
- package/dist/token.d.ts +107 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +143 -0
- package/dist/token.js.map +1 -0
- package/dist/tuple.d.ts +16 -0
- package/dist/tuple.d.ts.map +1 -0
- package/dist/tuple.js +16 -0
- package/dist/tuple.js.map +1 -0
- package/dist/type-utils.d.ts +41 -0
- package/dist/type-utils.d.ts.map +1 -0
- package/dist/type-utils.js +10 -0
- package/dist/type-utils.js.map +1 -0
- package/dist/typed-ref.d.ts +50 -0
- package/dist/typed-ref.d.ts.map +1 -0
- package/dist/typed-ref.js +59 -0
- package/dist/typed-ref.js.map +1 -0
- package/dist/ui-quality.d.ts +50 -0
- package/dist/ui-quality.d.ts.map +1 -0
- package/dist/ui-quality.js +64 -0
- package/dist/ui-quality.js.map +1 -0
- package/dist/validation-error.d.ts +25 -0
- package/dist/validation-error.d.ts.map +1 -0
- package/dist/validation-error.js +32 -0
- package/dist/validation-error.js.map +1 -0
- package/dist/vector-clock.d.ts +46 -0
- package/dist/vector-clock.d.ts.map +1 -0
- package/dist/vector-clock.js +91 -0
- package/dist/vector-clock.js.map +1 -0
- package/dist/video.d.ts +62 -0
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +59 -0
- package/dist/video.js.map +1 -0
- package/dist/wasm-dispatch.d.ts +52 -0
- package/dist/wasm-dispatch.d.ts.map +1 -0
- package/dist/wasm-dispatch.js +204 -0
- package/dist/wasm-dispatch.js.map +1 -0
- package/dist/wasm-fallback.d.ts +19 -0
- package/dist/wasm-fallback.d.ts.map +1 -0
- package/dist/wasm-fallback.js +93 -0
- package/dist/wasm-fallback.js.map +1 -0
- package/dist/wire.d.ts +49 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +201 -0
- package/dist/wire.js.map +1 -0
- package/dist/zap.d.ts +42 -0
- package/dist/zap.d.ts.map +1 -0
- package/dist/zap.js +172 -0
- package/dist/zap.js.map +1 -0
- package/package.json +71 -0
- package/src/addressed-digest.ts +48 -0
- package/src/animation.ts +103 -0
- package/src/assembly.ts +76 -0
- package/src/av-bridge.ts +161 -0
- package/src/av-renderer.ts +118 -0
- package/src/blend.ts +135 -0
- package/src/boundary.ts +363 -0
- package/src/brands.ts +86 -0
- package/src/caps.ts +100 -0
- package/src/capsule.ts +95 -0
- package/src/capsules/boundary-evaluate.ts +128 -0
- package/src/capsules/canonical-cbor.ts +60 -0
- package/src/capsules/token-buffer.ts +57 -0
- package/src/capture.ts +48 -0
- package/src/cbor.ts +199 -0
- package/src/cell.ts +130 -0
- package/src/codec.ts +39 -0
- package/src/component.ts +102 -0
- package/src/composable.ts +328 -0
- package/src/compositor-pool.ts +162 -0
- package/src/compositor.ts +387 -0
- package/src/config.ts +157 -0
- package/src/dag.ts +527 -0
- package/src/defaults.ts +60 -0
- package/src/derived.ts +164 -0
- package/src/diagnostics.ts +186 -0
- package/src/dirty.ts +101 -0
- package/src/easing.ts +334 -0
- package/src/ecs.ts +382 -0
- package/src/fnv.ts +31 -0
- package/src/frame-budget.ts +149 -0
- package/src/gen-frame.ts +229 -0
- package/src/harness/arbitrary-from-schema.ts +270 -0
- package/src/harness/cached-projection.ts +46 -0
- package/src/harness/index.ts +16 -0
- package/src/harness/policy-gate.ts +51 -0
- package/src/harness/pure-transform.ts +121 -0
- package/src/harness/receipted-mutation.ts +59 -0
- package/src/harness/scene-composition.ts +54 -0
- package/src/harness/site-adapter.ts +43 -0
- package/src/harness/state-machine.ts +49 -0
- package/src/hlc.ts +238 -0
- package/src/index.ts +274 -0
- package/src/interpolate.ts +37 -0
- package/src/live-cell.ts +199 -0
- package/src/op.ts +233 -0
- package/src/plan.ts +317 -0
- package/src/protocol.ts +49 -0
- package/src/quantizer-types.ts +29 -0
- package/src/receipt.ts +444 -0
- package/src/runtime-coordinator.ts +230 -0
- package/src/scheduler.ts +161 -0
- package/src/ship-capsule.ts +191 -0
- package/src/signal.ts +345 -0
- package/src/speculative.ts +186 -0
- package/src/store.ts +77 -0
- package/src/style.ts +249 -0
- package/src/testing.ts +14 -0
- package/src/theme.ts +153 -0
- package/src/timeline.ts +146 -0
- package/src/token-buffer.ts +151 -0
- package/src/token.ts +197 -0
- package/src/tuple.ts +19 -0
- package/src/type-utils.ts +48 -0
- package/src/typed-ref.ts +79 -0
- package/src/ui-quality.ts +105 -0
- package/src/validation-error.ts +34 -0
- package/src/vector-clock.ts +111 -0
- package/src/video.ts +106 -0
- package/src/wasm-dispatch.ts +300 -0
- package/src/wasm-fallback.ts +102 -0
- package/src/wire.ts +274 -0
- package/src/zap.ts +241 -0
package/src/scheduler.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler -- clock abstraction decoupling animation from requestAnimationFrame.
|
|
3
|
+
*
|
|
4
|
+
* Four implementations:
|
|
5
|
+
* - raf: browser real-time (default)
|
|
6
|
+
* - noop: SSR-safe
|
|
7
|
+
* - fixedStep: deterministic timestamps at target fps (video rendering)
|
|
8
|
+
* - audioSync: ticks in lockstep with an AVBridge sample counter
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { AVBridge } from './av-bridge.js';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Types
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
interface SchedulerShape {
|
|
20
|
+
readonly _tag: 'FrameScheduler';
|
|
21
|
+
schedule(callback: (now: number) => void): number;
|
|
22
|
+
cancel(id: number): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface FixedStepShape extends SchedulerShape {
|
|
26
|
+
step(): void;
|
|
27
|
+
readonly frame: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Implementations
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/** Default: requestAnimationFrame. Used by Timeline/animate in browser. */
|
|
35
|
+
function _raf(): SchedulerShape {
|
|
36
|
+
return {
|
|
37
|
+
_tag: 'FrameScheduler',
|
|
38
|
+
schedule: (cb) => requestAnimationFrame(cb),
|
|
39
|
+
cancel: (id) => cancelAnimationFrame(id),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** SSR-safe: noop scheduler for server environments. */
|
|
44
|
+
function _noop(): SchedulerShape {
|
|
45
|
+
return {
|
|
46
|
+
_tag: 'FrameScheduler',
|
|
47
|
+
schedule: () => 0,
|
|
48
|
+
cancel: () => {},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Fixed-step: deterministic timestamps at target fps. For video rendering.
|
|
53
|
+
* Uses a class for V8 hidden-class optimization (stable inline caches). */
|
|
54
|
+
class FixedStepSchedulerImpl implements FixedStepShape {
|
|
55
|
+
readonly _tag = 'FrameScheduler' as const;
|
|
56
|
+
_frame: number = 0;
|
|
57
|
+
_cb: ((now: number) => void) | null = null;
|
|
58
|
+
_dt: number;
|
|
59
|
+
|
|
60
|
+
constructor(fps: number) {
|
|
61
|
+
this._dt = 1000 / fps;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get frame() {
|
|
65
|
+
return this._frame;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
schedule(cb: (now: number) => void) {
|
|
69
|
+
this._cb = cb;
|
|
70
|
+
return this._frame;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
cancel() {
|
|
74
|
+
this._cb = null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
step() {
|
|
78
|
+
const cb = this._cb;
|
|
79
|
+
if (cb) {
|
|
80
|
+
this._cb = null;
|
|
81
|
+
cb(this._frame * this._dt);
|
|
82
|
+
}
|
|
83
|
+
this._frame++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _fixedStep(fps: number): FixedStepShape {
|
|
88
|
+
return new FixedStepSchedulerImpl(fps);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Audio-sync scheduler
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
interface AudioSyncShape extends SchedulerShape {
|
|
96
|
+
poll(): void;
|
|
97
|
+
readonly frame: number;
|
|
98
|
+
readonly bridge: AVBridge.Shape;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function _audioSync(bridge: AVBridge.Shape): AudioSyncShape {
|
|
102
|
+
let lastFrame = -1;
|
|
103
|
+
let pendingCallback: ((now: number) => void) | null = null;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
_tag: 'FrameScheduler',
|
|
107
|
+
bridge,
|
|
108
|
+
|
|
109
|
+
get frame() {
|
|
110
|
+
return bridge.getCurrentFrame();
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
schedule(cb) {
|
|
114
|
+
pendingCallback = cb;
|
|
115
|
+
return bridge.getCurrentFrame();
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
cancel() {
|
|
119
|
+
pendingCallback = null;
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
poll() {
|
|
123
|
+
const currentFrame = bridge.getCurrentFrame();
|
|
124
|
+
if (currentFrame !== lastFrame) {
|
|
125
|
+
lastFrame = currentFrame;
|
|
126
|
+
const cb = pendingCallback;
|
|
127
|
+
if (cb) {
|
|
128
|
+
pendingCallback = null;
|
|
129
|
+
const timestampMs = bridge.sampleToTime(bridge.getCurrentSample()) * 1000;
|
|
130
|
+
cb(timestampMs);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Scheduler — clock abstraction that decouples animation driver from real time.
|
|
139
|
+
* Pick the impl that matches the runtime: `raf` in browser, `noop` on the
|
|
140
|
+
* server, `fixedStep` for deterministic video render, `audioSync` to drive UI
|
|
141
|
+
* in lockstep with an {@link AVBridge}.
|
|
142
|
+
*/
|
|
143
|
+
export const Scheduler = {
|
|
144
|
+
/** `requestAnimationFrame`-backed scheduler for browser real-time work. */
|
|
145
|
+
raf: _raf,
|
|
146
|
+
/** No-op scheduler for SSR / environments without rAF. */
|
|
147
|
+
noop: _noop,
|
|
148
|
+
/** Fixed-step scheduler at the given fps — deterministic timestamps for offline rendering. */
|
|
149
|
+
fixedStep: _fixedStep,
|
|
150
|
+
/** Scheduler that polls an {@link AVBridge} and fires callbacks when the sample frame advances. */
|
|
151
|
+
audioSync: _audioSync,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export declare namespace Scheduler {
|
|
155
|
+
/** Common structural shape every scheduler variant satisfies. */
|
|
156
|
+
export type Shape = SchedulerShape;
|
|
157
|
+
/** Fixed-step scheduler with manual `step()` advancement. */
|
|
158
|
+
export type FixedStep = FixedStepShape;
|
|
159
|
+
/** Audio-synchronized scheduler bound to an {@link AVBridge}. */
|
|
160
|
+
export type AudioSync = AudioSyncShape;
|
|
161
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShipCapsule — release-artifact receipt (ADR-0011).
|
|
3
|
+
*
|
|
4
|
+
* Same canonical-CBOR + ContentAddress kernel as runtime primitives, applied
|
|
5
|
+
* to a published-package tarball. `id` is the fnv1a label over the
|
|
6
|
+
* canonical bytes of every field except `id` and `integrity`; `integrity`
|
|
7
|
+
* pairs that label with a sha256 digest over the same bytes.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Effect } from 'effect';
|
|
13
|
+
import { decode as cborDecode } from 'cborg';
|
|
14
|
+
import type { AddressedDigest, ContentAddress, HLC } from './brands.js';
|
|
15
|
+
import { CanonicalCbor } from './cbor.js';
|
|
16
|
+
import { AddressedDigest as AddressedDigestNs } from './addressed-digest.js';
|
|
17
|
+
|
|
18
|
+
interface ShipCapsuleBuildEnv {
|
|
19
|
+
readonly node_version: string;
|
|
20
|
+
readonly pnpm_version: string;
|
|
21
|
+
readonly os: 'linux' | 'darwin' | 'win32';
|
|
22
|
+
readonly arch: 'x64' | 'arm64';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ShipCapsuleShape {
|
|
26
|
+
readonly _kind: 'shipCapsule';
|
|
27
|
+
readonly schema_version: 1;
|
|
28
|
+
readonly id: ContentAddress;
|
|
29
|
+
readonly integrity: AddressedDigest;
|
|
30
|
+
readonly package_name: string;
|
|
31
|
+
readonly package_version: string;
|
|
32
|
+
readonly source_commit: string;
|
|
33
|
+
readonly source_dirty: boolean;
|
|
34
|
+
readonly lockfile_address: AddressedDigest;
|
|
35
|
+
readonly workspace_manifest_address: AddressedDigest;
|
|
36
|
+
readonly tarball_manifest_address: AddressedDigest;
|
|
37
|
+
readonly build_env: ShipCapsuleBuildEnv;
|
|
38
|
+
readonly package_manager: 'pnpm';
|
|
39
|
+
readonly package_manager_version: string;
|
|
40
|
+
readonly publish_dry_run_address: AddressedDigest;
|
|
41
|
+
readonly lifecycle_scripts_observed: readonly string[];
|
|
42
|
+
readonly generated_at: HLC;
|
|
43
|
+
readonly previous_ship_capsule: ContentAddress | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type ShipCapsuleInput = Omit<ShipCapsuleShape, 'id' | 'integrity'>;
|
|
47
|
+
|
|
48
|
+
type ShipCapsuleDecodeError = 'non_canonical' | 'malformed_cbor' | 'invalid_shape';
|
|
49
|
+
|
|
50
|
+
const REQUIRED_KEYS: readonly (keyof ShipCapsuleShape)[] = [
|
|
51
|
+
'_kind',
|
|
52
|
+
'schema_version',
|
|
53
|
+
'id',
|
|
54
|
+
'integrity',
|
|
55
|
+
'package_name',
|
|
56
|
+
'package_version',
|
|
57
|
+
'source_commit',
|
|
58
|
+
'source_dirty',
|
|
59
|
+
'lockfile_address',
|
|
60
|
+
'workspace_manifest_address',
|
|
61
|
+
'tarball_manifest_address',
|
|
62
|
+
'build_env',
|
|
63
|
+
'package_manager',
|
|
64
|
+
'package_manager_version',
|
|
65
|
+
'publish_dry_run_address',
|
|
66
|
+
'lifecycle_scripts_observed',
|
|
67
|
+
'generated_at',
|
|
68
|
+
'previous_ship_capsule',
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const encodeIdentityBearing = (capsule: ShipCapsuleInput): Uint8Array =>
|
|
72
|
+
CanonicalCbor.encode({
|
|
73
|
+
_kind: capsule._kind,
|
|
74
|
+
schema_version: capsule.schema_version,
|
|
75
|
+
package_name: capsule.package_name,
|
|
76
|
+
package_version: capsule.package_version,
|
|
77
|
+
source_commit: capsule.source_commit,
|
|
78
|
+
source_dirty: capsule.source_dirty,
|
|
79
|
+
lockfile_address: capsule.lockfile_address,
|
|
80
|
+
workspace_manifest_address: capsule.workspace_manifest_address,
|
|
81
|
+
tarball_manifest_address: capsule.tarball_manifest_address,
|
|
82
|
+
build_env: capsule.build_env,
|
|
83
|
+
package_manager: capsule.package_manager,
|
|
84
|
+
package_manager_version: capsule.package_manager_version,
|
|
85
|
+
publish_dry_run_address: capsule.publish_dry_run_address,
|
|
86
|
+
lifecycle_scripts_observed: capsule.lifecycle_scripts_observed,
|
|
87
|
+
generated_at: capsule.generated_at,
|
|
88
|
+
previous_ship_capsule: capsule.previous_ship_capsule,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const computeId = (
|
|
92
|
+
capsuleWithoutIdentity: ShipCapsuleInput,
|
|
93
|
+
): Effect.Effect<AddressedDigest, Error> => AddressedDigestNs.of(encodeIdentityBearing(capsuleWithoutIdentity));
|
|
94
|
+
|
|
95
|
+
const make = (input: ShipCapsuleInput): Effect.Effect<ShipCapsuleShape, Error> =>
|
|
96
|
+
Effect.gen(function* () {
|
|
97
|
+
const digest = yield* computeId(input);
|
|
98
|
+
return {
|
|
99
|
+
_kind: input._kind,
|
|
100
|
+
schema_version: input.schema_version,
|
|
101
|
+
id: digest.display_id,
|
|
102
|
+
integrity: digest,
|
|
103
|
+
package_name: input.package_name,
|
|
104
|
+
package_version: input.package_version,
|
|
105
|
+
source_commit: input.source_commit,
|
|
106
|
+
source_dirty: input.source_dirty,
|
|
107
|
+
lockfile_address: input.lockfile_address,
|
|
108
|
+
workspace_manifest_address: input.workspace_manifest_address,
|
|
109
|
+
tarball_manifest_address: input.tarball_manifest_address,
|
|
110
|
+
build_env: input.build_env,
|
|
111
|
+
package_manager: input.package_manager,
|
|
112
|
+
package_manager_version: input.package_manager_version,
|
|
113
|
+
publish_dry_run_address: input.publish_dry_run_address,
|
|
114
|
+
lifecycle_scripts_observed: input.lifecycle_scripts_observed,
|
|
115
|
+
generated_at: input.generated_at,
|
|
116
|
+
previous_ship_capsule: input.previous_ship_capsule,
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const canonicalize = (capsule: ShipCapsuleShape): Uint8Array =>
|
|
121
|
+
CanonicalCbor.encode({
|
|
122
|
+
_kind: capsule._kind,
|
|
123
|
+
schema_version: capsule.schema_version,
|
|
124
|
+
id: capsule.id,
|
|
125
|
+
integrity: capsule.integrity,
|
|
126
|
+
package_name: capsule.package_name,
|
|
127
|
+
package_version: capsule.package_version,
|
|
128
|
+
source_commit: capsule.source_commit,
|
|
129
|
+
source_dirty: capsule.source_dirty,
|
|
130
|
+
lockfile_address: capsule.lockfile_address,
|
|
131
|
+
workspace_manifest_address: capsule.workspace_manifest_address,
|
|
132
|
+
tarball_manifest_address: capsule.tarball_manifest_address,
|
|
133
|
+
build_env: capsule.build_env,
|
|
134
|
+
package_manager: capsule.package_manager,
|
|
135
|
+
package_manager_version: capsule.package_manager_version,
|
|
136
|
+
publish_dry_run_address: capsule.publish_dry_run_address,
|
|
137
|
+
lifecycle_scripts_observed: capsule.lifecycle_scripts_observed,
|
|
138
|
+
generated_at: capsule.generated_at,
|
|
139
|
+
previous_ship_capsule: capsule.previous_ship_capsule,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
143
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
144
|
+
|
|
145
|
+
const validateShape = (value: unknown): value is ShipCapsuleShape => {
|
|
146
|
+
if (!isRecord(value)) return false;
|
|
147
|
+
for (const k of REQUIRED_KEYS) {
|
|
148
|
+
if (!(k in value)) return false;
|
|
149
|
+
}
|
|
150
|
+
if (value._kind !== 'shipCapsule') return false;
|
|
151
|
+
if (value.schema_version !== 1) return false;
|
|
152
|
+
if (!isRecord(value.integrity)) return false;
|
|
153
|
+
if (!isRecord(value.build_env)) return false;
|
|
154
|
+
if (!Array.isArray(value.lifecycle_scripts_observed)) return false;
|
|
155
|
+
if (!isRecord(value.generated_at)) return false;
|
|
156
|
+
return true;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const decode = (
|
|
160
|
+
bytes: Uint8Array,
|
|
161
|
+
): Effect.Effect<ShipCapsuleShape, ShipCapsuleDecodeError> =>
|
|
162
|
+
Effect.gen(function* () {
|
|
163
|
+
let decoded: unknown;
|
|
164
|
+
try {
|
|
165
|
+
decoded = cborDecode(bytes);
|
|
166
|
+
} catch {
|
|
167
|
+
return yield* Effect.fail('malformed_cbor' as const);
|
|
168
|
+
}
|
|
169
|
+
if (!validateShape(decoded)) {
|
|
170
|
+
return yield* Effect.fail('invalid_shape' as const);
|
|
171
|
+
}
|
|
172
|
+
const reencoded = canonicalize(decoded);
|
|
173
|
+
if (reencoded.length !== bytes.length) {
|
|
174
|
+
return yield* Effect.fail('non_canonical' as const);
|
|
175
|
+
}
|
|
176
|
+
for (let i = 0; i < reencoded.length; i++) {
|
|
177
|
+
if (reencoded[i] !== bytes[i]) {
|
|
178
|
+
return yield* Effect.fail('non_canonical' as const);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return decoded;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
export const ShipCapsule = { make, canonicalize, decode, computeId };
|
|
185
|
+
|
|
186
|
+
export declare namespace ShipCapsule {
|
|
187
|
+
export type Shape = ShipCapsuleShape;
|
|
188
|
+
export type Input = ShipCapsuleInput;
|
|
189
|
+
export type DecodeError = ShipCapsuleDecodeError;
|
|
190
|
+
export type BuildEnv = ShipCapsuleBuildEnv;
|
|
191
|
+
}
|
package/src/signal.ts
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signal -- live data feeds from the browser environment.
|
|
3
|
+
*
|
|
4
|
+
* (viewport, scroll, pointer, time, media queries, custom).
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Stream, Scope } from 'effect';
|
|
10
|
+
import { Effect, SubscriptionRef, Ref } from 'effect';
|
|
11
|
+
import type { AVBridge } from './av-bridge.js';
|
|
12
|
+
|
|
13
|
+
/** Tag of a {@link SignalSource} — the family of live data feed a signal binds to. */
|
|
14
|
+
export type SignalSourceType = 'viewport' | 'time' | 'pointer' | 'scroll' | 'media' | 'custom' | 'audio';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Configuration describing what a {@link Signal} reads from: viewport axis,
|
|
18
|
+
* time mode, pointer axis, scroll axis, media query, custom push source,
|
|
19
|
+
* or audio sample/normalized mode.
|
|
20
|
+
*/
|
|
21
|
+
export type SignalSource =
|
|
22
|
+
| { readonly type: 'viewport'; readonly axis: 'width' | 'height' }
|
|
23
|
+
| { readonly type: 'time'; readonly mode: 'elapsed' | 'absolute' | 'scheduled' }
|
|
24
|
+
| { readonly type: 'pointer'; readonly axis: 'x' | 'y' | 'pressure' }
|
|
25
|
+
| { readonly type: 'scroll'; readonly axis: 'x' | 'y' | 'progress' }
|
|
26
|
+
| { readonly type: 'media'; readonly query: string }
|
|
27
|
+
| { readonly type: 'custom'; readonly id: string }
|
|
28
|
+
| { readonly type: 'audio'; readonly mode: 'sample' | 'normalized' };
|
|
29
|
+
|
|
30
|
+
interface SignalShape<T> {
|
|
31
|
+
readonly source: SignalSource;
|
|
32
|
+
readonly current: Effect.Effect<T>;
|
|
33
|
+
readonly changes: Stream.Stream<T>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ControllableSignalShape<T> extends SignalShape<T> {
|
|
37
|
+
seek(to: T): Effect.Effect<void>;
|
|
38
|
+
pause(): Effect.Effect<void>;
|
|
39
|
+
resume(): Effect.Effect<void>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function initialValueForSource(source: SignalSource): number {
|
|
43
|
+
switch (source.type) {
|
|
44
|
+
case 'viewport':
|
|
45
|
+
return typeof globalThis.window !== 'undefined'
|
|
46
|
+
? source.axis === 'width'
|
|
47
|
+
? window.innerWidth
|
|
48
|
+
: window.innerHeight
|
|
49
|
+
: 0;
|
|
50
|
+
case 'scroll':
|
|
51
|
+
if (typeof globalThis.window === 'undefined') return 0;
|
|
52
|
+
if (source.axis === 'x') return window.scrollX;
|
|
53
|
+
if (source.axis === 'y') return window.scrollY;
|
|
54
|
+
{
|
|
55
|
+
const max = document.documentElement.scrollHeight - window.innerHeight;
|
|
56
|
+
return max > 0 ? window.scrollY / max : 0;
|
|
57
|
+
}
|
|
58
|
+
case 'pointer':
|
|
59
|
+
return 0;
|
|
60
|
+
case 'time':
|
|
61
|
+
return source.mode === 'absolute' ? Date.now() : 0;
|
|
62
|
+
case 'media':
|
|
63
|
+
return typeof globalThis.window !== 'undefined' && window.matchMedia(source.query).matches ? 1 : 0;
|
|
64
|
+
case 'custom':
|
|
65
|
+
return 0;
|
|
66
|
+
case 'audio':
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create a reactive signal from a browser environment source.
|
|
73
|
+
*
|
|
74
|
+
* Returns a scoped Effect that sets up event listeners (resize, scroll,
|
|
75
|
+
* pointermove, etc.) and cleans them up when the scope closes. The signal
|
|
76
|
+
* exposes `.current` (latest value) and `.changes` (stream of updates).
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* import { Effect, Scope } from 'effect';
|
|
81
|
+
* import { Signal } from '@czap/core';
|
|
82
|
+
*
|
|
83
|
+
* const program = Effect.scoped(Effect.gen(function* () {
|
|
84
|
+
* const sig = yield* Signal.make({ type: 'viewport', axis: 'width' });
|
|
85
|
+
* const width = yield* sig.current;
|
|
86
|
+
* // width === current window.innerWidth
|
|
87
|
+
* }));
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
function _make(source: SignalSource): Effect.Effect<SignalShape<number>, never, Scope.Scope> {
|
|
91
|
+
return Effect.gen(function* () {
|
|
92
|
+
const initial = initialValueForSource(source);
|
|
93
|
+
const ref = yield* SubscriptionRef.make(initial);
|
|
94
|
+
|
|
95
|
+
const setupListener = Effect.gen(function* () {
|
|
96
|
+
switch (source.type) {
|
|
97
|
+
case 'viewport': {
|
|
98
|
+
if (typeof globalThis.window === 'undefined') return;
|
|
99
|
+
const handler = () => {
|
|
100
|
+
const val = source.axis === 'width' ? window.innerWidth : window.innerHeight;
|
|
101
|
+
Effect.runSync(SubscriptionRef.set(ref, val));
|
|
102
|
+
};
|
|
103
|
+
yield* Effect.acquireRelease(
|
|
104
|
+
Effect.sync(() => {
|
|
105
|
+
window.addEventListener('resize', handler);
|
|
106
|
+
}),
|
|
107
|
+
() =>
|
|
108
|
+
Effect.sync(() => {
|
|
109
|
+
window.removeEventListener('resize', handler);
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case 'scroll': {
|
|
115
|
+
if (typeof globalThis.window === 'undefined') return;
|
|
116
|
+
const handler = () => {
|
|
117
|
+
let val: number;
|
|
118
|
+
if (source.axis === 'x') val = window.scrollX;
|
|
119
|
+
else if (source.axis === 'y') val = window.scrollY;
|
|
120
|
+
else {
|
|
121
|
+
const max = document.documentElement.scrollHeight - window.innerHeight;
|
|
122
|
+
val = max > 0 ? window.scrollY / max : 0;
|
|
123
|
+
}
|
|
124
|
+
Effect.runSync(SubscriptionRef.set(ref, val));
|
|
125
|
+
};
|
|
126
|
+
yield* Effect.acquireRelease(
|
|
127
|
+
Effect.sync(() => {
|
|
128
|
+
window.addEventListener('scroll', handler, { passive: true });
|
|
129
|
+
}),
|
|
130
|
+
() =>
|
|
131
|
+
Effect.sync(() => {
|
|
132
|
+
window.removeEventListener('scroll', handler);
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'pointer': {
|
|
138
|
+
if (typeof globalThis.window === 'undefined') return;
|
|
139
|
+
const handler = (e: PointerEvent) => {
|
|
140
|
+
const val = source.axis === 'x' ? e.clientX : source.axis === 'y' ? e.clientY : e.pressure;
|
|
141
|
+
Effect.runSync(SubscriptionRef.set(ref, val));
|
|
142
|
+
};
|
|
143
|
+
yield* Effect.acquireRelease(
|
|
144
|
+
Effect.sync(() => {
|
|
145
|
+
window.addEventListener('pointermove', handler);
|
|
146
|
+
}),
|
|
147
|
+
() =>
|
|
148
|
+
Effect.sync(() => {
|
|
149
|
+
window.removeEventListener('pointermove', handler);
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case 'time': {
|
|
155
|
+
if (source.mode === 'elapsed') {
|
|
156
|
+
if (typeof requestAnimationFrame === 'undefined') return;
|
|
157
|
+
const start = Date.now();
|
|
158
|
+
const id = { current: 0 };
|
|
159
|
+
const tick = () => {
|
|
160
|
+
Effect.runSync(SubscriptionRef.set(ref, Date.now() - start));
|
|
161
|
+
id.current = requestAnimationFrame(tick);
|
|
162
|
+
};
|
|
163
|
+
id.current = requestAnimationFrame(tick);
|
|
164
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => cancelAnimationFrame(id.current)));
|
|
165
|
+
} else if (source.mode === 'absolute') {
|
|
166
|
+
const id = setInterval(() => {
|
|
167
|
+
Effect.runSync(SubscriptionRef.set(ref, Date.now()));
|
|
168
|
+
}, 1000);
|
|
169
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => clearInterval(id)));
|
|
170
|
+
} else {
|
|
171
|
+
// Scheduled mode: no automatic ticking.
|
|
172
|
+
// External code drives this signal via SubscriptionRef.set(ref, value).
|
|
173
|
+
// The ref is already created -- caller controls it via ControllableSignal.
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case 'media': {
|
|
178
|
+
if (typeof globalThis.window === 'undefined') return;
|
|
179
|
+
const mql = window.matchMedia(source.query);
|
|
180
|
+
const handler = (e: MediaQueryListEvent) => {
|
|
181
|
+
Effect.runSync(SubscriptionRef.set(ref, e.matches ? 1 : 0));
|
|
182
|
+
};
|
|
183
|
+
yield* Effect.acquireRelease(
|
|
184
|
+
Effect.sync(() => {
|
|
185
|
+
mql.addEventListener('change', handler);
|
|
186
|
+
}),
|
|
187
|
+
() =>
|
|
188
|
+
Effect.sync(() => {
|
|
189
|
+
mql.removeEventListener('change', handler);
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case 'custom':
|
|
195
|
+
// Custom signals are driven externally via Signal.custom() push API.
|
|
196
|
+
// No browser listener needed — the caller pushes values directly.
|
|
197
|
+
break;
|
|
198
|
+
case 'audio':
|
|
199
|
+
// Audio signals are driven externally via Signal.audio() / AVBridge.
|
|
200
|
+
// No browser listener needed — audio analysis pushes values on its own cadence.
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
yield* Effect.forkScoped(setupListener);
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
source,
|
|
209
|
+
current: SubscriptionRef.get(ref),
|
|
210
|
+
changes: SubscriptionRef.changes(ref),
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Create a controllable time signal for video rendering / scrubbing.
|
|
217
|
+
*
|
|
218
|
+
* External code drives the signal value via seek(); no automatic ticking.
|
|
219
|
+
* Supports pause/resume to temporarily ignore seek updates.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```ts
|
|
223
|
+
* import { Effect } from 'effect';
|
|
224
|
+
* import { Signal } from '@czap/core';
|
|
225
|
+
*
|
|
226
|
+
* const program = Effect.scoped(Effect.gen(function* () {
|
|
227
|
+
* const ctrl = yield* Signal.controllable();
|
|
228
|
+
* yield* ctrl.seek(1500);
|
|
229
|
+
* const t = yield* ctrl.current;
|
|
230
|
+
* // t === 1500
|
|
231
|
+
* yield* ctrl.pause();
|
|
232
|
+
* yield* ctrl.seek(2000); // ignored while paused
|
|
233
|
+
* }));
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
function _controllable(): Effect.Effect<ControllableSignalShape<number>, never, Scope.Scope> {
|
|
237
|
+
return Effect.gen(function* () {
|
|
238
|
+
const ref = yield* SubscriptionRef.make(0);
|
|
239
|
+
const pausedRef = yield* Ref.make(false);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
source: { type: 'time' as const, mode: 'scheduled' as const },
|
|
243
|
+
current: SubscriptionRef.get(ref),
|
|
244
|
+
changes: SubscriptionRef.changes(ref),
|
|
245
|
+
seek: (to: number) =>
|
|
246
|
+
Effect.gen(function* () {
|
|
247
|
+
const paused = yield* Ref.get(pausedRef);
|
|
248
|
+
if (!paused) {
|
|
249
|
+
yield* SubscriptionRef.set(ref, to);
|
|
250
|
+
}
|
|
251
|
+
}),
|
|
252
|
+
pause: () => Ref.set(pausedRef, true),
|
|
253
|
+
resume: () => Ref.set(pausedRef, false),
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Audio signal
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
interface AudioSignalShape extends SignalShape<number> {
|
|
263
|
+
poll(): Effect.Effect<number>;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Create an audio signal backed by an AVBridge.
|
|
268
|
+
*
|
|
269
|
+
* In 'sample' mode, returns the raw sample index. In 'normalized' mode,
|
|
270
|
+
* returns a 0..1 progress value based on totalDurationSec. Call `.poll()`
|
|
271
|
+
* to read the latest sample from the bridge and update the signal.
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```ts
|
|
275
|
+
* import { Effect } from 'effect';
|
|
276
|
+
* import { Signal } from '@czap/core';
|
|
277
|
+
*
|
|
278
|
+
* const program = Effect.scoped(Effect.gen(function* () {
|
|
279
|
+
* const audioSig = yield* Signal.audio(bridge, 'normalized', 120);
|
|
280
|
+
* const progress = yield* audioSig.poll();
|
|
281
|
+
* // progress is a number between 0 and 1
|
|
282
|
+
* }));
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
function _audio(
|
|
286
|
+
bridge: AVBridge.Shape,
|
|
287
|
+
mode: 'sample' | 'normalized' = 'sample',
|
|
288
|
+
totalDurationSec?: number,
|
|
289
|
+
): Effect.Effect<AudioSignalShape, never, Scope.Scope> {
|
|
290
|
+
return Effect.gen(function* () {
|
|
291
|
+
const ref = yield* SubscriptionRef.make(0);
|
|
292
|
+
|
|
293
|
+
const poll = () =>
|
|
294
|
+
Effect.gen(function* () {
|
|
295
|
+
const sample = bridge.getCurrentSample();
|
|
296
|
+
let value: number;
|
|
297
|
+
if (mode === 'normalized' && totalDurationSec !== undefined && totalDurationSec > 0) {
|
|
298
|
+
const totalSamples = totalDurationSec * bridge.sampleRate;
|
|
299
|
+
value = Math.min(sample / totalSamples, 1);
|
|
300
|
+
} else {
|
|
301
|
+
value = sample;
|
|
302
|
+
}
|
|
303
|
+
yield* SubscriptionRef.set(ref, value);
|
|
304
|
+
return value;
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
source: { type: 'audio' as const, mode } as const,
|
|
309
|
+
current: SubscriptionRef.get(ref),
|
|
310
|
+
changes: SubscriptionRef.changes(ref),
|
|
311
|
+
poll: () => poll(),
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Signal namespace -- live data feeds from the browser environment.
|
|
318
|
+
*
|
|
319
|
+
* Create reactive signals from viewport, scroll, pointer, time, media query,
|
|
320
|
+
* audio, or custom sources. Each signal provides `.current` and `.changes`
|
|
321
|
+
* backed by Effect's SubscriptionRef. Scoped for automatic listener cleanup.
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```ts
|
|
325
|
+
* import { Effect } from 'effect';
|
|
326
|
+
* import { Signal } from '@czap/core';
|
|
327
|
+
*
|
|
328
|
+
* const program = Effect.scoped(Effect.gen(function* () {
|
|
329
|
+
* const viewport = yield* Signal.make({ type: 'viewport', axis: 'width' });
|
|
330
|
+
* const width = yield* viewport.current;
|
|
331
|
+
* const ctrl = yield* Signal.controllable();
|
|
332
|
+
* yield* ctrl.seek(500);
|
|
333
|
+
* }));
|
|
334
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
export const Signal = { make: _make, controllable: _controllable, audio: _audio };
|
|
337
|
+
|
|
338
|
+
export declare namespace Signal {
|
|
339
|
+
/** Structural shape of a passive {@link Signal}: `source` + `current` + `changes`. */
|
|
340
|
+
export type Shape<T> = SignalShape<T>;
|
|
341
|
+
/** Structural shape of a seekable, pausable signal — e.g. driven by Remotion or a scrub UI. */
|
|
342
|
+
export type Controllable<T> = ControllableSignalShape<T>;
|
|
343
|
+
/** Structural shape of an audio-sourced signal backed by an {@link AVBridge}. */
|
|
344
|
+
export type Audio = AudioSignalShape;
|
|
345
|
+
}
|