@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.
Files changed (372) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -0
  3. package/dist/addressed-digest.d.ts +15 -0
  4. package/dist/addressed-digest.d.ts.map +1 -0
  5. package/dist/addressed-digest.js +35 -0
  6. package/dist/addressed-digest.js.map +1 -0
  7. package/dist/animation.d.ts +46 -0
  8. package/dist/animation.d.ts.map +1 -0
  9. package/dist/animation.js +70 -0
  10. package/dist/animation.js.map +1 -0
  11. package/dist/assembly.d.ts +25 -0
  12. package/dist/assembly.d.ts.map +1 -0
  13. package/dist/assembly.js +58 -0
  14. package/dist/assembly.js.map +1 -0
  15. package/dist/av-bridge.d.ts +74 -0
  16. package/dist/av-bridge.d.ts.map +1 -0
  17. package/dist/av-bridge.js +107 -0
  18. package/dist/av-bridge.js.map +1 -0
  19. package/dist/av-renderer.d.ts +56 -0
  20. package/dist/av-renderer.d.ts.map +1 -0
  21. package/dist/av-renderer.js +65 -0
  22. package/dist/av-renderer.js.map +1 -0
  23. package/dist/blend.d.ts +61 -0
  24. package/dist/blend.d.ts.map +1 -0
  25. package/dist/blend.js +100 -0
  26. package/dist/blend.js.map +1 -0
  27. package/dist/boundary.d.ts +154 -0
  28. package/dist/boundary.d.ts.map +1 -0
  29. package/dist/boundary.js +269 -0
  30. package/dist/boundary.js.map +1 -0
  31. package/dist/brands.d.ts +63 -0
  32. package/dist/brands.d.ts.map +1 -0
  33. package/dist/brands.js +31 -0
  34. package/dist/brands.js.map +1 -0
  35. package/dist/caps.d.ts +49 -0
  36. package/dist/caps.d.ts.map +1 -0
  37. package/dist/caps.js +73 -0
  38. package/dist/caps.js.map +1 -0
  39. package/dist/capsule.d.ts +77 -0
  40. package/dist/capsule.d.ts.map +1 -0
  41. package/dist/capsule.js +18 -0
  42. package/dist/capsule.js.map +1 -0
  43. package/dist/capsules/boundary-evaluate.d.ts +28 -0
  44. package/dist/capsules/boundary-evaluate.d.ts.map +1 -0
  45. package/dist/capsules/boundary-evaluate.js +117 -0
  46. package/dist/capsules/boundary-evaluate.js.map +1 -0
  47. package/dist/capsules/canonical-cbor.d.ts +13 -0
  48. package/dist/capsules/canonical-cbor.d.ts.map +1 -0
  49. package/dist/capsules/canonical-cbor.js +60 -0
  50. package/dist/capsules/canonical-cbor.js.map +1 -0
  51. package/dist/capsules/token-buffer.d.ts +24 -0
  52. package/dist/capsules/token-buffer.d.ts.map +1 -0
  53. package/dist/capsules/token-buffer.js +53 -0
  54. package/dist/capsules/token-buffer.js.map +1 -0
  55. package/dist/capture.d.ts +40 -0
  56. package/dist/capture.d.ts.map +1 -0
  57. package/dist/capture.js +10 -0
  58. package/dist/capture.js.map +1 -0
  59. package/dist/cbor.d.ts +33 -0
  60. package/dist/cbor.d.ts.map +1 -0
  61. package/dist/cbor.js +179 -0
  62. package/dist/cbor.js.map +1 -0
  63. package/dist/cell.d.ts +53 -0
  64. package/dist/cell.d.ts.map +1 -0
  65. package/dist/cell.js +83 -0
  66. package/dist/cell.js.map +1 -0
  67. package/dist/codec.d.ts +30 -0
  68. package/dist/codec.d.ts.map +1 -0
  69. package/dist/codec.js +25 -0
  70. package/dist/codec.js.map +1 -0
  71. package/dist/component.d.ts +52 -0
  72. package/dist/component.d.ts.map +1 -0
  73. package/dist/component.js +44 -0
  74. package/dist/component.js.map +1 -0
  75. package/dist/composable.d.ts +76 -0
  76. package/dist/composable.d.ts.map +1 -0
  77. package/dist/composable.js +221 -0
  78. package/dist/composable.js.map +1 -0
  79. package/dist/compositor-pool.d.ts +74 -0
  80. package/dist/compositor-pool.d.ts.map +1 -0
  81. package/dist/compositor-pool.js +119 -0
  82. package/dist/compositor-pool.js.map +1 -0
  83. package/dist/compositor.d.ts +90 -0
  84. package/dist/compositor.d.ts.map +1 -0
  85. package/dist/compositor.js +278 -0
  86. package/dist/compositor.js.map +1 -0
  87. package/dist/config.d.ts +72 -0
  88. package/dist/config.d.ts.map +1 -0
  89. package/dist/config.js +97 -0
  90. package/dist/config.js.map +1 -0
  91. package/dist/dag.d.ts +251 -0
  92. package/dist/dag.d.ts.map +1 -0
  93. package/dist/dag.js +450 -0
  94. package/dist/dag.js.map +1 -0
  95. package/dist/defaults.d.ts +45 -0
  96. package/dist/defaults.d.ts.map +1 -0
  97. package/dist/defaults.js +45 -0
  98. package/dist/defaults.js.map +1 -0
  99. package/dist/derived.d.ts +34 -0
  100. package/dist/derived.d.ts.map +1 -0
  101. package/dist/derived.js +101 -0
  102. package/dist/derived.js.map +1 -0
  103. package/dist/diagnostics.d.ts +77 -0
  104. package/dist/diagnostics.d.ts.map +1 -0
  105. package/dist/diagnostics.js +122 -0
  106. package/dist/diagnostics.js.map +1 -0
  107. package/dist/dirty.d.ts +55 -0
  108. package/dist/dirty.d.ts.map +1 -0
  109. package/dist/dirty.js +80 -0
  110. package/dist/dirty.js.map +1 -0
  111. package/dist/easing.d.ts +55 -0
  112. package/dist/easing.d.ts.map +1 -0
  113. package/dist/easing.js +291 -0
  114. package/dist/easing.js.map +1 -0
  115. package/dist/ecs.d.ts +105 -0
  116. package/dist/ecs.d.ts.map +1 -0
  117. package/dist/ecs.js +245 -0
  118. package/dist/ecs.js.map +1 -0
  119. package/dist/fnv.d.ts +14 -0
  120. package/dist/fnv.d.ts.map +1 -0
  121. package/dist/fnv.js +28 -0
  122. package/dist/fnv.js.map +1 -0
  123. package/dist/frame-budget.d.ts +73 -0
  124. package/dist/frame-budget.d.ts.map +1 -0
  125. package/dist/frame-budget.js +114 -0
  126. package/dist/frame-budget.js.map +1 -0
  127. package/dist/gen-frame.d.ts +102 -0
  128. package/dist/gen-frame.d.ts.map +1 -0
  129. package/dist/gen-frame.js +121 -0
  130. package/dist/gen-frame.js.map +1 -0
  131. package/dist/harness/arbitrary-from-schema.d.ts +28 -0
  132. package/dist/harness/arbitrary-from-schema.d.ts.map +1 -0
  133. package/dist/harness/arbitrary-from-schema.js +262 -0
  134. package/dist/harness/arbitrary-from-schema.js.map +1 -0
  135. package/dist/harness/cached-projection.d.ts +19 -0
  136. package/dist/harness/cached-projection.d.ts.map +1 -0
  137. package/dist/harness/cached-projection.js +39 -0
  138. package/dist/harness/cached-projection.js.map +1 -0
  139. package/dist/harness/index.d.ts +16 -0
  140. package/dist/harness/index.d.ts.map +1 -0
  141. package/dist/harness/index.js +15 -0
  142. package/dist/harness/index.js.map +1 -0
  143. package/dist/harness/policy-gate.d.ts +18 -0
  144. package/dist/harness/policy-gate.d.ts.map +1 -0
  145. package/dist/harness/policy-gate.js +46 -0
  146. package/dist/harness/policy-gate.js.map +1 -0
  147. package/dist/harness/pure-transform.d.ts +42 -0
  148. package/dist/harness/pure-transform.d.ts.map +1 -0
  149. package/dist/harness/pure-transform.js +76 -0
  150. package/dist/harness/pure-transform.js.map +1 -0
  151. package/dist/harness/receipted-mutation.d.ts +23 -0
  152. package/dist/harness/receipted-mutation.d.ts.map +1 -0
  153. package/dist/harness/receipted-mutation.js +52 -0
  154. package/dist/harness/receipted-mutation.js.map +1 -0
  155. package/dist/harness/scene-composition.d.ts +19 -0
  156. package/dist/harness/scene-composition.d.ts.map +1 -0
  157. package/dist/harness/scene-composition.js +47 -0
  158. package/dist/harness/scene-composition.js.map +1 -0
  159. package/dist/harness/site-adapter.d.ts +18 -0
  160. package/dist/harness/site-adapter.d.ts.map +1 -0
  161. package/dist/harness/site-adapter.js +38 -0
  162. package/dist/harness/site-adapter.js.map +1 -0
  163. package/dist/harness/state-machine.d.ts +19 -0
  164. package/dist/harness/state-machine.d.ts.map +1 -0
  165. package/dist/harness/state-machine.js +44 -0
  166. package/dist/harness/state-machine.js.map +1 -0
  167. package/dist/hlc.d.ts +99 -0
  168. package/dist/hlc.d.ts.map +1 -0
  169. package/dist/hlc.js +219 -0
  170. package/dist/hlc.js.map +1 -0
  171. package/dist/index.d.ts +104 -0
  172. package/dist/index.d.ts.map +1 -0
  173. package/dist/index.js +137 -0
  174. package/dist/index.js.map +1 -0
  175. package/dist/interpolate.d.ts +14 -0
  176. package/dist/interpolate.d.ts.map +1 -0
  177. package/dist/interpolate.js +31 -0
  178. package/dist/interpolate.js.map +1 -0
  179. package/dist/live-cell.d.ts +46 -0
  180. package/dist/live-cell.d.ts.map +1 -0
  181. package/dist/live-cell.js +154 -0
  182. package/dist/live-cell.js.map +1 -0
  183. package/dist/op.d.ts +58 -0
  184. package/dist/op.d.ts.map +1 -0
  185. package/dist/op.js +171 -0
  186. package/dist/op.js.map +1 -0
  187. package/dist/plan.d.ts +195 -0
  188. package/dist/plan.d.ts.map +1 -0
  189. package/dist/plan.js +211 -0
  190. package/dist/plan.js.map +1 -0
  191. package/dist/protocol.d.ts +33 -0
  192. package/dist/protocol.d.ts.map +1 -0
  193. package/dist/protocol.js +10 -0
  194. package/dist/protocol.js.map +1 -0
  195. package/dist/quantizer-types.d.ts +28 -0
  196. package/dist/quantizer-types.d.ts.map +1 -0
  197. package/dist/quantizer-types.js +9 -0
  198. package/dist/quantizer-types.js.map +1 -0
  199. package/dist/receipt.d.ts +294 -0
  200. package/dist/receipt.d.ts.map +1 -0
  201. package/dist/receipt.js +352 -0
  202. package/dist/receipt.js.map +1 -0
  203. package/dist/runtime-coordinator.d.ts +75 -0
  204. package/dist/runtime-coordinator.d.ts.map +1 -0
  205. package/dist/runtime-coordinator.js +149 -0
  206. package/dist/runtime-coordinator.js.map +1 -0
  207. package/dist/scheduler.d.ts +58 -0
  208. package/dist/scheduler.d.ts.map +1 -0
  209. package/dist/scheduler.js +109 -0
  210. package/dist/scheduler.js.map +1 -0
  211. package/dist/ship-capsule.d.ts +54 -0
  212. package/dist/ship-capsule.d.ts.map +1 -0
  213. package/dist/ship-capsule.js +142 -0
  214. package/dist/ship-capsule.js.map +1 -0
  215. package/dist/ship-manifest.d.ts +45 -0
  216. package/dist/ship-manifest.d.ts.map +1 -0
  217. package/dist/ship-manifest.js +175 -0
  218. package/dist/ship-manifest.js.map +1 -0
  219. package/dist/signal.d.ts +149 -0
  220. package/dist/signal.d.ts.map +1 -0
  221. package/dist/signal.js +277 -0
  222. package/dist/signal.js.map +1 -0
  223. package/dist/speculative.d.ts +67 -0
  224. package/dist/speculative.d.ts.map +1 -0
  225. package/dist/speculative.js +139 -0
  226. package/dist/speculative.js.map +1 -0
  227. package/dist/store.d.ts +39 -0
  228. package/dist/store.d.ts.map +1 -0
  229. package/dist/store.js +42 -0
  230. package/dist/store.js.map +1 -0
  231. package/dist/style.d.ts +119 -0
  232. package/dist/style.d.ts.map +1 -0
  233. package/dist/style.js +168 -0
  234. package/dist/style.js.map +1 -0
  235. package/dist/testing.d.ts +14 -0
  236. package/dist/testing.d.ts.map +1 -0
  237. package/dist/testing.js +14 -0
  238. package/dist/testing.js.map +1 -0
  239. package/dist/theme.d.ts +78 -0
  240. package/dist/theme.d.ts.map +1 -0
  241. package/dist/theme.js +109 -0
  242. package/dist/theme.js.map +1 -0
  243. package/dist/timeline.d.ts +45 -0
  244. package/dist/timeline.d.ts.map +1 -0
  245. package/dist/timeline.js +101 -0
  246. package/dist/timeline.js.map +1 -0
  247. package/dist/token-buffer.d.ts +43 -0
  248. package/dist/token-buffer.d.ts.map +1 -0
  249. package/dist/token-buffer.js +112 -0
  250. package/dist/token-buffer.js.map +1 -0
  251. package/dist/token.d.ts +107 -0
  252. package/dist/token.d.ts.map +1 -0
  253. package/dist/token.js +143 -0
  254. package/dist/token.js.map +1 -0
  255. package/dist/tuple.d.ts +16 -0
  256. package/dist/tuple.d.ts.map +1 -0
  257. package/dist/tuple.js +16 -0
  258. package/dist/tuple.js.map +1 -0
  259. package/dist/type-utils.d.ts +41 -0
  260. package/dist/type-utils.d.ts.map +1 -0
  261. package/dist/type-utils.js +10 -0
  262. package/dist/type-utils.js.map +1 -0
  263. package/dist/typed-ref.d.ts +50 -0
  264. package/dist/typed-ref.d.ts.map +1 -0
  265. package/dist/typed-ref.js +59 -0
  266. package/dist/typed-ref.js.map +1 -0
  267. package/dist/ui-quality.d.ts +50 -0
  268. package/dist/ui-quality.d.ts.map +1 -0
  269. package/dist/ui-quality.js +64 -0
  270. package/dist/ui-quality.js.map +1 -0
  271. package/dist/validation-error.d.ts +25 -0
  272. package/dist/validation-error.d.ts.map +1 -0
  273. package/dist/validation-error.js +32 -0
  274. package/dist/validation-error.js.map +1 -0
  275. package/dist/vector-clock.d.ts +46 -0
  276. package/dist/vector-clock.d.ts.map +1 -0
  277. package/dist/vector-clock.js +91 -0
  278. package/dist/vector-clock.js.map +1 -0
  279. package/dist/video.d.ts +62 -0
  280. package/dist/video.d.ts.map +1 -0
  281. package/dist/video.js +59 -0
  282. package/dist/video.js.map +1 -0
  283. package/dist/wasm-dispatch.d.ts +52 -0
  284. package/dist/wasm-dispatch.d.ts.map +1 -0
  285. package/dist/wasm-dispatch.js +204 -0
  286. package/dist/wasm-dispatch.js.map +1 -0
  287. package/dist/wasm-fallback.d.ts +19 -0
  288. package/dist/wasm-fallback.d.ts.map +1 -0
  289. package/dist/wasm-fallback.js +93 -0
  290. package/dist/wasm-fallback.js.map +1 -0
  291. package/dist/wire.d.ts +49 -0
  292. package/dist/wire.d.ts.map +1 -0
  293. package/dist/wire.js +201 -0
  294. package/dist/wire.js.map +1 -0
  295. package/dist/zap.d.ts +42 -0
  296. package/dist/zap.d.ts.map +1 -0
  297. package/dist/zap.js +172 -0
  298. package/dist/zap.js.map +1 -0
  299. package/package.json +71 -0
  300. package/src/addressed-digest.ts +48 -0
  301. package/src/animation.ts +103 -0
  302. package/src/assembly.ts +76 -0
  303. package/src/av-bridge.ts +161 -0
  304. package/src/av-renderer.ts +118 -0
  305. package/src/blend.ts +135 -0
  306. package/src/boundary.ts +363 -0
  307. package/src/brands.ts +86 -0
  308. package/src/caps.ts +100 -0
  309. package/src/capsule.ts +95 -0
  310. package/src/capsules/boundary-evaluate.ts +128 -0
  311. package/src/capsules/canonical-cbor.ts +60 -0
  312. package/src/capsules/token-buffer.ts +57 -0
  313. package/src/capture.ts +48 -0
  314. package/src/cbor.ts +199 -0
  315. package/src/cell.ts +130 -0
  316. package/src/codec.ts +39 -0
  317. package/src/component.ts +102 -0
  318. package/src/composable.ts +328 -0
  319. package/src/compositor-pool.ts +162 -0
  320. package/src/compositor.ts +387 -0
  321. package/src/config.ts +157 -0
  322. package/src/dag.ts +527 -0
  323. package/src/defaults.ts +60 -0
  324. package/src/derived.ts +164 -0
  325. package/src/diagnostics.ts +186 -0
  326. package/src/dirty.ts +101 -0
  327. package/src/easing.ts +334 -0
  328. package/src/ecs.ts +382 -0
  329. package/src/fnv.ts +31 -0
  330. package/src/frame-budget.ts +149 -0
  331. package/src/gen-frame.ts +229 -0
  332. package/src/harness/arbitrary-from-schema.ts +270 -0
  333. package/src/harness/cached-projection.ts +46 -0
  334. package/src/harness/index.ts +16 -0
  335. package/src/harness/policy-gate.ts +51 -0
  336. package/src/harness/pure-transform.ts +121 -0
  337. package/src/harness/receipted-mutation.ts +59 -0
  338. package/src/harness/scene-composition.ts +54 -0
  339. package/src/harness/site-adapter.ts +43 -0
  340. package/src/harness/state-machine.ts +49 -0
  341. package/src/hlc.ts +238 -0
  342. package/src/index.ts +274 -0
  343. package/src/interpolate.ts +37 -0
  344. package/src/live-cell.ts +199 -0
  345. package/src/op.ts +233 -0
  346. package/src/plan.ts +317 -0
  347. package/src/protocol.ts +49 -0
  348. package/src/quantizer-types.ts +29 -0
  349. package/src/receipt.ts +444 -0
  350. package/src/runtime-coordinator.ts +230 -0
  351. package/src/scheduler.ts +161 -0
  352. package/src/ship-capsule.ts +191 -0
  353. package/src/signal.ts +345 -0
  354. package/src/speculative.ts +186 -0
  355. package/src/store.ts +77 -0
  356. package/src/style.ts +249 -0
  357. package/src/testing.ts +14 -0
  358. package/src/theme.ts +153 -0
  359. package/src/timeline.ts +146 -0
  360. package/src/token-buffer.ts +151 -0
  361. package/src/token.ts +197 -0
  362. package/src/tuple.ts +19 -0
  363. package/src/type-utils.ts +48 -0
  364. package/src/typed-ref.ts +79 -0
  365. package/src/ui-quality.ts +105 -0
  366. package/src/validation-error.ts +34 -0
  367. package/src/vector-clock.ts +111 -0
  368. package/src/video.ts +106 -0
  369. package/src/wasm-dispatch.ts +300 -0
  370. package/src/wasm-fallback.ts +102 -0
  371. package/src/wire.ts +274 -0
  372. 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
+ }
@@ -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
+ }