@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/receipt.ts
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Receipt -- chain validation and envelope construction.
|
|
3
|
+
*
|
|
4
|
+
* Salvaged from `@kit/core`.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Effect } from 'effect';
|
|
10
|
+
import type { HLC } from './brands.js';
|
|
11
|
+
import { TypedRef as TypedRefModule, type TypedRef } from './typed-ref.js';
|
|
12
|
+
import { HLC as HLCOps } from './hlc.js';
|
|
13
|
+
|
|
14
|
+
/** The logical entity a receipt describes: an effect, a run, an artifact, or an intent. */
|
|
15
|
+
export interface ReceiptSubject {
|
|
16
|
+
readonly type: 'effect' | 'run' | 'artifact' | 'intent';
|
|
17
|
+
readonly id: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Single link in a receipt chain: timestamped, content-addressed, and linked
|
|
22
|
+
* to its predecessor(s). Merge envelopes carry an array of `previous` hashes;
|
|
23
|
+
* optionally MAC-signed via `Receipt.macEnvelope`.
|
|
24
|
+
*/
|
|
25
|
+
export interface ReceiptEnvelope {
|
|
26
|
+
readonly kind: string;
|
|
27
|
+
readonly timestamp: HLC;
|
|
28
|
+
readonly subject: ReceiptSubject;
|
|
29
|
+
readonly payload: TypedRef.Shape;
|
|
30
|
+
readonly hash: string;
|
|
31
|
+
readonly previous: string | readonly string[];
|
|
32
|
+
readonly signature?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Structured failure returned by `Receipt.validateChainDetailed`. */
|
|
36
|
+
export type ChainValidationError =
|
|
37
|
+
| { readonly type: 'not_genesis'; readonly index: 0 }
|
|
38
|
+
| { readonly type: 'hash_mismatch'; readonly index: number; readonly computed: string; readonly stored: string }
|
|
39
|
+
| { readonly type: 'chain_break'; readonly index: number; readonly expected: string; readonly actual: string }
|
|
40
|
+
| { readonly type: 'hlc_not_increasing'; readonly index: number };
|
|
41
|
+
|
|
42
|
+
/** Sentinel `previous` value marking the root of a receipt chain. */
|
|
43
|
+
export const GENESIS: string = 'genesis';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compute the content hash of a receipt envelope.
|
|
47
|
+
*
|
|
48
|
+
* Normalizes the `previous` field (sorts array form), canonicalizes the
|
|
49
|
+
* payload, and hashes with SHA-256 via TypedRef.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { Effect } from 'effect';
|
|
54
|
+
*
|
|
55
|
+
* const hash = yield* Receipt.hashEnvelope(envelope);
|
|
56
|
+
* // hash === envelope.hash (if envelope is valid)
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export const hashEnvelope = (envelope: ReceiptEnvelope): Effect.Effect<string> => {
|
|
60
|
+
const previousNormalized = Array.isArray(envelope.previous)
|
|
61
|
+
? [...(envelope.previous as readonly string[])].sort()
|
|
62
|
+
: envelope.previous;
|
|
63
|
+
const hashInput = TypedRefModule.canonicalize({
|
|
64
|
+
kind: envelope.kind,
|
|
65
|
+
timestamp: envelope.timestamp,
|
|
66
|
+
subject: envelope.subject,
|
|
67
|
+
payload: envelope.payload,
|
|
68
|
+
previous: previousNormalized,
|
|
69
|
+
});
|
|
70
|
+
return TypedRefModule.hash(hashInput);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create a new receipt envelope with an auto-computed content hash.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* const envelope = yield* Receipt.createEnvelope(
|
|
79
|
+
* 'state-change',
|
|
80
|
+
* { type: 'effect', id: 'actor-1' },
|
|
81
|
+
* { _tag: 'TypedRef', mediaType: 'application/json', data: { key: 'value' } },
|
|
82
|
+
* hlcTimestamp,
|
|
83
|
+
* Receipt.GENESIS,
|
|
84
|
+
* );
|
|
85
|
+
* // envelope.hash is the computed SHA-256 content address
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export const createEnvelope = (
|
|
89
|
+
kind: string,
|
|
90
|
+
subject: ReceiptSubject,
|
|
91
|
+
payload: TypedRef.Shape,
|
|
92
|
+
timestamp: HLC,
|
|
93
|
+
previousHash: string | readonly string[],
|
|
94
|
+
): Effect.Effect<ReceiptEnvelope> =>
|
|
95
|
+
Effect.gen(function* () {
|
|
96
|
+
const previousNormalized = Array.isArray(previousHash)
|
|
97
|
+
? [...(previousHash as readonly string[])].sort()
|
|
98
|
+
: previousHash;
|
|
99
|
+
const partial = { kind, timestamp, subject, payload, previous: previousNormalized };
|
|
100
|
+
const h = yield* TypedRefModule.hash(TypedRefModule.canonicalize(partial));
|
|
101
|
+
return { kind, timestamp, subject, payload, hash: h, previous: previousNormalized };
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build a linear chain of receipt envelopes from an array of entries.
|
|
106
|
+
*
|
|
107
|
+
* Each envelope's `previous` points to the prior envelope's hash,
|
|
108
|
+
* starting from GENESIS.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* const chain = yield* Receipt.buildChain([
|
|
113
|
+
* { kind: 'init', subject: { type: 'effect', id: 'a' }, payload, timestamp: ts1 },
|
|
114
|
+
* { kind: 'update', subject: { type: 'effect', id: 'a' }, payload, timestamp: ts2 },
|
|
115
|
+
* ]);
|
|
116
|
+
* // chain.length === 2
|
|
117
|
+
* // chain[1].previous === chain[0].hash
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export const buildChain = (
|
|
121
|
+
entries: ReadonlyArray<{
|
|
122
|
+
kind: string;
|
|
123
|
+
subject: ReceiptSubject;
|
|
124
|
+
payload: TypedRef.Shape;
|
|
125
|
+
timestamp: HLC;
|
|
126
|
+
}>,
|
|
127
|
+
): Effect.Effect<ReceiptEnvelope[]> =>
|
|
128
|
+
Effect.gen(function* () {
|
|
129
|
+
const chain: ReceiptEnvelope[] = [];
|
|
130
|
+
let previousHash = GENESIS;
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
const envelope = yield* createEnvelope(entry.kind, entry.subject, entry.payload, entry.timestamp, previousHash);
|
|
133
|
+
chain.push(envelope);
|
|
134
|
+
previousHash = envelope.hash;
|
|
135
|
+
}
|
|
136
|
+
return chain;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Validate a receipt chain: genesis link, hash integrity, chain continuity, HLC ordering.
|
|
141
|
+
*
|
|
142
|
+
* Returns true on success or fails with an Error describing the violation.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* const chain = yield* Receipt.buildChain(entries);
|
|
147
|
+
* const valid = yield* Receipt.validateChain(chain);
|
|
148
|
+
* // valid === true
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export const validateChain = (chain: ReadonlyArray<ReceiptEnvelope>): Effect.Effect<boolean, Error> =>
|
|
152
|
+
Effect.gen(function* () {
|
|
153
|
+
if (chain.length === 0) return true;
|
|
154
|
+
const first = chain[0]!;
|
|
155
|
+
const firstPrev = first.previous;
|
|
156
|
+
if (firstPrev !== GENESIS && !(Array.isArray(firstPrev) && (firstPrev as readonly string[]).includes(GENESIS))) {
|
|
157
|
+
return yield* Effect.fail(new Error('First envelope must have previous=genesis'));
|
|
158
|
+
}
|
|
159
|
+
for (let i = 0; i < chain.length; i++) {
|
|
160
|
+
const envelope = chain[i]!;
|
|
161
|
+
const computedHash = yield* hashEnvelope(envelope);
|
|
162
|
+
if (computedHash !== envelope.hash) {
|
|
163
|
+
return yield* Effect.fail(
|
|
164
|
+
new Error(`Envelope ${i}: hash mismatch (expected "${envelope.hash}", computed "${computedHash}")`),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const isMerge = Array.isArray(envelope.previous);
|
|
168
|
+
if (!isMerge && i > 0 && envelope.previous !== chain[i - 1]!.hash) {
|
|
169
|
+
return yield* Effect.fail(new Error(`Envelope ${i}: chain break`));
|
|
170
|
+
}
|
|
171
|
+
if (!isMerge && i > 0 && HLCOps.compare(chain[i - 1]!.timestamp, envelope.timestamp) >= 0) {
|
|
172
|
+
return yield* Effect.fail(new Error(`Envelope ${i}: HLC not monotonically increasing`));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Validate a receipt chain with detailed, structured error reporting.
|
|
180
|
+
*
|
|
181
|
+
* Returns `true` on success or fails with a typed `ChainValidationError`
|
|
182
|
+
* discriminated union (not_genesis | hash_mismatch | chain_break | hlc_not_increasing).
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```ts
|
|
186
|
+
* import { Effect } from 'effect';
|
|
187
|
+
*
|
|
188
|
+
* const result = yield* Effect.either(Receipt.validateChainDetailed(chain));
|
|
189
|
+
* // result._tag === 'Right' on success
|
|
190
|
+
* // result._tag === 'Left' with .left.type on failure
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
export const validateChainDetailed = (
|
|
194
|
+
chain: ReadonlyArray<ReceiptEnvelope>,
|
|
195
|
+
): Effect.Effect<true, ChainValidationError> =>
|
|
196
|
+
Effect.gen(function* () {
|
|
197
|
+
if (chain.length === 0) return true as const;
|
|
198
|
+
|
|
199
|
+
const first = chain[0]!;
|
|
200
|
+
const firstPrev = first.previous;
|
|
201
|
+
const firstIsGenesis =
|
|
202
|
+
firstPrev === GENESIS || (Array.isArray(firstPrev) && (firstPrev as readonly string[]).includes(GENESIS));
|
|
203
|
+
if (!firstIsGenesis) {
|
|
204
|
+
return yield* Effect.fail({ type: 'not_genesis' as const, index: 0 as const });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (let i = 0; i < chain.length; i++) {
|
|
208
|
+
const envelope = chain[i]!;
|
|
209
|
+
const isMerge = Array.isArray(envelope.previous);
|
|
210
|
+
|
|
211
|
+
const computedHash = yield* hashEnvelope(envelope);
|
|
212
|
+
if (computedHash !== envelope.hash) {
|
|
213
|
+
return yield* Effect.fail({
|
|
214
|
+
type: 'hash_mismatch' as const,
|
|
215
|
+
index: i,
|
|
216
|
+
computed: computedHash,
|
|
217
|
+
stored: envelope.hash,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!isMerge && i > 0 && envelope.previous !== chain[i - 1]!.hash) {
|
|
222
|
+
return yield* Effect.fail({
|
|
223
|
+
type: 'chain_break' as const,
|
|
224
|
+
index: i,
|
|
225
|
+
expected: chain[i - 1]!.hash,
|
|
226
|
+
actual: envelope.previous as string,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!isMerge && i > 0 && HLCOps.compare(chain[i - 1]!.timestamp, envelope.timestamp) >= 0) {
|
|
231
|
+
return yield* Effect.fail({
|
|
232
|
+
type: 'hlc_not_increasing' as const,
|
|
233
|
+
index: i,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return true as const;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check whether a receipt envelope is a genesis (root) envelope.
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```ts
|
|
246
|
+
* const chain = yield* Receipt.buildChain(entries);
|
|
247
|
+
* Receipt.isGenesis(chain[0]); // true
|
|
248
|
+
* Receipt.isGenesis(chain[1]); // false
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
export const isGenesis = (receipt: ReceiptEnvelope): boolean =>
|
|
252
|
+
receipt.previous === GENESIS ||
|
|
253
|
+
(Array.isArray(receipt.previous) && (receipt.previous as readonly string[]).includes(GENESIS));
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get the last (most recent) envelope in a chain.
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```ts
|
|
260
|
+
* const latest = Receipt.head(chain);
|
|
261
|
+
* // latest === chain[chain.length - 1]
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
export const head = (chain: ReadonlyArray<ReceiptEnvelope>): ReceiptEnvelope | undefined =>
|
|
265
|
+
chain.length > 0 ? chain[chain.length - 1] : undefined;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get the first (genesis) envelope in a chain.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```ts
|
|
272
|
+
* const first = Receipt.tail(chain);
|
|
273
|
+
* // first === chain[0]
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
export const tail = (chain: ReadonlyArray<ReceiptEnvelope>): ReceiptEnvelope | undefined =>
|
|
277
|
+
chain.length > 0 ? chain[0] : undefined;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Append a new entry to an existing chain, auto-linking to the previous hash.
|
|
281
|
+
*
|
|
282
|
+
* Optionally accepts explicit previous hashes for merge envelopes.
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```ts
|
|
286
|
+
* const chain = yield* Receipt.buildChain([entry1]);
|
|
287
|
+
* const extended = yield* Receipt.append(chain, {
|
|
288
|
+
* kind: 'update', subject: { type: 'effect', id: 'a' }, payload, timestamp: ts2,
|
|
289
|
+
* });
|
|
290
|
+
* // extended.length === 2
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
export const append = (
|
|
294
|
+
chain: ReadonlyArray<ReceiptEnvelope>,
|
|
295
|
+
entry: { kind: string; subject: ReceiptSubject; payload: TypedRef.Shape; timestamp: HLC },
|
|
296
|
+
previousHashes?: readonly string[],
|
|
297
|
+
): Effect.Effect<ReceiptEnvelope[]> =>
|
|
298
|
+
Effect.gen(function* () {
|
|
299
|
+
const previousHash: string | readonly string[] = previousHashes
|
|
300
|
+
? previousHashes
|
|
301
|
+
: chain.length > 0
|
|
302
|
+
? chain[chain.length - 1]!.hash
|
|
303
|
+
: GENESIS;
|
|
304
|
+
const envelope = yield* createEnvelope(entry.kind, entry.subject, entry.payload, entry.timestamp, previousHash);
|
|
305
|
+
return [...chain, envelope];
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Find an envelope in a chain by its content hash.
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```ts
|
|
313
|
+
* const found = Receipt.findByHash(chain, targetHash);
|
|
314
|
+
* // found?.hash === targetHash
|
|
315
|
+
* ```
|
|
316
|
+
*/
|
|
317
|
+
export const findByHash = (chain: ReadonlyArray<ReceiptEnvelope>, hash: string): ReceiptEnvelope | undefined =>
|
|
318
|
+
chain.find((e) => e.hash === hash);
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Find all envelopes in a chain matching a given kind.
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```ts
|
|
325
|
+
* const updates = Receipt.findByKind(chain, 'update');
|
|
326
|
+
* // updates contains all envelopes with kind === 'update'
|
|
327
|
+
* ```
|
|
328
|
+
*/
|
|
329
|
+
export const findByKind = (chain: ReadonlyArray<ReceiptEnvelope>, kind: string): ReceiptEnvelope[] =>
|
|
330
|
+
chain.filter((e) => e.kind === kind);
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Generate an HMAC-SHA-256 key for signing receipt envelopes.
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* ```ts
|
|
337
|
+
* const key = yield* Receipt.generateMACKey();
|
|
338
|
+
* const signed = yield* Receipt.macEnvelope(envelope, key);
|
|
339
|
+
* // signed.signature is a hex string
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
export const generateMACKey = (): Effect.Effect<CryptoKey, Error> =>
|
|
343
|
+
Effect.tryPromise({
|
|
344
|
+
try: () => crypto.subtle.generateKey({ name: 'HMAC', hash: { name: 'SHA-256' } }, true, ['sign', 'verify']),
|
|
345
|
+
catch: (error) => new Error(`Failed to generate MAC key: ${error}`),
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Sign a receipt envelope with an HMAC key, adding a `signature` field.
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```ts
|
|
353
|
+
* const key = yield* Receipt.generateMACKey();
|
|
354
|
+
* const signed = yield* Receipt.macEnvelope(envelope, key);
|
|
355
|
+
* // signed.signature !== undefined
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
export const macEnvelope = (envelope: ReceiptEnvelope, key: CryptoKey): Effect.Effect<ReceiptEnvelope, Error> =>
|
|
359
|
+
Effect.gen(function* () {
|
|
360
|
+
const data = new TextEncoder().encode(envelope.hash);
|
|
361
|
+
const signatureBuffer = yield* Effect.tryPromise({
|
|
362
|
+
try: () => crypto.subtle.sign('HMAC', key, data),
|
|
363
|
+
catch: (error) => new Error(`Failed to MAC envelope: ${error}`),
|
|
364
|
+
});
|
|
365
|
+
const signatureArray = Array.from(new Uint8Array(signatureBuffer));
|
|
366
|
+
const signature = signatureArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
367
|
+
return { ...envelope, signature };
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Verify an envelope's HMAC signature against a key.
|
|
372
|
+
*
|
|
373
|
+
* Returns false if the envelope has no signature.
|
|
374
|
+
*
|
|
375
|
+
* @example
|
|
376
|
+
* ```ts
|
|
377
|
+
* const valid = yield* Receipt.verifyMAC(signedEnvelope, key);
|
|
378
|
+
* // valid === true if signature matches
|
|
379
|
+
* ```
|
|
380
|
+
*/
|
|
381
|
+
export const verifyMAC = (envelope: ReceiptEnvelope, key: CryptoKey): Effect.Effect<boolean, Error> =>
|
|
382
|
+
Effect.gen(function* () {
|
|
383
|
+
if (!envelope.signature) return false;
|
|
384
|
+
const signatureHex = envelope.signature;
|
|
385
|
+
if (!/^[0-9a-fA-F]+$/.test(signatureHex) || signatureHex.length % 2 !== 0) {
|
|
386
|
+
return yield* Effect.fail(new Error('Invalid signature hex: expected even-length hex string'));
|
|
387
|
+
}
|
|
388
|
+
const signatureArray = new Uint8Array(signatureHex.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)));
|
|
389
|
+
const data = new TextEncoder().encode(envelope.hash);
|
|
390
|
+
const valid = yield* Effect.tryPromise({
|
|
391
|
+
try: () => crypto.subtle.verify('HMAC', key, signatureArray, data),
|
|
392
|
+
catch: (error) => new Error(`Failed to verify signature: ${error}`),
|
|
393
|
+
});
|
|
394
|
+
return valid;
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Receipt namespace -- chain validation and envelope construction.
|
|
399
|
+
*
|
|
400
|
+
* Build, validate, append, query, and sign linear receipt chains.
|
|
401
|
+
* Each envelope is content-addressed and linked to its predecessor.
|
|
402
|
+
* Supports HMAC signing/verification for tamper detection.
|
|
403
|
+
*
|
|
404
|
+
* @example
|
|
405
|
+
* ```ts
|
|
406
|
+
* import { Effect } from 'effect';
|
|
407
|
+
* import { Receipt, HLC } from '@czap/core';
|
|
408
|
+
*
|
|
409
|
+
* const program = Effect.gen(function* () {
|
|
410
|
+
* const ts = HLC.increment(HLC.create('node-1'), Date.now());
|
|
411
|
+
* const chain = yield* Receipt.buildChain([
|
|
412
|
+
* { kind: 'init', subject: { type: 'effect', id: 'a' }, payload, timestamp: ts },
|
|
413
|
+
* ]);
|
|
414
|
+
* const valid = yield* Receipt.validateChain(chain);
|
|
415
|
+
* const latest = Receipt.head(chain);
|
|
416
|
+
* });
|
|
417
|
+
* ```
|
|
418
|
+
*/
|
|
419
|
+
export const Receipt = {
|
|
420
|
+
GENESIS,
|
|
421
|
+
createEnvelope,
|
|
422
|
+
buildChain,
|
|
423
|
+
validateChain,
|
|
424
|
+
validateChainDetailed,
|
|
425
|
+
hashEnvelope,
|
|
426
|
+
isGenesis,
|
|
427
|
+
head,
|
|
428
|
+
tail,
|
|
429
|
+
append,
|
|
430
|
+
findByHash,
|
|
431
|
+
findByKind,
|
|
432
|
+
generateMACKey,
|
|
433
|
+
macEnvelope,
|
|
434
|
+
verifyMAC,
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
export declare namespace Receipt {
|
|
438
|
+
/** Alias for {@link ReceiptSubject}. */
|
|
439
|
+
export type Subject = ReceiptSubject;
|
|
440
|
+
/** Alias for {@link ReceiptEnvelope}. */
|
|
441
|
+
export type Envelope = ReceiptEnvelope;
|
|
442
|
+
/** Alias for {@link ChainValidationError}. */
|
|
443
|
+
export type ChainError = ChainValidationError;
|
|
444
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RuntimeCoordinator -- shared host/runtime coordination surface.
|
|
3
|
+
*
|
|
4
|
+
* Bridges Plan and ECS into the live host path by:
|
|
5
|
+
* - defining the execution graph for runtime passes
|
|
6
|
+
* - backing quantizer state indices and dirty epochs with dense stores
|
|
7
|
+
*
|
|
8
|
+
* The coordinator is intentionally light-weight on the hot path: the plan is
|
|
9
|
+
* constructed once up front, and dense stores are used for numeric state that
|
|
10
|
+
* is read repeatedly during runtime work.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Part, EntityId } from './ecs.js';
|
|
16
|
+
import type { DenseStore } from './ecs.js';
|
|
17
|
+
import { Plan } from './plan.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Named stages of the runtime frame pass, in canonical topological order:
|
|
21
|
+
* discrete quantization first, then blend weights, then target emitters.
|
|
22
|
+
*/
|
|
23
|
+
export type RuntimePhase = 'compute-discrete' | 'compute-blend' | 'emit-css' | 'emit-glsl' | 'emit-aria';
|
|
24
|
+
|
|
25
|
+
/** Options accepted by {@link RuntimeCoordinator.create}: entity capacity and plan name. */
|
|
26
|
+
export interface RuntimeCoordinatorConfig {
|
|
27
|
+
readonly capacity?: number;
|
|
28
|
+
readonly name?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Live coordinator surface: the immutable runtime {@link Plan}, ordered phase
|
|
33
|
+
* list, dense stores for state index + dirty epoch, and registration/mutation
|
|
34
|
+
* APIs used by the compositor on the hot path.
|
|
35
|
+
*/
|
|
36
|
+
export interface RuntimeCoordinatorShape {
|
|
37
|
+
readonly plan: Plan.IR;
|
|
38
|
+
readonly phases: readonly RuntimePhase[];
|
|
39
|
+
readonly stores: {
|
|
40
|
+
readonly stateIndex: DenseStore;
|
|
41
|
+
readonly dirtyEpoch: DenseStore;
|
|
42
|
+
};
|
|
43
|
+
reset(
|
|
44
|
+
registrations?: readonly {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
readonly states: readonly string[];
|
|
47
|
+
}[],
|
|
48
|
+
): void;
|
|
49
|
+
registerQuantizer(name: string, states: readonly string[]): EntityId;
|
|
50
|
+
removeQuantizer(name: string): void;
|
|
51
|
+
hasQuantizer(name: string): boolean;
|
|
52
|
+
setState(name: string, state: string): void;
|
|
53
|
+
applyState(name: string, state: string): number;
|
|
54
|
+
getStateIndex(name: string): number;
|
|
55
|
+
markDirty(name: string): void;
|
|
56
|
+
getDirtyEpoch(name: string): number;
|
|
57
|
+
registeredNames(): readonly string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface RegisteredQuantizer {
|
|
61
|
+
readonly entityId: EntityId;
|
|
62
|
+
readonly stateLookup: Readonly<Record<string, number>>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const DEFAULT_RUNTIME_CAPACITY = 128;
|
|
66
|
+
|
|
67
|
+
function makeRuntimePlan(name: string): Plan.IR {
|
|
68
|
+
return Plan.make(name)
|
|
69
|
+
.step('compute-discrete', { type: 'noop' }, { phase: 'compute-discrete' })
|
|
70
|
+
.step('compute-blend', { type: 'noop' }, { phase: 'compute-blend' })
|
|
71
|
+
.step('emit-css', { type: 'noop' }, { phase: 'emit-css' })
|
|
72
|
+
.step('emit-glsl', { type: 'noop' }, { phase: 'emit-glsl' })
|
|
73
|
+
.step('emit-aria', { type: 'noop' }, { phase: 'emit-aria' })
|
|
74
|
+
.seq('step-1', 'step-2')
|
|
75
|
+
.par('step-2', 'step-3')
|
|
76
|
+
.par('step-2', 'step-4')
|
|
77
|
+
.par('step-2', 'step-5')
|
|
78
|
+
.build();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function orderedPhases(plan: Plan.IR): readonly RuntimePhase[] {
|
|
82
|
+
const sorted = Plan.topoSort(plan).sorted;
|
|
83
|
+
const stepsById = new Map(plan.steps.map((step) => [step.id, step]));
|
|
84
|
+
return sorted
|
|
85
|
+
.map((id) => stepsById.get(id)?.metadata?.['phase'])
|
|
86
|
+
.filter((phase): phase is RuntimePhase => typeof phase === 'string');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const RUNTIME_PLAN_TEMPLATE = makeRuntimePlan('czap-runtime');
|
|
90
|
+
const RUNTIME_PHASES = orderedPhases(RUNTIME_PLAN_TEMPLATE);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build a fresh {@link RuntimeCoordinator} with dense backing stores and the
|
|
94
|
+
* canonical runtime plan. Prefer {@link RuntimeCoordinator.create}, which is
|
|
95
|
+
* the exported entry point.
|
|
96
|
+
*/
|
|
97
|
+
export function createRuntimeCoordinator(config?: RuntimeCoordinatorConfig): RuntimeCoordinatorShape {
|
|
98
|
+
const name = config?.name ?? 'czap-runtime';
|
|
99
|
+
const plan = name === RUNTIME_PLAN_TEMPLATE.name ? RUNTIME_PLAN_TEMPLATE : { ...RUNTIME_PLAN_TEMPLATE, name };
|
|
100
|
+
const phases = RUNTIME_PHASES;
|
|
101
|
+
const stateIndex = Part.dense('state-index', config?.capacity ?? DEFAULT_RUNTIME_CAPACITY);
|
|
102
|
+
const dirtyEpoch = Part.dense('dirty-epoch', config?.capacity ?? DEFAULT_RUNTIME_CAPACITY);
|
|
103
|
+
const quantizerByName = new Map<string, RegisteredQuantizer>();
|
|
104
|
+
let nextEntity = 0;
|
|
105
|
+
|
|
106
|
+
const ensureQuantizer = (name: string): RegisteredQuantizer | undefined => quantizerByName.get(name);
|
|
107
|
+
return {
|
|
108
|
+
plan,
|
|
109
|
+
phases,
|
|
110
|
+
stores: {
|
|
111
|
+
stateIndex,
|
|
112
|
+
dirtyEpoch,
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
reset(registrations) {
|
|
116
|
+
quantizerByName.clear();
|
|
117
|
+
nextEntity = 0;
|
|
118
|
+
stateIndex.reset();
|
|
119
|
+
dirtyEpoch.reset();
|
|
120
|
+
|
|
121
|
+
for (const registration of registrations ?? []) {
|
|
122
|
+
this.registerQuantizer(registration.name, registration.states);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
registerQuantizer(name, states) {
|
|
127
|
+
const existing = ensureQuantizer(name);
|
|
128
|
+
if (existing) {
|
|
129
|
+
return existing.entityId;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const entityId = EntityId(`runtime-${++nextEntity}`);
|
|
133
|
+
const stateLookup: Record<string, number> = Object.create(null);
|
|
134
|
+
for (let index = 0; index < states.length; index++) {
|
|
135
|
+
stateLookup[states[index]!] = index;
|
|
136
|
+
}
|
|
137
|
+
quantizerByName.set(name, {
|
|
138
|
+
entityId,
|
|
139
|
+
stateLookup,
|
|
140
|
+
});
|
|
141
|
+
stateIndex.set(entityId, 0);
|
|
142
|
+
dirtyEpoch.set(entityId, 1);
|
|
143
|
+
return entityId;
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
removeQuantizer(name) {
|
|
147
|
+
const quantizer = ensureQuantizer(name);
|
|
148
|
+
if (!quantizer) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
quantizerByName.delete(name);
|
|
153
|
+
stateIndex.delete(quantizer.entityId);
|
|
154
|
+
dirtyEpoch.delete(quantizer.entityId);
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
hasQuantizer(name) {
|
|
158
|
+
return quantizerByName.has(name);
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
setState(name, state) {
|
|
162
|
+
const quantizer = ensureQuantizer(name);
|
|
163
|
+
if (!quantizer) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
stateIndex.set(quantizer.entityId, quantizer.stateLookup[state] ?? 0);
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
applyState(name, state) {
|
|
171
|
+
const quantizer = ensureQuantizer(name);
|
|
172
|
+
if (!quantizer) {
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const nextIndex = quantizer.stateLookup[state] ?? 0;
|
|
177
|
+
stateIndex.set(quantizer.entityId, nextIndex);
|
|
178
|
+
return nextIndex;
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
getStateIndex(name) {
|
|
182
|
+
const quantizer = ensureQuantizer(name);
|
|
183
|
+
if (!quantizer) {
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return stateIndex.get(quantizer.entityId)!;
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
markDirty(name) {
|
|
191
|
+
const quantizer = ensureQuantizer(name);
|
|
192
|
+
if (!quantizer) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
dirtyEpoch.set(quantizer.entityId, dirtyEpoch.get(quantizer.entityId)! + 1);
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
getDirtyEpoch(name) {
|
|
200
|
+
const quantizer = ensureQuantizer(name);
|
|
201
|
+
if (!quantizer) {
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return dirtyEpoch.get(quantizer.entityId)!;
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
registeredNames() {
|
|
209
|
+
return Array.from(quantizerByName.keys());
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Runtime coordinator namespace — single entry point for building the shared
|
|
216
|
+
* {@link Plan} + ECS store bundle consumed by every host adapter.
|
|
217
|
+
*/
|
|
218
|
+
export const RuntimeCoordinator = {
|
|
219
|
+
/** Create a fresh coordinator. See {@link createRuntimeCoordinator}. */
|
|
220
|
+
create: createRuntimeCoordinator,
|
|
221
|
+
} as const;
|
|
222
|
+
|
|
223
|
+
export declare namespace RuntimeCoordinator {
|
|
224
|
+
/** Alias for `RuntimeCoordinatorShape`. */
|
|
225
|
+
export type Shape = RuntimeCoordinatorShape;
|
|
226
|
+
/** Alias for `RuntimeCoordinatorConfig`. */
|
|
227
|
+
export type Config = RuntimeCoordinatorConfig;
|
|
228
|
+
/** Alias for `RuntimePhase`. */
|
|
229
|
+
export type Phase = RuntimePhase;
|
|
230
|
+
}
|