@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/component.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ComponentDef -- adaptive component primitive for constraint-based rendering.
|
|
3
|
+
*
|
|
4
|
+
* A component binds a boundary, styles, and named slots into a single
|
|
5
|
+
* content-addressed unit. Content-addressed via FNV-1a.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ContentAddress } from './brands.js';
|
|
11
|
+
import type { Boundary } from './boundary.js';
|
|
12
|
+
import type { Style } from './style.js';
|
|
13
|
+
import { CanonicalCbor } from './cbor.js';
|
|
14
|
+
import { fnv1aBytes } from './fnv.js';
|
|
15
|
+
|
|
16
|
+
/** Per-slot configuration on a component — whether the slot must be provided, plus optional description. */
|
|
17
|
+
export interface SlotConfig {
|
|
18
|
+
readonly required: boolean;
|
|
19
|
+
readonly description?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ComponentDef<
|
|
23
|
+
B extends Boundary.Shape = Boundary.Shape,
|
|
24
|
+
SlotNames extends readonly string[] = readonly string[],
|
|
25
|
+
> {
|
|
26
|
+
readonly _tag: 'ComponentDef';
|
|
27
|
+
readonly _version: 1;
|
|
28
|
+
readonly id: ContentAddress;
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly boundary?: B;
|
|
31
|
+
readonly styles: Style.Shape<B>;
|
|
32
|
+
readonly slots: { readonly [K in SlotNames[number]]: SlotConfig };
|
|
33
|
+
readonly defaultSlot?: SlotNames[number];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ComponentFactory {
|
|
37
|
+
make<B extends Boundary.Shape, const SN extends readonly [string, ...string[]]>(config: {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly boundary?: B;
|
|
40
|
+
readonly styles: Style.Shape<B>;
|
|
41
|
+
readonly slots: { readonly [K in SN[number]]: SlotConfig };
|
|
42
|
+
readonly defaultSlot?: SN[number];
|
|
43
|
+
}): ComponentDef<B, SN>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function deterministicId<SlotNames extends readonly string[]>(
|
|
47
|
+
name: string,
|
|
48
|
+
boundaryId: string | undefined,
|
|
49
|
+
stylesId: string,
|
|
50
|
+
slots: { readonly [K in SlotNames[number]]: SlotConfig },
|
|
51
|
+
defaultSlot?: string,
|
|
52
|
+
): ContentAddress {
|
|
53
|
+
return fnv1aBytes(
|
|
54
|
+
CanonicalCbor.encode({
|
|
55
|
+
_tag: 'ComponentDef',
|
|
56
|
+
_version: 1,
|
|
57
|
+
name,
|
|
58
|
+
boundaryId: boundaryId ?? null,
|
|
59
|
+
stylesId,
|
|
60
|
+
slots,
|
|
61
|
+
defaultSlot: defaultSlot ?? null,
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Component — the content-addressed unit that binds a {@link Boundary}, a
|
|
68
|
+
* {@link Style}, and named slots into a single declaration compilers can
|
|
69
|
+
* target. The optional boundary gates style variants; the slots describe
|
|
70
|
+
* the consumer-facing API.
|
|
71
|
+
*/
|
|
72
|
+
export const Component: ComponentFactory = {
|
|
73
|
+
make<B extends Boundary.Shape, const SN extends readonly [string, ...string[]]>(config: {
|
|
74
|
+
readonly name: string;
|
|
75
|
+
readonly boundary?: B;
|
|
76
|
+
readonly styles: Style.Shape<B>;
|
|
77
|
+
readonly slots: { readonly [K in SN[number]]: SlotConfig };
|
|
78
|
+
readonly defaultSlot?: SN[number];
|
|
79
|
+
}): ComponentDef<B, SN> {
|
|
80
|
+
const id = deterministicId<SN>(config.name, config.boundary?.id, config.styles.id, config.slots, config.defaultSlot);
|
|
81
|
+
|
|
82
|
+
const def: ComponentDef<B, SN> = {
|
|
83
|
+
_tag: 'ComponentDef',
|
|
84
|
+
_version: 1,
|
|
85
|
+
id,
|
|
86
|
+
name: config.name,
|
|
87
|
+
...(config.boundary !== undefined ? { boundary: config.boundary } : {}),
|
|
88
|
+
styles: config.styles,
|
|
89
|
+
slots: config.slots,
|
|
90
|
+
...(config.defaultSlot !== undefined ? { defaultSlot: config.defaultSlot } : {}),
|
|
91
|
+
};
|
|
92
|
+
return Object.freeze(def);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export declare namespace Component {
|
|
97
|
+
/** Structural shape of a component definition, parameterized by its boundary and slot names. */
|
|
98
|
+
export type Shape<
|
|
99
|
+
B extends Boundary.Shape = Boundary.Shape,
|
|
100
|
+
SN extends readonly string[] = readonly string[],
|
|
101
|
+
> = ComponentDef<B, SN>;
|
|
102
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable -- ECS Composition over Existing Primitives
|
|
3
|
+
*
|
|
4
|
+
* Universal composition API leveraging existing deterministic primitives.
|
|
5
|
+
* Zero boilerplate, type-safe, content-addressed entity composition.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ContentAddress } from './brands.js';
|
|
11
|
+
import type { Token } from './token.js';
|
|
12
|
+
import type { Style } from './style.js';
|
|
13
|
+
import type { World } from './ecs.js';
|
|
14
|
+
import type { EntityId } from './ecs.js';
|
|
15
|
+
import { Token as TokenNS } from './token.js';
|
|
16
|
+
import { Style as StyleNS } from './style.js';
|
|
17
|
+
import { Boundary } from './boundary.js';
|
|
18
|
+
import { Part } from './ecs.js';
|
|
19
|
+
import { fnv1aBytes } from './fnv.js';
|
|
20
|
+
import { TypedRef } from './typed-ref.js';
|
|
21
|
+
import { Effect } from 'effect';
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Entity Composition Types
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Component map for a {@link ComposableEntity} — well-known slots for czap
|
|
29
|
+
* primitives plus arbitrary user-defined keys.
|
|
30
|
+
*/
|
|
31
|
+
export interface EntityComponents {
|
|
32
|
+
readonly boundary?: Boundary.Shape;
|
|
33
|
+
readonly token?: Token.Shape;
|
|
34
|
+
readonly style?: Style.Shape;
|
|
35
|
+
readonly [key: string]: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Content-addressed entity: the identity is an FNV-1a hash over its components,
|
|
40
|
+
* so two entities with structurally equal components share the same `id`.
|
|
41
|
+
*/
|
|
42
|
+
export interface ComposableEntity<T extends EntityComponents = EntityComponents> {
|
|
43
|
+
readonly id: ContentAddress;
|
|
44
|
+
readonly components: T;
|
|
45
|
+
readonly _tag: 'ComposableEntity';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Composable Factory
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
interface ComposableFactory {
|
|
53
|
+
make<T extends EntityComponents>(components: T): ComposableEntity<T>;
|
|
54
|
+
compose<T extends EntityComponents>(entity1: ComposableEntity<T>, entity2: ComposableEntity<T>): ComposableEntity<T>;
|
|
55
|
+
merge<T extends EntityComponents>(...entities: ComposableEntity<T>[]): ComposableEntity<T>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function makeEntityId(components: EntityComponents): ContentAddress {
|
|
59
|
+
const canonical = canonicalizeForAddress(components);
|
|
60
|
+
return fnv1aBytes(TypedRef.canonicalize(canonical));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function canonicalizeForAddress(value: unknown): unknown {
|
|
64
|
+
if (value === undefined) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
return value.map((entry) => {
|
|
74
|
+
const canonical = canonicalizeForAddress(entry);
|
|
75
|
+
return canonical === undefined ? null : canonical;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof value === 'object') {
|
|
80
|
+
const entries = Object.entries(value)
|
|
81
|
+
.filter(([, entry]) => entry !== undefined)
|
|
82
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
83
|
+
.map(([key, entry]) => [key, canonicalizeForAddress(entry)]);
|
|
84
|
+
|
|
85
|
+
return Object.fromEntries(entries);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return String(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _make<T extends EntityComponents>(components: T): ComposableEntity<T> {
|
|
92
|
+
const id = makeEntityId(components);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
id,
|
|
96
|
+
components,
|
|
97
|
+
_tag: 'ComposableEntity',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function _compose<T extends EntityComponents>(
|
|
102
|
+
entity1: ComposableEntity<T>,
|
|
103
|
+
entity2: ComposableEntity<T>,
|
|
104
|
+
): ComposableEntity<T> {
|
|
105
|
+
// Merge components with entity2 taking precedence
|
|
106
|
+
const merged = { ...entity1.components, ...entity2.components };
|
|
107
|
+
return _make(merged);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function _merge<T extends EntityComponents>(...entities: ComposableEntity<T>[]): ComposableEntity<T> {
|
|
111
|
+
if (entities.length === 0) {
|
|
112
|
+
throw new Error('Cannot merge zero entities');
|
|
113
|
+
}
|
|
114
|
+
const first = entities[0];
|
|
115
|
+
if (!first) {
|
|
116
|
+
throw new Error('First entity is undefined');
|
|
117
|
+
}
|
|
118
|
+
return entities.slice(1).reduce((acc, entity) => _compose(acc, entity), first);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// ECS Integration
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Convert a runtime `Map<string, unknown>` (from ECS query results) into a typed
|
|
127
|
+
* `Pick<Schema, K>`. The ECS query filters guarantee the required keys are present;
|
|
128
|
+
* this helper contains the one boundary cast where runtime shape joins the type lattice.
|
|
129
|
+
*/
|
|
130
|
+
function entriesToPick<Schema extends EntityComponents, K extends keyof Schema>(
|
|
131
|
+
components: ReadonlyMap<string, unknown>,
|
|
132
|
+
): Pick<Schema, K> {
|
|
133
|
+
return Object.fromEntries(components) as Pick<Schema, K>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface TypedComposableWorld<Schema extends EntityComponents = EntityComponents> {
|
|
137
|
+
spawn<T extends Schema>(components: T): Effect.Effect<ComposableEntity<T>>;
|
|
138
|
+
spawnWith<T extends Schema>(entity: ComposableEntity<T>): Effect.Effect<ComposableEntity<T>>;
|
|
139
|
+
query<K extends keyof Schema>(...componentTypes: K[]): Effect.Effect<readonly ComposableEntity<Pick<Schema, K>>[]>;
|
|
140
|
+
evaluate<T extends Schema>(
|
|
141
|
+
entity: ComposableEntity<T>,
|
|
142
|
+
input: Record<string, number>,
|
|
143
|
+
): Effect.Effect<Record<string, string>>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function makeComposableWorld<Schema extends EntityComponents = EntityComponents>(
|
|
147
|
+
world: World.Shape,
|
|
148
|
+
): TypedComposableWorld<Schema> {
|
|
149
|
+
// Mapping from ContentAddress to ECS EntityId for query reconstruction
|
|
150
|
+
const addressToEntityId = new Map<ContentAddress, EntityId>();
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
spawn<T extends Schema>(components: T): Effect.Effect<ComposableEntity<T>> {
|
|
154
|
+
return Effect.gen(function* () {
|
|
155
|
+
const entity = _make(components);
|
|
156
|
+
const ecsId = yield* world.spawn(components);
|
|
157
|
+
addressToEntityId.set(entity.id, ecsId);
|
|
158
|
+
return entity;
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
spawnWith<T extends Schema>(entity: ComposableEntity<T>): Effect.Effect<ComposableEntity<T>> {
|
|
163
|
+
return Effect.gen(function* () {
|
|
164
|
+
const ecsId = yield* world.spawn(entity.components);
|
|
165
|
+
addressToEntityId.set(entity.id, ecsId);
|
|
166
|
+
return entity;
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
query<K extends keyof Schema>(...componentTypes: K[]): Effect.Effect<readonly ComposableEntity<Pick<Schema, K>>[]> {
|
|
171
|
+
return Effect.gen(function* () {
|
|
172
|
+
const names = [...componentTypes].map((k) => String(k)).sort();
|
|
173
|
+
const entities = yield* world.query(...names);
|
|
174
|
+
return [...entities]
|
|
175
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
176
|
+
.map((entityShape) => {
|
|
177
|
+
// world.query guarantees entityShape.components contains at least the K keys
|
|
178
|
+
// that were queried for; convert the runtime Map<string, unknown> to the typed
|
|
179
|
+
// Pick<Schema, K> via a single contained cast (runtime shape is validated by
|
|
180
|
+
// the ECS query filter).
|
|
181
|
+
const components = entriesToPick<Schema, K>(entityShape.components);
|
|
182
|
+
return _make(components);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
evaluate<T extends Schema>(
|
|
188
|
+
entity: ComposableEntity<T>,
|
|
189
|
+
input: Record<string, number>,
|
|
190
|
+
): Effect.Effect<Record<string, string>> {
|
|
191
|
+
return Effect.gen(function* () {
|
|
192
|
+
const results: Record<string, string> = {};
|
|
193
|
+
|
|
194
|
+
// Evaluate boundary component: quantize continuous input to discrete state
|
|
195
|
+
let boundaryState: string | undefined;
|
|
196
|
+
if (entity.components.boundary) {
|
|
197
|
+
const boundary = entity.components.boundary;
|
|
198
|
+
const boundaryInput = input[boundary.input] ?? 0;
|
|
199
|
+
const state = Boundary.evaluate(boundary, boundaryInput);
|
|
200
|
+
results[boundary.input] = state;
|
|
201
|
+
boundaryState = state;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Evaluate token component: resolve axis values or fall back
|
|
205
|
+
if (entity.components.token) {
|
|
206
|
+
const token = entity.components.token;
|
|
207
|
+
// Build axis values from input keys. Token.tap expects string axis values,
|
|
208
|
+
// so we convert matching numeric inputs to strings.
|
|
209
|
+
const axisValues: Record<string, string> = {};
|
|
210
|
+
for (const axis of token.axes) {
|
|
211
|
+
if (axis in input) {
|
|
212
|
+
axisValues[axis] = String(input[axis]);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Use Token.tap for proper axis-key lookup with fallback
|
|
216
|
+
const resolved = TokenNS.tap(token, axisValues);
|
|
217
|
+
results[token.name] = String(resolved);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Evaluate style component: resolve properties for the current boundary state
|
|
221
|
+
if (entity.components.style) {
|
|
222
|
+
const style = entity.components.style;
|
|
223
|
+
const resolvedProps = StyleNS.tap(style, boundaryState);
|
|
224
|
+
for (const [prop, val] of Object.entries(resolvedProps)) {
|
|
225
|
+
results[prop] = val;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return results;
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Dense Store Integration
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
interface ComposableDenseStore {
|
|
240
|
+
create(name: string, capacity: number): Effect.Effect<Part.Dense>;
|
|
241
|
+
store<T extends EntityComponents>(entity: ComposableEntity<T>, value: number): Effect.Effect<void>;
|
|
242
|
+
retrieve<T extends EntityComponents>(entity: ComposableEntity<T>): Effect.Effect<number | undefined>;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function makeComposableDenseStore(world: World.Shape): ComposableDenseStore {
|
|
246
|
+
// Maintain a mapping from ContentAddress to ECS EntityId for dense store ops
|
|
247
|
+
const addressToEntityId = new Map<ContentAddress, EntityId>();
|
|
248
|
+
let denseStore: Part.Dense | undefined;
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
create(name: string, capacity: number): Effect.Effect<Part.Dense> {
|
|
252
|
+
return Effect.gen(function* () {
|
|
253
|
+
const store = Part.dense(name, capacity);
|
|
254
|
+
yield* world.addDenseStore(store);
|
|
255
|
+
denseStore = store;
|
|
256
|
+
return store;
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
store<T extends EntityComponents>(entity: ComposableEntity<T>, value: number): Effect.Effect<void> {
|
|
261
|
+
return Effect.gen(function* () {
|
|
262
|
+
if (!denseStore) {
|
|
263
|
+
throw new Error('No dense store created. Call create() first.');
|
|
264
|
+
}
|
|
265
|
+
// Ensure we have an ECS EntityId for this composable entity
|
|
266
|
+
let ecsId = addressToEntityId.get(entity.id);
|
|
267
|
+
if (!ecsId) {
|
|
268
|
+
// Spawn into the world to get an EntityId, then track mapping
|
|
269
|
+
ecsId = yield* world.spawn(entity.components);
|
|
270
|
+
addressToEntityId.set(entity.id, ecsId);
|
|
271
|
+
}
|
|
272
|
+
denseStore.set(ecsId, value);
|
|
273
|
+
});
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
retrieve<T extends EntityComponents>(entity: ComposableEntity<T>): Effect.Effect<number | undefined> {
|
|
277
|
+
return Effect.gen(function* () {
|
|
278
|
+
if (!denseStore) {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
const ecsId = addressToEntityId.get(entity.id);
|
|
282
|
+
if (!ecsId) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
return denseStore.get(ecsId);
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Exports
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Composable — content-addressed entity algebra over czap primitives.
|
|
297
|
+
*
|
|
298
|
+
* Build entities from a bag of components (boundaries, tokens, styles, …),
|
|
299
|
+
* merge them associatively via `Composable.compose` / `Composable.merge`, and
|
|
300
|
+
* rely on the content address to deduplicate structurally-equal entities.
|
|
301
|
+
*/
|
|
302
|
+
export const Composable: ComposableFactory = {
|
|
303
|
+
/** Content-address a component bag into a {@link ComposableEntity}. */
|
|
304
|
+
make: _make,
|
|
305
|
+
/** Pairwise merge — right-biased; produces a new entity with a fresh content address. */
|
|
306
|
+
compose: _compose,
|
|
307
|
+
/** Variadic `Composable.compose`. Throws if called with zero entities. */
|
|
308
|
+
merge: _merge,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Bridge between a raw ECS {@link World} and typed {@link ComposableEntity}
|
|
313
|
+
* operations (`spawn`, `query`, `evaluate`) plus a thin dense-store integration.
|
|
314
|
+
*/
|
|
315
|
+
export const ComposableWorld = {
|
|
316
|
+
/** Wrap a {@link World} with the typed composable-entity API. */
|
|
317
|
+
make: makeComposableWorld,
|
|
318
|
+
/** Build a dense-store bridge over a {@link World} for per-entity numeric data. */
|
|
319
|
+
dense: makeComposableDenseStore,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
export declare namespace ComposableWorld {
|
|
323
|
+
/** Structural shape of the typed world returned by {@link ComposableWorld.make}. */
|
|
324
|
+
export type Shape<Schema extends EntityComponents = EntityComponents> = TypedComposableWorld<Schema>;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Type exports -- keep legacy alias for backward compatibility
|
|
328
|
+
export type { TypedComposableWorld as ComposableWorldShape };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CompositorStatePool -- ring buffer of pre-allocated CompositeState objects.
|
|
3
|
+
*
|
|
4
|
+
* The compositor grabs one, writes into it, hands it to the renderer,
|
|
5
|
+
* which returns it after DOM application. Zero-allocation hot path.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CompositeState } from './compositor.js';
|
|
11
|
+
import { COMPOSITOR_POOL_CAP } from './defaults.js';
|
|
12
|
+
|
|
13
|
+
// Zero-allocation ring buffer: pre-allocated fixed-size pool with index-wrapping reads/writes. No GC pressure on hot path.
|
|
14
|
+
|
|
15
|
+
interface CompositorStatePoolShape {
|
|
16
|
+
acquire(): CompositeState;
|
|
17
|
+
release(state: CompositeState): void;
|
|
18
|
+
readonly size: number;
|
|
19
|
+
readonly available: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Mutable views into a CompositeState's fields. `CompositeState` declares
|
|
24
|
+
* its fields as `readonly Record<...>`, but the compositor and the pool
|
|
25
|
+
* both need to mutate those records in place for the zero-allocation
|
|
26
|
+
* hot path. This helper centralises the single cast that strips
|
|
27
|
+
* `readonly` so callers can stay type-safe without re-casting at each
|
|
28
|
+
* write site.
|
|
29
|
+
*/
|
|
30
|
+
export interface MutableCompositeStateViews {
|
|
31
|
+
readonly discrete: Record<string, string>;
|
|
32
|
+
readonly blend: Record<string, Record<string, number>>;
|
|
33
|
+
readonly css: Record<string, number | string>;
|
|
34
|
+
readonly glsl: Record<string, number>;
|
|
35
|
+
readonly aria: Record<string, string>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type MutableCompositeState = {
|
|
39
|
+
-readonly [K in keyof CompositeState]: CompositeState[K] extends Readonly<infer U> ? U : CompositeState[K];
|
|
40
|
+
} & {
|
|
41
|
+
outputs: {
|
|
42
|
+
-readonly [K in keyof CompositeState['outputs']]: CompositeState['outputs'][K];
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Expose the inner `Record<…>`s of a {@link CompositeState} as mutable views
|
|
48
|
+
* for the zero-allocation hot path. Single sanctioned site that strips
|
|
49
|
+
* `readonly` — do not cast at other call sites.
|
|
50
|
+
*/
|
|
51
|
+
export function accessCompositeState(state: CompositeState): MutableCompositeStateViews {
|
|
52
|
+
const mutable = state as MutableCompositeState;
|
|
53
|
+
return {
|
|
54
|
+
discrete: mutable.discrete,
|
|
55
|
+
blend: mutable.blend,
|
|
56
|
+
css: mutable.outputs.css,
|
|
57
|
+
glsl: mutable.outputs.glsl,
|
|
58
|
+
aria: mutable.outputs.aria,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createMutableState(): CompositeState {
|
|
63
|
+
return {
|
|
64
|
+
discrete: {},
|
|
65
|
+
blend: {},
|
|
66
|
+
outputs: { css: {}, glsl: {}, aria: {} },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resetState(state: CompositeState): void {
|
|
71
|
+
// Clear all fields in-place (cheaper than allocation)
|
|
72
|
+
const { discrete, blend, css, glsl, aria } = accessCompositeState(state);
|
|
73
|
+
for (const k of Object.keys(discrete)) delete discrete[k];
|
|
74
|
+
for (const k of Object.keys(blend)) delete blend[k];
|
|
75
|
+
for (const k of Object.keys(css)) delete css[k];
|
|
76
|
+
for (const k of Object.keys(glsl)) delete glsl[k];
|
|
77
|
+
for (const k of Object.keys(aria)) delete aria[k];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Creates a ring-buffer pool of pre-allocated CompositeState objects.
|
|
82
|
+
* Acquire/release pattern avoids GC allocations on the hot render path.
|
|
83
|
+
* Default 8 slots -- enough for typical compositor with 4-6 quantizers + headroom.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* const pool = CompositorStatePool.make(4);
|
|
88
|
+
* const state = pool.acquire();
|
|
89
|
+
* state.discrete['theme'] = 'dark';
|
|
90
|
+
* state.outputs.css['--bg'] = '#000';
|
|
91
|
+
* pool.release(state); // resets and returns to pool
|
|
92
|
+
* pool.available; // 4
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
function _make(capacity = COMPOSITOR_POOL_CAP): CompositorStatePoolShape {
|
|
96
|
+
const buffer: CompositeState[] = [];
|
|
97
|
+
for (let i = 0; i < capacity; i++) {
|
|
98
|
+
buffer.push(createMutableState());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const free: boolean[] = new Array(capacity).fill(true);
|
|
102
|
+
let acquirePtr = 0;
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
acquire(): CompositeState {
|
|
106
|
+
// Scan from current pointer for a free slot
|
|
107
|
+
for (let i = 0; i < capacity; i++) {
|
|
108
|
+
const idx = (acquirePtr + i) % capacity;
|
|
109
|
+
if (free[idx]) {
|
|
110
|
+
free[idx] = false;
|
|
111
|
+
acquirePtr = (idx + 1) % capacity;
|
|
112
|
+
return buffer[idx]!;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// All slots in use — allocate overflow (cold path)
|
|
116
|
+
const overflow = createMutableState();
|
|
117
|
+
buffer.push(overflow);
|
|
118
|
+
free.push(false);
|
|
119
|
+
return overflow;
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
release(state: CompositeState): void {
|
|
123
|
+
const idx = buffer.indexOf(state);
|
|
124
|
+
if (idx !== -1) {
|
|
125
|
+
resetState(state);
|
|
126
|
+
free[idx] = true;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
get size(): number {
|
|
131
|
+
return buffer.length;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
get available(): number {
|
|
135
|
+
let count = 0;
|
|
136
|
+
for (let i = 0; i < free.length; i++) {
|
|
137
|
+
if (free[i]) count++;
|
|
138
|
+
}
|
|
139
|
+
return count;
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* CompositorStatePool -- ring buffer of pre-allocated CompositeState objects.
|
|
146
|
+
* Zero-allocation hot path: acquire a state, write into it, render, then release.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```ts
|
|
150
|
+
* const pool = CompositorStatePool.make(8);
|
|
151
|
+
* const state = pool.acquire();
|
|
152
|
+
* // Write compositor output into state.discrete, state.blend, state.outputs
|
|
153
|
+
* pool.release(state); // resets and returns to pool
|
|
154
|
+
* console.log(pool.size, pool.available); // 8, 8
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export const CompositorStatePool = { make: _make };
|
|
158
|
+
|
|
159
|
+
export declare namespace CompositorStatePool {
|
|
160
|
+
/** Structural shape of a pool instance: `acquire`, `release`, `size`, `available`. */
|
|
161
|
+
export type Shape = CompositorStatePoolShape;
|
|
162
|
+
}
|