@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/fnv.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FNV-1a hash utility for content addressing.
|
|
3
|
+
*
|
|
4
|
+
* Shared implementation used by Boundary, Token, Style, Theme, Component,
|
|
5
|
+
* and GenFrame modules. Produces `fnv1a:XXXXXXXX` ContentAddress values.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ContentAddress } from './brands.js';
|
|
11
|
+
import { ContentAddress as mkContentAddress } from './brands.js';
|
|
12
|
+
|
|
13
|
+
/** FNV-1a hash of a string, returned as a ContentAddress. */
|
|
14
|
+
export function fnv1a(str: string): ContentAddress {
|
|
15
|
+
let h = 0x811c9dc5;
|
|
16
|
+
for (let i = 0; i < str.length; i++) {
|
|
17
|
+
h ^= str.charCodeAt(i);
|
|
18
|
+
h = Math.imul(h, 0x01000193);
|
|
19
|
+
}
|
|
20
|
+
return mkContentAddress(`fnv1a:${(h >>> 0).toString(16).padStart(8, '0')}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** FNV-1a hash of raw bytes, returned as a ContentAddress. */
|
|
24
|
+
export function fnv1aBytes(bytes: Uint8Array): ContentAddress {
|
|
25
|
+
let h = 0x811c9dc5;
|
|
26
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
27
|
+
h ^= bytes[i]!;
|
|
28
|
+
h = Math.imul(h, 0x01000193);
|
|
29
|
+
}
|
|
30
|
+
return mkContentAddress(`fnv1a:${(h >>> 0).toString(16).padStart(8, '0')}`);
|
|
31
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FrameBudget -- rAF priority lanes for frame budget management.
|
|
3
|
+
*
|
|
4
|
+
* Tracks remaining frame budget per animation frame and
|
|
5
|
+
* schedules work by priority: `critical > high > low > idle`.
|
|
6
|
+
*
|
|
7
|
+
* Hot path methods (remaining, canRun, scheduleSync) are plain JS.
|
|
8
|
+
* Effect is used only for resource lifecycle (rAF cleanup) and
|
|
9
|
+
* backwards-compatible schedule() wrapper.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Scope } from 'effect';
|
|
15
|
+
import { Effect } from 'effect';
|
|
16
|
+
import { DEFAULT_TARGET_FPS, MS_PER_SEC } from './defaults.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Frame-budget priority lane in descending urgency. `critical` always runs;
|
|
20
|
+
* `high` / `low` / `idle` gate based on the milliseconds remaining in the
|
|
21
|
+
* current frame.
|
|
22
|
+
*/
|
|
23
|
+
export type Priority = 'critical' | 'high' | 'low' | 'idle';
|
|
24
|
+
|
|
25
|
+
// ms budget per priority lane within a 16ms frame (critical=0 runs first, high=2ms, low=6ms, idle=12ms)
|
|
26
|
+
const PRIORITY_THRESHOLDS: Record<Priority, number> = {
|
|
27
|
+
critical: 0,
|
|
28
|
+
high: 2,
|
|
29
|
+
low: 6,
|
|
30
|
+
idle: 12,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
interface FrameBudgetShape {
|
|
34
|
+
remaining(): number;
|
|
35
|
+
canRun(priority: Priority): boolean;
|
|
36
|
+
/** Synchronous scheduler for hot paths — no Effect overhead. */
|
|
37
|
+
scheduleSync<A>(priority: Priority, task: () => A): A | null;
|
|
38
|
+
schedule<A>(priority: Priority, task: Effect.Effect<A>): Effect.Effect<A | null>;
|
|
39
|
+
readonly fps: Effect.Effect<number>;
|
|
40
|
+
/** Synchronous FPS accessor for hot paths. */
|
|
41
|
+
readonly fpsSync: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates a FrameBudget tracker tied to rAF, with priority-based scheduling.
|
|
46
|
+
* Critical tasks always run; lower priorities are deferred if budget is exhausted.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* const program = Effect.scoped(Effect.gen(function* () {
|
|
51
|
+
* const budget = yield* FrameBudget.make({ targetFps: 60 });
|
|
52
|
+
* const remaining = budget.remaining(); // ms left in this frame
|
|
53
|
+
* const canAnimate = budget.canRun('high'); // true if enough budget
|
|
54
|
+
* const result = yield* budget.schedule('low', Effect.succeed('done'));
|
|
55
|
+
* // result is 'done' if budget permits, null otherwise
|
|
56
|
+
* }));
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
function _make(config?: { targetFps?: number }): Effect.Effect<FrameBudgetShape, never, Scope.Scope> {
|
|
60
|
+
const targetFps = config?.targetFps ?? DEFAULT_TARGET_FPS;
|
|
61
|
+
if (targetFps <= 0 || !Number.isFinite(targetFps)) {
|
|
62
|
+
throw new RangeError(`FrameBudget.make: targetFps must be a positive finite number, got ${targetFps}`);
|
|
63
|
+
}
|
|
64
|
+
const frameBudgetMs = MS_PER_SEC / targetFps;
|
|
65
|
+
|
|
66
|
+
return Effect.gen(function* () {
|
|
67
|
+
let frameStart = typeof performance !== 'undefined' ? performance.now() : 0;
|
|
68
|
+
let currentFps = targetFps;
|
|
69
|
+
let lastFrameTime = typeof performance !== 'undefined' ? performance.now() : 0;
|
|
70
|
+
let frameCount = 0;
|
|
71
|
+
let fpsAccum = 0;
|
|
72
|
+
|
|
73
|
+
if (typeof requestAnimationFrame !== 'undefined') {
|
|
74
|
+
const tick = (now: number) => {
|
|
75
|
+
frameStart = now;
|
|
76
|
+
frameCount++;
|
|
77
|
+
fpsAccum += now - lastFrameTime;
|
|
78
|
+
lastFrameTime = now;
|
|
79
|
+
if (fpsAccum >= MS_PER_SEC) {
|
|
80
|
+
currentFps = Math.round((frameCount * MS_PER_SEC) / fpsAccum);
|
|
81
|
+
frameCount = 0;
|
|
82
|
+
fpsAccum %= MS_PER_SEC;
|
|
83
|
+
}
|
|
84
|
+
rafId = requestAnimationFrame(tick);
|
|
85
|
+
};
|
|
86
|
+
let rafId = requestAnimationFrame(tick);
|
|
87
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => cancelAnimationFrame(rafId)));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const budget: FrameBudgetShape = {
|
|
91
|
+
remaining(): number {
|
|
92
|
+
if (typeof performance === 'undefined') return frameBudgetMs;
|
|
93
|
+
return Math.max(0, frameBudgetMs - (performance.now() - frameStart));
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
canRun(priority: Priority): boolean {
|
|
97
|
+
const rem = budget.remaining();
|
|
98
|
+
return rem >= PRIORITY_THRESHOLDS[priority]!;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
scheduleSync<A>(priority: Priority, task: () => A): A | null {
|
|
102
|
+
if (budget.canRun(priority) || priority === 'critical') {
|
|
103
|
+
return task();
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
schedule<A>(priority: Priority, task: Effect.Effect<A>): Effect.Effect<A | null> {
|
|
109
|
+
return Effect.gen(function* () {
|
|
110
|
+
if (priority === 'critical' || budget.canRun(priority)) {
|
|
111
|
+
return yield* task;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
fps: Effect.sync(() => currentFps),
|
|
118
|
+
|
|
119
|
+
get fpsSync(): number {
|
|
120
|
+
return currentFps;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return budget;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* FrameBudget -- rAF-based frame budget manager with priority lanes.
|
|
130
|
+
* Tracks remaining time per animation frame and gates work by priority:
|
|
131
|
+
* `critical` (always runs) `> high > low > idle`.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* const program = Effect.scoped(Effect.gen(function* () {
|
|
136
|
+
* const budget = yield* FrameBudget.make({ targetFps: 60 });
|
|
137
|
+
* if (budget.canRun('high')) {
|
|
138
|
+
* yield* budget.schedule('high', Effect.succeed('rendered'));
|
|
139
|
+
* }
|
|
140
|
+
* const fps = yield* budget.fps; // current measured FPS
|
|
141
|
+
* }));
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export const FrameBudget = { make: _make };
|
|
145
|
+
|
|
146
|
+
export declare namespace FrameBudget {
|
|
147
|
+
/** Structural shape of a {@link FrameBudget} instance — `canRun`, `schedule`, `remaining`, `fps`. */
|
|
148
|
+
export type Shape = FrameBudgetShape;
|
|
149
|
+
}
|
package/src/gen-frame.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenFrame -- generative UI frame scheduler.
|
|
3
|
+
*
|
|
4
|
+
* Fixed-step scheduler producing "UI frames" at configurable fps
|
|
5
|
+
* from token buffer. Each frame classified as keyframe (I-frame),
|
|
6
|
+
* delta (P-frame), or interpolated (B-frame).
|
|
7
|
+
*
|
|
8
|
+
* Integrates with FrameBudget for priority scheduling and receipt
|
|
9
|
+
* chain for disconnect-resilient generative UI state.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ContentAddress } from './brands.js';
|
|
15
|
+
import type { TokenBuffer } from './token-buffer.js';
|
|
16
|
+
import { fnv1a } from './fnv.js';
|
|
17
|
+
import type { UIQualityTier } from './ui-quality.js';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// UIFrame
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Classification of a {@link UIFrame} in the generative-UI pipeline, analogous to
|
|
25
|
+
* I/P/B frames in video: `keyframe` replaces, `delta` patches, `interpolated`
|
|
26
|
+
* keeps the DOM still and animates via CSS only.
|
|
27
|
+
*/
|
|
28
|
+
export type FrameType = 'keyframe' | 'delta' | 'interpolated';
|
|
29
|
+
|
|
30
|
+
/** How a {@link UIFrame} is applied to the DOM: full replace, patch, or CSS-only motion. */
|
|
31
|
+
export type MorphStrategy = 'replace' | 'patch' | 'css-only';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A single frame emitted by the {@link GenFrame} scheduler — the unit of work
|
|
35
|
+
* the DOM runtime consumes. Carries the drained tokens, its classification,
|
|
36
|
+
* the quality tier that produced it, and a content-addressed receipt for
|
|
37
|
+
* disconnect-resilient replay.
|
|
38
|
+
*/
|
|
39
|
+
export interface UIFrame {
|
|
40
|
+
readonly type: FrameType;
|
|
41
|
+
readonly tokens: readonly string[];
|
|
42
|
+
readonly qualityTier: UIQualityTier;
|
|
43
|
+
readonly morphStrategy: MorphStrategy;
|
|
44
|
+
readonly timestamp: number;
|
|
45
|
+
readonly receiptId: ContentAddress;
|
|
46
|
+
readonly bufferPosition: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Gap resolution
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Recovery plan returned by {@link GenFrame.resolveGap} when a stream disconnects:
|
|
55
|
+
* resume from a buffer position, replay cached frames, request a full restart,
|
|
56
|
+
* or do nothing.
|
|
57
|
+
*/
|
|
58
|
+
export type GapStrategy =
|
|
59
|
+
| { readonly type: 'resume'; readonly bufferPosition: number }
|
|
60
|
+
| { readonly type: 'replay'; readonly frames: readonly UIFrame[] }
|
|
61
|
+
| { readonly type: 're-request'; readonly fromScratch: true }
|
|
62
|
+
| { readonly type: 'noop' };
|
|
63
|
+
|
|
64
|
+
/** Transport-layer snapshot indicating whether the stream can resume from its last event. */
|
|
65
|
+
export interface ResumptionInfo {
|
|
66
|
+
readonly canResume: boolean;
|
|
67
|
+
readonly lastEventId?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Accessor bundle that exposes the receipt chain to {@link GenFrame.resolveGap}. */
|
|
71
|
+
export interface ReceiptChainInfo {
|
|
72
|
+
readonly hasFramesAfter: (receiptId: ContentAddress | null) => boolean;
|
|
73
|
+
readonly getFramesAfter: (receiptId: ContentAddress | null) => readonly UIFrame[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveGap(
|
|
77
|
+
lastAckReceiptId: ContentAddress | null,
|
|
78
|
+
currentStreamPosition: number,
|
|
79
|
+
receiptChain: ReceiptChainInfo,
|
|
80
|
+
resumptionState: ResumptionInfo,
|
|
81
|
+
): GapStrategy {
|
|
82
|
+
// 1. Can the stream resume?
|
|
83
|
+
if (resumptionState.canResume) {
|
|
84
|
+
return { type: 'resume', bufferPosition: currentStreamPosition };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 2. Do we have cached frames in the receipt chain?
|
|
88
|
+
if (receiptChain.hasFramesAfter(lastAckReceiptId)) {
|
|
89
|
+
const frames = receiptChain.getFramesAfter(lastAckReceiptId);
|
|
90
|
+
if (frames.length > 0) {
|
|
91
|
+
return { type: 'replay', frames };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 3. Neither available — full re-request
|
|
96
|
+
return { type: 're-request', fromScratch: true };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Frame scheduler
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
interface GenFrameConfig {
|
|
104
|
+
readonly fps?: number;
|
|
105
|
+
readonly tokenBuffer: TokenBuffer.Shape<string>;
|
|
106
|
+
readonly getQualityTier: () => UIQualityTier;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface GenFrameSchedulerShape {
|
|
110
|
+
tick(): UIFrame | null;
|
|
111
|
+
readonly frameCount: number;
|
|
112
|
+
readonly lastFrame: UIFrame | null;
|
|
113
|
+
markKeyframe(): void;
|
|
114
|
+
reset(): void;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// FNV-1a for receipt IDs — delegated to shared fnv.ts (see fnv.ts)
|
|
118
|
+
|
|
119
|
+
function _make(config: GenFrameConfig): GenFrameSchedulerShape {
|
|
120
|
+
const { tokenBuffer, getQualityTier } = config;
|
|
121
|
+
|
|
122
|
+
let frameCount = 0;
|
|
123
|
+
let lastFrame: UIFrame | null = null;
|
|
124
|
+
let lastQualityTier: UIQualityTier | null = null;
|
|
125
|
+
let forceKeyframe = true; // First frame is always a keyframe
|
|
126
|
+
let totalTokensDrained = 0;
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
tick(): UIFrame | null {
|
|
130
|
+
const tier = getQualityTier();
|
|
131
|
+
const tokens = tokenBuffer.drain(32); // Drain up to 32 tokens per frame
|
|
132
|
+
|
|
133
|
+
// Determine frame type
|
|
134
|
+
let type: FrameType;
|
|
135
|
+
if (forceKeyframe || lastQualityTier !== tier) {
|
|
136
|
+
type = 'keyframe';
|
|
137
|
+
forceKeyframe = false;
|
|
138
|
+
} else if (tokens.length === 0) {
|
|
139
|
+
// Stall — produce interpolated frame (CSS-only, no DOM mutation)
|
|
140
|
+
if (tokenBuffer.isStalled) {
|
|
141
|
+
type = 'interpolated';
|
|
142
|
+
} else {
|
|
143
|
+
// No tokens but not stalled — skip frame
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
type = 'delta';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Determine morph strategy
|
|
151
|
+
let morphStrategy: MorphStrategy;
|
|
152
|
+
switch (type) {
|
|
153
|
+
case 'keyframe':
|
|
154
|
+
morphStrategy = 'replace';
|
|
155
|
+
break;
|
|
156
|
+
case 'interpolated':
|
|
157
|
+
morphStrategy = 'css-only';
|
|
158
|
+
break;
|
|
159
|
+
default:
|
|
160
|
+
morphStrategy = 'patch';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
totalTokensDrained += tokens.length;
|
|
164
|
+
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
165
|
+
|
|
166
|
+
// Generate receipt ID from frame content
|
|
167
|
+
const receiptId = fnv1a(`${frameCount}:${now}:${tokens.join('')}`);
|
|
168
|
+
|
|
169
|
+
const frame: UIFrame = {
|
|
170
|
+
type,
|
|
171
|
+
tokens,
|
|
172
|
+
qualityTier: tier,
|
|
173
|
+
morphStrategy,
|
|
174
|
+
timestamp: now,
|
|
175
|
+
receiptId,
|
|
176
|
+
bufferPosition: totalTokensDrained,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
lastFrame = frame;
|
|
180
|
+
lastQualityTier = tier;
|
|
181
|
+
frameCount++;
|
|
182
|
+
|
|
183
|
+
return frame;
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
get frameCount(): number {
|
|
187
|
+
return frameCount;
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
get lastFrame(): UIFrame | null {
|
|
191
|
+
return lastFrame;
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
markKeyframe(): void {
|
|
195
|
+
forceKeyframe = true;
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
reset(): void {
|
|
199
|
+
frameCount = 0;
|
|
200
|
+
lastFrame = null;
|
|
201
|
+
lastQualityTier = null;
|
|
202
|
+
forceKeyframe = true;
|
|
203
|
+
totalTokensDrained = 0;
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Generative-UI frame scheduler namespace.
|
|
210
|
+
*
|
|
211
|
+
* Turns a bursty LLM token stream into evenly-paced frames the DOM runtime
|
|
212
|
+
* can apply without stalling, and resolves disconnect gaps using the receipt
|
|
213
|
+
* chain or transport resumption.
|
|
214
|
+
*/
|
|
215
|
+
export const GenFrame = {
|
|
216
|
+
/** Create a new fixed-step scheduler bound to a {@link TokenBuffer} and quality-tier probe. */
|
|
217
|
+
make: _make,
|
|
218
|
+
/** Pick a recovery {@link GapStrategy} after a stream disconnect. */
|
|
219
|
+
resolveGap,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export declare namespace GenFrame {
|
|
223
|
+
/** Structural shape of a scheduler instance returned by {@link GenFrame.make}. */
|
|
224
|
+
export type Shape = GenFrameSchedulerShape;
|
|
225
|
+
/** Configuration accepted by {@link GenFrame.make}. */
|
|
226
|
+
export type Config = GenFrameConfig;
|
|
227
|
+
/** Alias for {@link UIFrame}. */
|
|
228
|
+
export type Frame = UIFrame;
|
|
229
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* arbitrary-from-schema — derive a `fast-check` arbitrary from an Effect
|
|
3
|
+
* `Schema.Codec<T>`. Used by the harness templates so generated property
|
|
4
|
+
* tests feed real, schema-conformant inputs into capsule run handlers.
|
|
5
|
+
*
|
|
6
|
+
* Coverage: scalars (String, Number, Boolean, BigInt), Literal,
|
|
7
|
+
* Null/Undefined/Void, Unknown/Any, ObjectKeyword, Enum, Union, Array
|
|
8
|
+
* (Schema.Array + fixed Tuple + NonEmptyArray-style elements+rest),
|
|
9
|
+
* TypeLiteral (Struct with optional property signatures), Suspend,
|
|
10
|
+
* Declaration (Date specifically; throws for other declarations), and
|
|
11
|
+
* AST-level `checks` (Filter / FilterGroup) which model refinements
|
|
12
|
+
* such as `Schema.NonEmptyString` and `Schema.minLength(n)` — these
|
|
13
|
+
* post-filter the underlying arbitrary by running each Filter's
|
|
14
|
+
* predicate.
|
|
15
|
+
*
|
|
16
|
+
* KNOWN GAPS — these AST nodes throw `UnsupportedSchemaError` and the
|
|
17
|
+
* harness falls back to `it.skip` rather than a vacuous test:
|
|
18
|
+
* - Transformation (Schema.transform, Schema.compose chains)
|
|
19
|
+
* - TemplateLiteral (Schema.TemplateLiteral)
|
|
20
|
+
* - Declaration for non-Date opaque types (e.g. Uint8Array)
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
import { Effect } from 'effect';
|
|
25
|
+
import type { Schema, SchemaAST } from 'effect';
|
|
26
|
+
import * as fc from 'fast-check';
|
|
27
|
+
|
|
28
|
+
/** Error thrown when an AST node has no supported arbitrary mapping. */
|
|
29
|
+
export class UnsupportedSchemaError extends Error {
|
|
30
|
+
readonly _tag = 'UnsupportedSchemaError';
|
|
31
|
+
readonly nodeTag: string;
|
|
32
|
+
constructor(nodeTag: string, hint?: string) {
|
|
33
|
+
super(`arbitrary-from-schema: AST node "${nodeTag}" is not supported${hint ? ` (${hint})` : ''}`);
|
|
34
|
+
this.nodeTag = nodeTag;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Apply post-type-match `checks` (Filter / FilterGroup) declared on the
|
|
40
|
+
* AST node to the produced arbitrary. Each Filter's `run` returns
|
|
41
|
+
* `Issue | undefined`; `undefined` means the input passed. We compose all
|
|
42
|
+
* checks and `.filter` the arbitrary so only conforming samples survive.
|
|
43
|
+
*
|
|
44
|
+
* fast-check throws if the filter rejection rate exceeds ~10%. For
|
|
45
|
+
* common refinements (NonEmptyString, minLength) the underlying
|
|
46
|
+
* arbitrary already biases toward populated values so rejection stays
|
|
47
|
+
* well below the threshold.
|
|
48
|
+
*/
|
|
49
|
+
function _applyChecks(ast: SchemaAST.AST, arb: fc.Arbitrary<unknown>): fc.Arbitrary<unknown> {
|
|
50
|
+
const checks = ast.checks;
|
|
51
|
+
if (checks === undefined || checks.length === 0) return arb;
|
|
52
|
+
return arb.filter((sample) => {
|
|
53
|
+
for (const check of checks) {
|
|
54
|
+
if (check._tag === 'Filter') {
|
|
55
|
+
// ParseOptions is opaque — pass an empty object; the runtime
|
|
56
|
+
// tolerates missing fields for filter execution.
|
|
57
|
+
const issue = (check as SchemaAST.Filter<unknown>).run(sample, ast, {} as SchemaAST.ParseOptions);
|
|
58
|
+
if (issue !== undefined) return false;
|
|
59
|
+
} else if (check._tag === 'FilterGroup') {
|
|
60
|
+
const group = check as SchemaAST.FilterGroup<unknown>;
|
|
61
|
+
for (const inner of group.checks) {
|
|
62
|
+
if (inner._tag === 'Filter') {
|
|
63
|
+
const issue = (inner as SchemaAST.Filter<unknown>).run(sample, ast, {} as SchemaAST.ParseOptions);
|
|
64
|
+
if (issue !== undefined) return false;
|
|
65
|
+
}
|
|
66
|
+
// Nested FilterGroup is theoretically possible but rare;
|
|
67
|
+
// ignore for now and let the outer test catch failures.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Probe a `Declaration` node to determine the JavaScript class it accepts.
|
|
77
|
+
* We attempt to construct a sentinel value and see whether the node's
|
|
78
|
+
* `run` parser accepts it. If yes, return a fast-check arbitrary that
|
|
79
|
+
* produces values of that shape. Otherwise throw.
|
|
80
|
+
*
|
|
81
|
+
* Currently supports `Date`. Add new probes here when production
|
|
82
|
+
* capsules require them.
|
|
83
|
+
*/
|
|
84
|
+
function _arbitraryForDeclaration(ast: SchemaAST.Declaration): fc.Arbitrary<unknown> {
|
|
85
|
+
const parser = ast.run(ast.typeParameters);
|
|
86
|
+
// Probe with `new Date()` — the most common Declaration in production.
|
|
87
|
+
const probeDate = new Date();
|
|
88
|
+
// The parser returns an Effect; we synchronously inspect via the
|
|
89
|
+
// runtime sync path. If it succeeds, the Declaration accepts Date.
|
|
90
|
+
// We avoid pulling in the full Effect runtime here — a try/catch
|
|
91
|
+
// around the parser's first sync step is enough for the probe.
|
|
92
|
+
let acceptsDate = false;
|
|
93
|
+
try {
|
|
94
|
+
const out = parser(probeDate, ast, {} as SchemaAST.ParseOptions);
|
|
95
|
+
// The Effect returned by `out` succeeds synchronously when the
|
|
96
|
+
// input matches; failure surfaces as an Issue. We use Effect's
|
|
97
|
+
// `runSyncExit` to inspect the success/failure tag without
|
|
98
|
+
// throwing on parse failures.
|
|
99
|
+
const exit = Effect.runSyncExit(out as never);
|
|
100
|
+
acceptsDate = exit._tag === 'Success';
|
|
101
|
+
} catch {
|
|
102
|
+
acceptsDate = false;
|
|
103
|
+
}
|
|
104
|
+
if (acceptsDate) return fc.date();
|
|
105
|
+
throw new UnsupportedSchemaError('Declaration', 'opaque user-defined type — only Date is currently probed');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function walk(ast: SchemaAST.AST): fc.Arbitrary<unknown> {
|
|
109
|
+
let arb: fc.Arbitrary<unknown>;
|
|
110
|
+
switch (ast._tag) {
|
|
111
|
+
case 'String':
|
|
112
|
+
arb = fc.string();
|
|
113
|
+
break;
|
|
114
|
+
case 'Number':
|
|
115
|
+
// Integer is safer than float — avoids NaN/Infinity which trip
|
|
116
|
+
// most user-defined invariants. Capsules that need floats can
|
|
117
|
+
// refine via filter checks (not yet handled here).
|
|
118
|
+
arb = fc.integer();
|
|
119
|
+
break;
|
|
120
|
+
case 'Boolean':
|
|
121
|
+
arb = fc.boolean();
|
|
122
|
+
break;
|
|
123
|
+
case 'BigInt':
|
|
124
|
+
arb = fc.bigInt();
|
|
125
|
+
break;
|
|
126
|
+
case 'Literal':
|
|
127
|
+
arb = fc.constant((ast as SchemaAST.Literal).literal);
|
|
128
|
+
break;
|
|
129
|
+
case 'Null':
|
|
130
|
+
arb = fc.constant(null);
|
|
131
|
+
break;
|
|
132
|
+
case 'Undefined':
|
|
133
|
+
case 'Void':
|
|
134
|
+
arb = fc.constant(undefined);
|
|
135
|
+
break;
|
|
136
|
+
case 'Unknown':
|
|
137
|
+
case 'Any':
|
|
138
|
+
arb = fc.anything();
|
|
139
|
+
break;
|
|
140
|
+
case 'ObjectKeyword':
|
|
141
|
+
arb = fc.object();
|
|
142
|
+
break;
|
|
143
|
+
case 'Enum': {
|
|
144
|
+
const enums = (ast as SchemaAST.Enum).enums;
|
|
145
|
+
if (enums.length === 0) {
|
|
146
|
+
throw new UnsupportedSchemaError('Enum', 'empty enum');
|
|
147
|
+
}
|
|
148
|
+
arb = fc.constantFrom(...enums.map(([, v]) => v));
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case 'Union': {
|
|
152
|
+
const u = ast as SchemaAST.Union;
|
|
153
|
+
if (u.types.length === 0) {
|
|
154
|
+
throw new UnsupportedSchemaError('Union', 'empty union');
|
|
155
|
+
}
|
|
156
|
+
const arbs = u.types.map(walk);
|
|
157
|
+
// fc.oneof accepts an arbitraries-array as variadic args
|
|
158
|
+
arb = fc.oneof(...arbs);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case 'Arrays': {
|
|
162
|
+
const a = ast as SchemaAST.Arrays;
|
|
163
|
+
// Common case: Schema.Array(T) yields elements=[], rest=[T]
|
|
164
|
+
if (a.elements.length === 0 && a.rest.length === 1) {
|
|
165
|
+
const elem = a.rest[0];
|
|
166
|
+
if (elem === undefined) {
|
|
167
|
+
throw new UnsupportedSchemaError('Arrays', 'rest[0] missing');
|
|
168
|
+
}
|
|
169
|
+
arb = fc.array(walk(elem), { maxLength: 8 });
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
// Fixed tuple
|
|
173
|
+
if (a.rest.length === 0 && a.elements.length > 0) {
|
|
174
|
+
const elemArbs = a.elements.map(walk);
|
|
175
|
+
arb = fc.tuple(...elemArbs);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
// Mixed: required leading element(s) + rest tail. NonEmptyArray
|
|
179
|
+
// surfaces here as elements=[T], rest=[T] — generate the leading
|
|
180
|
+
// tuple and append a variable-length tail of the same elem type.
|
|
181
|
+
if (a.elements.length > 0 && a.rest.length === 1) {
|
|
182
|
+
const headArbs = a.elements.map(walk);
|
|
183
|
+
const tailElem = a.rest[0];
|
|
184
|
+
if (tailElem === undefined) {
|
|
185
|
+
throw new UnsupportedSchemaError('Arrays', 'rest[0] missing');
|
|
186
|
+
}
|
|
187
|
+
const tailArb = fc.array(walk(tailElem), { maxLength: 7 });
|
|
188
|
+
arb = fc.tuple(fc.tuple(...headArbs), tailArb).map(([head, tail]) => [...head, ...tail]);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
throw new UnsupportedSchemaError(
|
|
192
|
+
'Arrays',
|
|
193
|
+
`unsupported tuple+rest shape (elements=${a.elements.length}, rest=${a.rest.length})`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
case 'Objects': {
|
|
197
|
+
const o = ast as SchemaAST.Objects;
|
|
198
|
+
if (o.indexSignatures.length > 0) {
|
|
199
|
+
throw new UnsupportedSchemaError('Objects', 'index signatures');
|
|
200
|
+
}
|
|
201
|
+
const required: Record<string, fc.Arbitrary<unknown>> = {};
|
|
202
|
+
const optional: Record<string, fc.Arbitrary<unknown>> = {};
|
|
203
|
+
for (const ps of o.propertySignatures) {
|
|
204
|
+
const key = String(ps.name);
|
|
205
|
+
const fieldArb = walk(ps.type);
|
|
206
|
+
const isOptional = ps.type.context?.isOptional === true;
|
|
207
|
+
if (isOptional) optional[key] = fieldArb;
|
|
208
|
+
else required[key] = fieldArb;
|
|
209
|
+
}
|
|
210
|
+
if (Object.keys(optional).length === 0) {
|
|
211
|
+
arb = fc.record(required);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
// fast-check supports `requiredKeys` to mark a subset as required —
|
|
215
|
+
// but the simpler, version-stable approach is to merge all keys and
|
|
216
|
+
// post-process: for each optional key, randomly drop it.
|
|
217
|
+
const allKeys = { ...required, ...optional };
|
|
218
|
+
arb = fc.record(allKeys).chain((rec) =>
|
|
219
|
+
fc.tuple(...Object.keys(optional).map(() => fc.boolean())).map((dropFlags) => {
|
|
220
|
+
const out: Record<string, unknown> = { ...rec };
|
|
221
|
+
const optKeys = Object.keys(optional);
|
|
222
|
+
for (let i = 0; i < optKeys.length; i++) {
|
|
223
|
+
if (dropFlags[i] === true) {
|
|
224
|
+
const k = optKeys[i];
|
|
225
|
+
if (k !== undefined) delete out[k];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
case 'Suspend': {
|
|
234
|
+
const s = ast as SchemaAST.Suspend;
|
|
235
|
+
// Resolve once; arbitrary depth control is left to fast-check defaults.
|
|
236
|
+
arb = walk(s.thunk());
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case 'Declaration':
|
|
240
|
+
arb = _arbitraryForDeclaration(ast as SchemaAST.Declaration);
|
|
241
|
+
break;
|
|
242
|
+
default:
|
|
243
|
+
throw new UnsupportedSchemaError(ast._tag);
|
|
244
|
+
}
|
|
245
|
+
return _applyChecks(ast, arb);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Walk a `Schema` AST and return a `fc.Arbitrary` that produces values
|
|
250
|
+
* structurally conforming to the schema. Throws
|
|
251
|
+
* {@link UnsupportedSchemaError} on AST nodes with no supported mapping.
|
|
252
|
+
*
|
|
253
|
+
* Accepts any `Schema.Schema<T>` (or `Codec`) — only `.ast` is read.
|
|
254
|
+
*/
|
|
255
|
+
function _schemaToArbitrary<T>(schema: Schema.Schema<T>): fc.Arbitrary<T> {
|
|
256
|
+
return walk(schema.ast) as fc.Arbitrary<T>;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Public namespace for the arbitrary-from-schema walker. */
|
|
260
|
+
export const ArbitraryFromSchema = {
|
|
261
|
+
fromSchema: _schemaToArbitrary,
|
|
262
|
+
} as const;
|
|
263
|
+
|
|
264
|
+
/** Convenience top-level export — most call sites use this directly. */
|
|
265
|
+
export const schemaToArbitrary = _schemaToArbitrary;
|
|
266
|
+
|
|
267
|
+
export declare namespace ArbitraryFromSchema {
|
|
268
|
+
/** The result type returned by {@link ArbitraryFromSchema.fromSchema}. */
|
|
269
|
+
export type Result<T> = fc.Arbitrary<T>;
|
|
270
|
+
}
|