@unrulysystems/native-motion-core 0.1.0-alpha.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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/clock.cjs +71 -0
- package/dist/clock.d.cts +23 -0
- package/dist/clock.d.ts +23 -0
- package/dist/clock.js +66 -0
- package/dist/collect-reads.cjs +68 -0
- package/dist/collect-reads.d.cts +23 -0
- package/dist/collect-reads.d.ts +23 -0
- package/dist/collect-reads.js +62 -0
- package/dist/component/boundedArray.cjs +74 -0
- package/dist/component/boundedArray.d.cts +25 -0
- package/dist/component/boundedArray.d.ts +25 -0
- package/dist/component/boundedArray.js +68 -0
- package/dist/component/index.cjs +57 -0
- package/dist/component/index.d.cts +12 -0
- package/dist/component/index.d.ts +12 -0
- package/dist/component/index.js +22 -0
- package/dist/component/orchestration.cjs +129 -0
- package/dist/component/orchestration.d.cts +66 -0
- package/dist/component/orchestration.d.ts +66 -0
- package/dist/component/orchestration.js +124 -0
- package/dist/component/resolve.cjs +104 -0
- package/dist/component/resolve.d.cts +11 -0
- package/dist/component/resolve.d.ts +11 -0
- package/dist/component/resolve.js +97 -0
- package/dist/component/transition.cjs +352 -0
- package/dist/component/transition.d.cts +33 -0
- package/dist/component/transition.d.ts +33 -0
- package/dist/component/transition.js +339 -0
- package/dist/component/types.cjs +114 -0
- package/dist/component/types.d.cts +60 -0
- package/dist/component/types.d.ts +60 -0
- package/dist/component/types.js +111 -0
- package/dist/component/validate.cjs +1015 -0
- package/dist/component/validate.d.cts +37 -0
- package/dist/component/validate.d.ts +37 -0
- package/dist/component/validate.js +1002 -0
- package/dist/component/variants.cjs +333 -0
- package/dist/component/variants.d.cts +106 -0
- package/dist/component/variants.d.ts +106 -0
- package/dist/component/variants.js +321 -0
- package/dist/config/constants.cjs +41 -0
- package/dist/config/constants.d.cts +29 -0
- package/dist/config/constants.d.ts +29 -0
- package/dist/config/constants.js +38 -0
- package/dist/delay.cjs +72 -0
- package/dist/delay.d.cts +26 -0
- package/dist/delay.d.ts +26 -0
- package/dist/delay.js +70 -0
- package/dist/derived.cjs +143 -0
- package/dist/derived.d.cts +41 -0
- package/dist/derived.d.ts +41 -0
- package/dist/derived.js +139 -0
- package/dist/driver/index.cjs +22 -0
- package/dist/driver/index.d.cts +7 -0
- package/dist/driver/index.d.ts +7 -0
- package/dist/driver/index.js +14 -0
- package/dist/driver/keyframeTiming.cjs +130 -0
- package/dist/driver/keyframeTiming.d.cts +18 -0
- package/dist/driver/keyframeTiming.d.ts +18 -0
- package/dist/driver/keyframeTiming.js +124 -0
- package/dist/driver/keyframeTimingConfig.cjs +24 -0
- package/dist/driver/keyframeTimingConfig.d.cts +5 -0
- package/dist/driver/keyframeTimingConfig.d.ts +5 -0
- package/dist/driver/keyframeTimingConfig.js +22 -0
- package/dist/driver/prepare.cjs +459 -0
- package/dist/driver/prepare.d.cts +4 -0
- package/dist/driver/prepare.d.ts +4 -0
- package/dist/driver/prepare.js +454 -0
- package/dist/driver/reference.cjs +762 -0
- package/dist/driver/reference.d.cts +53 -0
- package/dist/driver/reference.d.ts +53 -0
- package/dist/driver/reference.js +757 -0
- package/dist/driver/step.cjs +55 -0
- package/dist/driver/step.d.cts +9 -0
- package/dist/driver/step.d.ts +9 -0
- package/dist/driver/step.js +52 -0
- package/dist/driver/tiers.cjs +40 -0
- package/dist/driver/tiers.d.cts +2 -0
- package/dist/driver/tiers.d.ts +2 -0
- package/dist/driver/tiers.js +37 -0
- package/dist/driver/types.cjs +9 -0
- package/dist/driver/types.d.cts +68 -0
- package/dist/driver/types.d.ts +68 -0
- package/dist/driver/types.js +8 -0
- package/dist/external-animation-ledger.cjs +1250 -0
- package/dist/external-animation-ledger.d.cts +416 -0
- package/dist/external-animation-ledger.d.ts +416 -0
- package/dist/external-animation-ledger.js +1243 -0
- package/dist/gesture/directionLock.cjs +25 -0
- package/dist/gesture/directionLock.d.cts +8 -0
- package/dist/gesture/directionLock.d.ts +8 -0
- package/dist/gesture/directionLock.js +21 -0
- package/dist/gesture/dragConfig.cjs +634 -0
- package/dist/gesture/dragConfig.d.cts +298 -0
- package/dist/gesture/dragConfig.d.ts +298 -0
- package/dist/gesture/dragConfig.js +624 -0
- package/dist/gesture/elastic.cjs +44 -0
- package/dist/gesture/elastic.d.cts +4 -0
- package/dist/gesture/elastic.d.ts +4 -0
- package/dist/gesture/elastic.js +39 -0
- package/dist/gesture/handoffSession.cjs +161 -0
- package/dist/gesture/handoffSession.d.cts +35 -0
- package/dist/gesture/handoffSession.d.ts +35 -0
- package/dist/gesture/handoffSession.js +158 -0
- package/dist/gesture/index.cjs +37 -0
- package/dist/gesture/index.d.cts +12 -0
- package/dist/gesture/index.d.ts +12 -0
- package/dist/gesture/index.js +11 -0
- package/dist/gesture/projection.cjs +95 -0
- package/dist/gesture/projection.d.cts +7 -0
- package/dist/gesture/projection.d.ts +7 -0
- package/dist/gesture/projection.js +87 -0
- package/dist/gesture/session.cjs +162 -0
- package/dist/gesture/session.d.cts +29 -0
- package/dist/gesture/session.d.ts +29 -0
- package/dist/gesture/session.js +158 -0
- package/dist/gesture/types.cjs +5 -0
- package/dist/gesture/types.d.cts +13 -0
- package/dist/gesture/types.d.ts +13 -0
- package/dist/gesture/types.js +4 -0
- package/dist/gesture/viewportConstraints.cjs +38 -0
- package/dist/gesture/viewportConstraints.d.cts +6 -0
- package/dist/gesture/viewportConstraints.d.ts +6 -0
- package/dist/gesture/viewportConstraints.js +34 -0
- package/dist/graph.cjs +226 -0
- package/dist/graph.d.cts +2 -0
- package/dist/graph.d.ts +2 -0
- package/dist/graph.js +223 -0
- package/dist/index.cjs +140 -0
- package/dist/index.d.cts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +70 -0
- package/dist/inertia.cjs +214 -0
- package/dist/inertia.d.cts +53 -0
- package/dist/inertia.d.ts +53 -0
- package/dist/inertia.js +212 -0
- package/dist/instant.cjs +66 -0
- package/dist/instant.d.cts +16 -0
- package/dist/instant.d.ts +16 -0
- package/dist/instant.js +63 -0
- package/dist/internal-driver.cjs +81 -0
- package/dist/internal-driver.d.cts +19 -0
- package/dist/internal-driver.d.ts +19 -0
- package/dist/internal-driver.js +35 -0
- package/dist/keyframes.cjs +191 -0
- package/dist/keyframes.d.cts +13 -0
- package/dist/keyframes.d.ts +13 -0
- package/dist/keyframes.js +188 -0
- package/dist/layout/commitDetector.cjs +67 -0
- package/dist/layout/commitDetector.d.cts +21 -0
- package/dist/layout/commitDetector.d.ts +21 -0
- package/dist/layout/commitDetector.js +64 -0
- package/dist/layout/compose.cjs +67 -0
- package/dist/layout/compose.d.cts +30 -0
- package/dist/layout/compose.d.ts +30 -0
- package/dist/layout/compose.js +65 -0
- package/dist/layout/constants.cjs +13 -0
- package/dist/layout/constants.d.cts +6 -0
- package/dist/layout/constants.d.ts +6 -0
- package/dist/layout/constants.js +10 -0
- package/dist/layout/identity.cjs +700 -0
- package/dist/layout/identity.d.cts +67 -0
- package/dist/layout/identity.d.ts +67 -0
- package/dist/layout/identity.js +698 -0
- package/dist/layout/index.cjs +36 -0
- package/dist/layout/index.d.cts +18 -0
- package/dist/layout/index.d.ts +18 -0
- package/dist/layout/index.js +13 -0
- package/dist/layout/measure.cjs +81 -0
- package/dist/layout/measure.d.cts +43 -0
- package/dist/layout/measure.d.ts +43 -0
- package/dist/layout/measure.js +78 -0
- package/dist/layout/projection.cjs +99 -0
- package/dist/layout/projection.d.cts +23 -0
- package/dist/layout/projection.d.ts +23 -0
- package/dist/layout/projection.js +97 -0
- package/dist/layout/scroll.cjs +27 -0
- package/dist/layout/scroll.d.cts +13 -0
- package/dist/layout/scroll.d.ts +13 -0
- package/dist/layout/scroll.js +24 -0
- package/dist/layout/session.cjs +207 -0
- package/dist/layout/session.d.cts +73 -0
- package/dist/layout/session.d.ts +73 -0
- package/dist/layout/session.js +205 -0
- package/dist/layout/tree.cjs +826 -0
- package/dist/layout/tree.d.cts +70 -0
- package/dist/layout/tree.d.ts +70 -0
- package/dist/layout/tree.js +823 -0
- package/dist/layout/types.cjs +36 -0
- package/dist/layout/types.d.cts +31 -0
- package/dist/layout/types.d.ts +31 -0
- package/dist/layout/types.js +35 -0
- package/dist/motion-arc.cjs +184 -0
- package/dist/motion-arc.d.cts +78 -0
- package/dist/motion-arc.d.ts +78 -0
- package/dist/motion-arc.js +183 -0
- package/dist/motion-mix.cjs +205 -0
- package/dist/motion-mix.d.cts +3 -0
- package/dist/motion-mix.d.ts +3 -0
- package/dist/motion-mix.js +202 -0
- package/dist/motion-value-driver-port.cjs +636 -0
- package/dist/motion-value-driver-port.d.cts +406 -0
- package/dist/motion-value-driver-port.d.ts +406 -0
- package/dist/motion-value-driver-port.js +627 -0
- package/dist/motion-value.cjs +189 -0
- package/dist/motion-value.d.cts +51 -0
- package/dist/motion-value.d.ts +51 -0
- package/dist/motion-value.js +185 -0
- package/dist/presence/controller.cjs +657 -0
- package/dist/presence/controller.d.cts +6 -0
- package/dist/presence/controller.d.ts +6 -0
- package/dist/presence/controller.js +652 -0
- package/dist/presence/index.cjs +19 -0
- package/dist/presence/index.d.cts +4 -0
- package/dist/presence/index.d.ts +4 -0
- package/dist/presence/index.js +12 -0
- package/dist/presence/machine.cjs +50 -0
- package/dist/presence/machine.d.cts +10 -0
- package/dist/presence/machine.d.ts +10 -0
- package/dist/presence/machine.js +46 -0
- package/dist/presence/types.cjs +6 -0
- package/dist/presence/types.d.cts +32 -0
- package/dist/presence/types.d.ts +32 -0
- package/dist/presence/types.js +5 -0
- package/dist/repeat.cjs +311 -0
- package/dist/repeat.d.cts +174 -0
- package/dist/repeat.d.ts +174 -0
- package/dist/repeat.js +300 -0
- package/dist/spring.cjs +128 -0
- package/dist/spring.d.cts +18 -0
- package/dist/spring.d.ts +18 -0
- package/dist/spring.js +125 -0
- package/dist/subscriptions.cjs +74 -0
- package/dist/subscriptions.d.cts +19 -0
- package/dist/subscriptions.d.ts +19 -0
- package/dist/subscriptions.js +69 -0
- package/dist/subset/index.cjs +23 -0
- package/dist/subset/index.d.cts +6 -0
- package/dist/subset/index.d.ts +6 -0
- package/dist/subset/index.js +13 -0
- package/dist/subset/normalize.cjs +90 -0
- package/dist/subset/normalize.d.cts +10 -0
- package/dist/subset/normalize.d.ts +10 -0
- package/dist/subset/normalize.js +85 -0
- package/dist/subset/registry.cjs +259 -0
- package/dist/subset/registry.d.cts +25 -0
- package/dist/subset/registry.d.ts +25 -0
- package/dist/subset/registry.js +256 -0
- package/dist/subset/resolve.cjs +98 -0
- package/dist/subset/resolve.d.cts +24 -0
- package/dist/subset/resolve.d.ts +24 -0
- package/dist/subset/resolve.js +90 -0
- package/dist/timing.cjs +206 -0
- package/dist/timing.d.cts +17 -0
- package/dist/timing.d.ts +17 -0
- package/dist/timing.js +201 -0
- package/dist/transformTemplate.cjs +219 -0
- package/dist/transformTemplate.d.cts +29 -0
- package/dist/transformTemplate.d.ts +29 -0
- package/dist/transformTemplate.js +215 -0
- package/dist/transition.cjs +231 -0
- package/dist/transition.d.cts +89 -0
- package/dist/transition.d.ts +89 -0
- package/dist/transition.js +223 -0
- package/dist/types.cjs +4 -0
- package/dist/types.d.cts +175 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.js +3 -0
- package/dist/value-types/color.cjs +220 -0
- package/dist/value-types/color.d.cts +19 -0
- package/dist/value-types/color.d.ts +19 -0
- package/dist/value-types/color.js +218 -0
- package/dist/value-types/complex.cjs +160 -0
- package/dist/value-types/complex.d.cts +19 -0
- package/dist/value-types/complex.d.ts +19 -0
- package/dist/value-types/complex.js +155 -0
- package/dist/value-types/constants.cjs +15 -0
- package/dist/value-types/constants.d.cts +6 -0
- package/dist/value-types/constants.d.ts +6 -0
- package/dist/value-types/constants.js +12 -0
- package/dist/value-types/discrete.cjs +62 -0
- package/dist/value-types/discrete.d.cts +4 -0
- package/dist/value-types/discrete.d.ts +4 -0
- package/dist/value-types/discrete.js +56 -0
- package/dist/value-types/index.cjs +59 -0
- package/dist/value-types/index.d.cts +14 -0
- package/dist/value-types/index.d.ts +14 -0
- package/dist/value-types/index.js +24 -0
- package/dist/value-types/measure-resolve.cjs +325 -0
- package/dist/value-types/measure-resolve.d.cts +91 -0
- package/dist/value-types/measure-resolve.d.ts +91 -0
- package/dist/value-types/measure-resolve.js +313 -0
- package/dist/value-types/mix.cjs +90 -0
- package/dist/value-types/mix.d.cts +27 -0
- package/dist/value-types/mix.d.ts +27 -0
- package/dist/value-types/mix.js +85 -0
- package/dist/value-types/named-colors.cjs +61 -0
- package/dist/value-types/named-colors.d.cts +2 -0
- package/dist/value-types/named-colors.d.ts +2 -0
- package/dist/value-types/named-colors.js +58 -0
- package/dist/value-types/numeric.cjs +86 -0
- package/dist/value-types/numeric.d.cts +16 -0
- package/dist/value-types/numeric.d.ts +16 -0
- package/dist/value-types/numeric.js +79 -0
- package/dist/worklet-layout/config/constants.js +39 -0
- package/dist/worklet-layout/layout/constants.js +11 -0
- package/dist/worklet-layout/layout/identity.js +699 -0
- package/dist/worklet-layout/layout/projection.js +98 -0
- package/dist/worklet-layout/layout/session.js +206 -0
- package/dist/worklet-layout/layout/tree.js +824 -0
- package/dist/worklet-layout/layout/types.js +36 -0
- package/dist/worklet-layout/spring.js +125 -0
- package/dist/worklet-layout/timing.js +201 -0
- package/dist/worklet-layout/transition.js +223 -0
- package/dist/worklet-layout.cjs +47 -0
- package/dist/worklet-layout.d.cts +15 -0
- package/dist/worklet-layout.d.ts +15 -0
- package/dist/worklet-layout.js +28 -0
- package/dist/wrap.cjs +7 -0
- package/dist/wrap.d.cts +1 -0
- package/dist/wrap.d.ts +1 -0
- package/dist/wrap.js +4 -0
- package/package.json +45 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @unrulysystems/native-motion-core
|
|
2
|
+
|
|
3
|
+
## 0.1.0-alpha.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Initial alpha prerelease: the M3-era public surface (layout, presence, drag, gestures, variants,
|
|
8
|
+
orchestration, value types) with the cross-engine conformance suite published alongside the
|
|
9
|
+
runtime packages.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Unruly Systems LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @unrulysystems/native-motion-core
|
|
2
|
+
|
|
3
|
+
The host-agnostic heart of the system (SPEC-CORE): a deterministic motion graph plus every pure
|
|
4
|
+
animation subsystem, with **zero host dependencies** — nothing here may import react,
|
|
5
|
+
react-native, reanimated, gesture-handler, or motion. That law is structural: an oxlint
|
|
6
|
+
`no-restricted-imports` rule over `src/**` (root `.oxlintrc.json`, run by `nub run lint`) fails on
|
|
7
|
+
ANY non-relative import in non-test source (REQ-CORE-003). The native runtime, the web shim, and the conformance harness all consume this
|
|
8
|
+
one module.
|
|
9
|
+
|
|
10
|
+
## Surface map (`src/index.ts`)
|
|
11
|
+
|
|
12
|
+
| Area | Role | Key files |
|
|
13
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
14
|
+
| graph / clock / scheduler | The deterministic motion graph: injectable clock + scheduler, motion values with clock-timestamped velocity | `graph.ts`, `clock.ts`, `types.ts`, `motion-value.ts` (internal) |
|
|
15
|
+
| spring | Analytic closed-form scalar spring solver | `spring.ts` |
|
|
16
|
+
| transition | User transition config → concrete physics; the retarget/interruption seam (`retargetSpring` seeds from live state) | `transition.ts` |
|
|
17
|
+
| timing | Duration + easing tween generator | `timing.ts` |
|
|
18
|
+
| value-types | Typed parsers + gamma-aware mixers (numeric/unit, color, complex), unified `mix()` | `value-types/` |
|
|
19
|
+
| subset | The frozen universal-subset registry — the single source of truth every engine's prop surface derives from | `subset/` |
|
|
20
|
+
| component | The declarative public API contract: `Target`/`Transition` (seconds), loud-fail `validateTarget`, pure `resolveTarget` | `component/` |
|
|
21
|
+
| driver | The host-agnostic `Driver` seam + pure per-frame `stepProp`, reference driver, prop tiers | `driver/` |
|
|
22
|
+
| presence | Exit-lifecycle state machine + controller (present → exiting → removed, settle-coupled) | `presence/` |
|
|
23
|
+
| gesture | Pure gesture math (projection, snap, elastic, velocity normalization) + the begin→active→end/cancel session | `gesture/` |
|
|
24
|
+
| layout | Pure FLIP projection (`invert`/`projectAtProgress`, top-left anchored Transform) + measurement lifecycle + projection session | `layout/` |
|
|
25
|
+
|
|
26
|
+
Naming note: `src/transition.ts` (spring-config resolution, `resolveSpring`) and
|
|
27
|
+
`src/component/transition.ts` (public seconds→ms boundary, `toSpringConfig`) are different
|
|
28
|
+
seams that share a basename — read the directory.
|
|
29
|
+
|
|
30
|
+
## Verification
|
|
31
|
+
|
|
32
|
+
- 460 colocated unit tests (39 files, `src/**` plus the `scripts/**` gate suites), all
|
|
33
|
+
deterministic under `ManualClock`/`ManualScheduler`; green under Vitest.
|
|
34
|
+
- Golden files under `src/__goldens__/` pin the pinned-motion oracle's outputs; they are
|
|
35
|
+
GENERATED by `packages/conformance/scripts/gen-*-goldens.mjs` (core itself must never import
|
|
36
|
+
motion-dom) and regenerated only on an intentional oracle bump.
|
|
37
|
+
- Worklet tagging is manifest-driven (`scripts/worklet-tagging-manifest.mjs`, the single source):
|
|
38
|
+
`scripts/tag-worklet-dist.mjs` post-build tags the four FILE-level crossing modules
|
|
39
|
+
(`transition`, `spring`, `timing`, `driver/step`), 19 FUNCTION-level directives on the dual-use
|
|
40
|
+
modules (`value-types/color`, `layout/*`), and the tagged `worklet-layout/` runtime copies —
|
|
41
|
+
an un-tagged crossing fails only live on device (0.5.x SIGSEGV / 0.10.x Remote-Function throw),
|
|
42
|
+
so the lists are closed over transitive callees and verified fail-closed by
|
|
43
|
+
`scripts/check-worklet-banners.mjs` (in `nub run check`) and per-artifact by
|
|
44
|
+
`scripts/check-bundle-worklets.mjs` (run by `apps/mobile/scripts/proof-build.sh`).
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
cd packages/core && bunx vitest run
|
|
48
|
+
```
|
package/dist/clock.cjs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Deterministic clock + scheduler for the injectable time seam (REQ-CLOCK-001, REQ-VERIFY-001).
|
|
3
|
+
// These are real implementations, not stubs: the whole graph is made deterministic under test by
|
|
4
|
+
// advancing time in explicit increments and pumping frames. Exported so the conformance runner
|
|
5
|
+
// (M1b) and any consumer's tests reuse them.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.ManualScheduler = exports.ManualClock = void 0;
|
|
8
|
+
/** A clock whose time only moves when `advance` is called. */
|
|
9
|
+
class ManualClock {
|
|
10
|
+
#t;
|
|
11
|
+
constructor(start = 0) {
|
|
12
|
+
this.#t = start;
|
|
13
|
+
}
|
|
14
|
+
now() {
|
|
15
|
+
return this.#t;
|
|
16
|
+
}
|
|
17
|
+
/** Move time forward by `ms`. Negative deltas are a programming error. */
|
|
18
|
+
advance(ms) {
|
|
19
|
+
if (ms < 0)
|
|
20
|
+
throw new Error(`ManualClock.advance: negative delta ${ms}`);
|
|
21
|
+
this.#t += ms;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.ManualClock = ManualClock;
|
|
25
|
+
/**
|
|
26
|
+
* A scheduler whose frames only fire when `frame` is called. Advancing the clock and running the
|
|
27
|
+
* registered per-frame steps are coupled here so a test drives wall-time and frames in one call,
|
|
28
|
+
* keeping the sampled trajectory reproducible.
|
|
29
|
+
*/
|
|
30
|
+
class ManualScheduler {
|
|
31
|
+
#clock;
|
|
32
|
+
#steps = new Set();
|
|
33
|
+
// Command-edge-grown reusable snapshot (REQ-DRIVER-021, packet §6 ordinary local-JS repair):
|
|
34
|
+
// capacity extends at the registration edge (onFrame); frame() refills it by indexed writes and
|
|
35
|
+
// drains by index — no fresh Array.from snapshot per local frame.
|
|
36
|
+
#stepSnapshot = [];
|
|
37
|
+
#snapshotCount = 0;
|
|
38
|
+
constructor(clock) {
|
|
39
|
+
this.#clock = clock;
|
|
40
|
+
}
|
|
41
|
+
onFrame(step) {
|
|
42
|
+
this.#steps.add(step);
|
|
43
|
+
// Registration edge: grow the reusable snapshot's capacity here, never mid-frame.
|
|
44
|
+
if (this.#stepSnapshot.length < this.#steps.size)
|
|
45
|
+
this.#stepSnapshot.length = this.#steps.size;
|
|
46
|
+
return () => {
|
|
47
|
+
this.#steps.delete(step);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Advance the clock by `dtMs` and run one frame step for every registered listener. */
|
|
51
|
+
frame(dtMs) {
|
|
52
|
+
this.#clock.advance(dtMs);
|
|
53
|
+
// Snapshot into the reusable buffer so a step that unregisters mid-frame does not perturb
|
|
54
|
+
// this iteration. forEach walks the Set's internal slots — no iterator object per frame.
|
|
55
|
+
this.#snapshotCount = 0;
|
|
56
|
+
this.#steps.forEach(this.#collectStep);
|
|
57
|
+
const count = this.#snapshotCount;
|
|
58
|
+
for (let i = 0; i < count; i++)
|
|
59
|
+
this.#stepSnapshot[i](dtMs);
|
|
60
|
+
}
|
|
61
|
+
#collectStep = (step) => {
|
|
62
|
+
this.#stepSnapshot[this.#snapshotCount] = step;
|
|
63
|
+
this.#snapshotCount += 1;
|
|
64
|
+
};
|
|
65
|
+
/** Convenience: run `count` frames of `dtMs` each. */
|
|
66
|
+
frames(count, dtMs) {
|
|
67
|
+
for (let i = 0; i < count; i++)
|
|
68
|
+
this.frame(dtMs);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.ManualScheduler = ManualScheduler;
|
package/dist/clock.d.cts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Clock, Scheduler } from "./types.cjs";
|
|
2
|
+
/** A clock whose time only moves when `advance` is called. */
|
|
3
|
+
export declare class ManualClock implements Clock {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(start?: number);
|
|
6
|
+
now(): number;
|
|
7
|
+
/** Move time forward by `ms`. Negative deltas are a programming error. */
|
|
8
|
+
advance(ms: number): void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A scheduler whose frames only fire when `frame` is called. Advancing the clock and running the
|
|
12
|
+
* registered per-frame steps are coupled here so a test drives wall-time and frames in one call,
|
|
13
|
+
* keeping the sampled trajectory reproducible.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ManualScheduler implements Scheduler {
|
|
16
|
+
#private;
|
|
17
|
+
constructor(clock: ManualClock);
|
|
18
|
+
onFrame(step: (dtMs: number) => void): () => void;
|
|
19
|
+
/** Advance the clock by `dtMs` and run one frame step for every registered listener. */
|
|
20
|
+
frame(dtMs: number): void;
|
|
21
|
+
/** Convenience: run `count` frames of `dtMs` each. */
|
|
22
|
+
frames(count: number, dtMs: number): void;
|
|
23
|
+
}
|
package/dist/clock.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Clock, Scheduler } from "./types.js";
|
|
2
|
+
/** A clock whose time only moves when `advance` is called. */
|
|
3
|
+
export declare class ManualClock implements Clock {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(start?: number);
|
|
6
|
+
now(): number;
|
|
7
|
+
/** Move time forward by `ms`. Negative deltas are a programming error. */
|
|
8
|
+
advance(ms: number): void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A scheduler whose frames only fire when `frame` is called. Advancing the clock and running the
|
|
12
|
+
* registered per-frame steps are coupled here so a test drives wall-time and frames in one call,
|
|
13
|
+
* keeping the sampled trajectory reproducible.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ManualScheduler implements Scheduler {
|
|
16
|
+
#private;
|
|
17
|
+
constructor(clock: ManualClock);
|
|
18
|
+
onFrame(step: (dtMs: number) => void): () => void;
|
|
19
|
+
/** Advance the clock by `dtMs` and run one frame step for every registered listener. */
|
|
20
|
+
frame(dtMs: number): void;
|
|
21
|
+
/** Convenience: run `count` frames of `dtMs` each. */
|
|
22
|
+
frames(count: number, dtMs: number): void;
|
|
23
|
+
}
|
package/dist/clock.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Deterministic clock + scheduler for the injectable time seam (REQ-CLOCK-001, REQ-VERIFY-001).
|
|
2
|
+
// These are real implementations, not stubs: the whole graph is made deterministic under test by
|
|
3
|
+
// advancing time in explicit increments and pumping frames. Exported so the conformance runner
|
|
4
|
+
// (M1b) and any consumer's tests reuse them.
|
|
5
|
+
/** A clock whose time only moves when `advance` is called. */
|
|
6
|
+
export class ManualClock {
|
|
7
|
+
#t;
|
|
8
|
+
constructor(start = 0) {
|
|
9
|
+
this.#t = start;
|
|
10
|
+
}
|
|
11
|
+
now() {
|
|
12
|
+
return this.#t;
|
|
13
|
+
}
|
|
14
|
+
/** Move time forward by `ms`. Negative deltas are a programming error. */
|
|
15
|
+
advance(ms) {
|
|
16
|
+
if (ms < 0)
|
|
17
|
+
throw new Error(`ManualClock.advance: negative delta ${ms}`);
|
|
18
|
+
this.#t += ms;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A scheduler whose frames only fire when `frame` is called. Advancing the clock and running the
|
|
23
|
+
* registered per-frame steps are coupled here so a test drives wall-time and frames in one call,
|
|
24
|
+
* keeping the sampled trajectory reproducible.
|
|
25
|
+
*/
|
|
26
|
+
export class ManualScheduler {
|
|
27
|
+
#clock;
|
|
28
|
+
#steps = new Set();
|
|
29
|
+
// Command-edge-grown reusable snapshot (REQ-DRIVER-021, packet §6 ordinary local-JS repair):
|
|
30
|
+
// capacity extends at the registration edge (onFrame); frame() refills it by indexed writes and
|
|
31
|
+
// drains by index — no fresh Array.from snapshot per local frame.
|
|
32
|
+
#stepSnapshot = [];
|
|
33
|
+
#snapshotCount = 0;
|
|
34
|
+
constructor(clock) {
|
|
35
|
+
this.#clock = clock;
|
|
36
|
+
}
|
|
37
|
+
onFrame(step) {
|
|
38
|
+
this.#steps.add(step);
|
|
39
|
+
// Registration edge: grow the reusable snapshot's capacity here, never mid-frame.
|
|
40
|
+
if (this.#stepSnapshot.length < this.#steps.size)
|
|
41
|
+
this.#stepSnapshot.length = this.#steps.size;
|
|
42
|
+
return () => {
|
|
43
|
+
this.#steps.delete(step);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Advance the clock by `dtMs` and run one frame step for every registered listener. */
|
|
47
|
+
frame(dtMs) {
|
|
48
|
+
this.#clock.advance(dtMs);
|
|
49
|
+
// Snapshot into the reusable buffer so a step that unregisters mid-frame does not perturb
|
|
50
|
+
// this iteration. forEach walks the Set's internal slots — no iterator object per frame.
|
|
51
|
+
this.#snapshotCount = 0;
|
|
52
|
+
this.#steps.forEach(this.#collectStep);
|
|
53
|
+
const count = this.#snapshotCount;
|
|
54
|
+
for (let i = 0; i < count; i++)
|
|
55
|
+
this.#stepSnapshot[i](dtMs);
|
|
56
|
+
}
|
|
57
|
+
#collectStep = (step) => {
|
|
58
|
+
this.#stepSnapshot[this.#snapshotCount] = step;
|
|
59
|
+
this.#snapshotCount += 1;
|
|
60
|
+
};
|
|
61
|
+
/** Convenience: run `count` frames of `dtMs` each. */
|
|
62
|
+
frames(count, dtMs) {
|
|
63
|
+
for (let i = 0; i < count; i++)
|
|
64
|
+
this.frame(dtMs);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Dependency collection for reactive derived computes (REQ-API-027's `useTransform(() => expr)`
|
|
3
|
+
// form — the same actual-reads trick the pinned oracle uses). A collection frame records every
|
|
4
|
+
// MotionValue whose `get()` runs inside `compute`; outside a frame the report is a single null
|
|
5
|
+
// check, so the hot read path stays allocation- and branch-cheap. Frames nest (the previous
|
|
6
|
+
// collector is restored on exit) and always close, even when the compute throws — a stuck
|
|
7
|
+
// collector would silently attribute unrelated reads to the wrong derived.
|
|
8
|
+
//
|
|
9
|
+
// Frame-path repair (REQ-DRIVER-021, packet §6 ordinary local-JS bookkeeping): an active
|
|
10
|
+
// collector dedupes into prefilled indexed slots — no Set growth per read in-frame. The public
|
|
11
|
+
// ReadonlySet view is materialized once when the collection closes (a command/render edge),
|
|
12
|
+
// never per read.
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.ActiveMotionValueReadCollector = void 0;
|
|
15
|
+
exports.reportMotionValueRead = reportMotionValueRead;
|
|
16
|
+
exports.collectMotionValueReads = collectMotionValueReads;
|
|
17
|
+
/**
|
|
18
|
+
* The active collection frame. Reads dedupe by an indexed scan of the prefilled slot list —
|
|
19
|
+
* the read count per compute is tiny, so the linear membership check beats a Set's per-add
|
|
20
|
+
* growth on the frame path.
|
|
21
|
+
*/
|
|
22
|
+
class ActiveMotionValueReadCollector {
|
|
23
|
+
// Prefilled-slot list: capacity grows only to the compute's read high-water mark (below), then
|
|
24
|
+
// is reused — never a fresh array per read.
|
|
25
|
+
#slots = [];
|
|
26
|
+
#count = 0;
|
|
27
|
+
/** @internal Record one read, deduping into the prefilled slots. Called only via reportMotionValueRead. */
|
|
28
|
+
recordUniqueIntoPrefilledSlots(value) {
|
|
29
|
+
const slots = this.#slots;
|
|
30
|
+
const count = this.#count;
|
|
31
|
+
for (let i = 0; i < count; i++) {
|
|
32
|
+
if (slots[i] === value)
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (count === slots.length)
|
|
36
|
+
slots.length = count === 0 ? 8 : count * 2;
|
|
37
|
+
slots[count] = value;
|
|
38
|
+
this.#count = count + 1;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Materialize the public ReadonlySet view ONCE at collection close (the command/render edge
|
|
42
|
+
* that opened the frame) — the repair moves every per-read Set.add off the read path.
|
|
43
|
+
*/
|
|
44
|
+
sealReads() {
|
|
45
|
+
const reads = new Set();
|
|
46
|
+
for (let i = 0; i < this.#count; i++)
|
|
47
|
+
reads.add(this.#slots[i]);
|
|
48
|
+
return reads;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.ActiveMotionValueReadCollector = ActiveMotionValueReadCollector;
|
|
52
|
+
let activeCollector = null;
|
|
53
|
+
/** @internal Called by MotionValue implementations from `get()`. */
|
|
54
|
+
function reportMotionValueRead(value) {
|
|
55
|
+
if (activeCollector !== null)
|
|
56
|
+
activeCollector.recordUniqueIntoPrefilledSlots(value);
|
|
57
|
+
}
|
|
58
|
+
function collectMotionValueReads(compute) {
|
|
59
|
+
const previous = activeCollector;
|
|
60
|
+
const collector = new ActiveMotionValueReadCollector();
|
|
61
|
+
activeCollector = collector;
|
|
62
|
+
try {
|
|
63
|
+
return { result: compute(), reads: collector.sealReads() };
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
activeCollector = previous;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MotionValue } from "./types.cjs";
|
|
2
|
+
/**
|
|
3
|
+
* The active collection frame. Reads dedupe by an indexed scan of the prefilled slot list —
|
|
4
|
+
* the read count per compute is tiny, so the linear membership check beats a Set's per-add
|
|
5
|
+
* growth on the frame path.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ActiveMotionValueReadCollector {
|
|
8
|
+
#private;
|
|
9
|
+
/** @internal Record one read, deduping into the prefilled slots. Called only via reportMotionValueRead. */
|
|
10
|
+
recordUniqueIntoPrefilledSlots(value: MotionValue<unknown>): void;
|
|
11
|
+
/**
|
|
12
|
+
* Materialize the public ReadonlySet view ONCE at collection close (the command/render edge
|
|
13
|
+
* that opened the frame) — the repair moves every per-read Set.add off the read path.
|
|
14
|
+
*/
|
|
15
|
+
sealReads(): ReadonlySet<MotionValue<unknown>>;
|
|
16
|
+
}
|
|
17
|
+
/** @internal Called by MotionValue implementations from `get()`. */
|
|
18
|
+
export declare function reportMotionValueRead(value: MotionValue<unknown>): void;
|
|
19
|
+
export interface CollectedReads<T> {
|
|
20
|
+
readonly result: T;
|
|
21
|
+
readonly reads: ReadonlySet<MotionValue<unknown>>;
|
|
22
|
+
}
|
|
23
|
+
export declare function collectMotionValueReads<T>(compute: () => T): CollectedReads<T>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MotionValue } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The active collection frame. Reads dedupe by an indexed scan of the prefilled slot list —
|
|
4
|
+
* the read count per compute is tiny, so the linear membership check beats a Set's per-add
|
|
5
|
+
* growth on the frame path.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ActiveMotionValueReadCollector {
|
|
8
|
+
#private;
|
|
9
|
+
/** @internal Record one read, deduping into the prefilled slots. Called only via reportMotionValueRead. */
|
|
10
|
+
recordUniqueIntoPrefilledSlots(value: MotionValue<unknown>): void;
|
|
11
|
+
/**
|
|
12
|
+
* Materialize the public ReadonlySet view ONCE at collection close (the command/render edge
|
|
13
|
+
* that opened the frame) — the repair moves every per-read Set.add off the read path.
|
|
14
|
+
*/
|
|
15
|
+
sealReads(): ReadonlySet<MotionValue<unknown>>;
|
|
16
|
+
}
|
|
17
|
+
/** @internal Called by MotionValue implementations from `get()`. */
|
|
18
|
+
export declare function reportMotionValueRead(value: MotionValue<unknown>): void;
|
|
19
|
+
export interface CollectedReads<T> {
|
|
20
|
+
readonly result: T;
|
|
21
|
+
readonly reads: ReadonlySet<MotionValue<unknown>>;
|
|
22
|
+
}
|
|
23
|
+
export declare function collectMotionValueReads<T>(compute: () => T): CollectedReads<T>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Dependency collection for reactive derived computes (REQ-API-027's `useTransform(() => expr)`
|
|
2
|
+
// form — the same actual-reads trick the pinned oracle uses). A collection frame records every
|
|
3
|
+
// MotionValue whose `get()` runs inside `compute`; outside a frame the report is a single null
|
|
4
|
+
// check, so the hot read path stays allocation- and branch-cheap. Frames nest (the previous
|
|
5
|
+
// collector is restored on exit) and always close, even when the compute throws — a stuck
|
|
6
|
+
// collector would silently attribute unrelated reads to the wrong derived.
|
|
7
|
+
//
|
|
8
|
+
// Frame-path repair (REQ-DRIVER-021, packet §6 ordinary local-JS bookkeeping): an active
|
|
9
|
+
// collector dedupes into prefilled indexed slots — no Set growth per read in-frame. The public
|
|
10
|
+
// ReadonlySet view is materialized once when the collection closes (a command/render edge),
|
|
11
|
+
// never per read.
|
|
12
|
+
/**
|
|
13
|
+
* The active collection frame. Reads dedupe by an indexed scan of the prefilled slot list —
|
|
14
|
+
* the read count per compute is tiny, so the linear membership check beats a Set's per-add
|
|
15
|
+
* growth on the frame path.
|
|
16
|
+
*/
|
|
17
|
+
export class ActiveMotionValueReadCollector {
|
|
18
|
+
// Prefilled-slot list: capacity grows only to the compute's read high-water mark (below), then
|
|
19
|
+
// is reused — never a fresh array per read.
|
|
20
|
+
#slots = [];
|
|
21
|
+
#count = 0;
|
|
22
|
+
/** @internal Record one read, deduping into the prefilled slots. Called only via reportMotionValueRead. */
|
|
23
|
+
recordUniqueIntoPrefilledSlots(value) {
|
|
24
|
+
const slots = this.#slots;
|
|
25
|
+
const count = this.#count;
|
|
26
|
+
for (let i = 0; i < count; i++) {
|
|
27
|
+
if (slots[i] === value)
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (count === slots.length)
|
|
31
|
+
slots.length = count === 0 ? 8 : count * 2;
|
|
32
|
+
slots[count] = value;
|
|
33
|
+
this.#count = count + 1;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Materialize the public ReadonlySet view ONCE at collection close (the command/render edge
|
|
37
|
+
* that opened the frame) — the repair moves every per-read Set.add off the read path.
|
|
38
|
+
*/
|
|
39
|
+
sealReads() {
|
|
40
|
+
const reads = new Set();
|
|
41
|
+
for (let i = 0; i < this.#count; i++)
|
|
42
|
+
reads.add(this.#slots[i]);
|
|
43
|
+
return reads;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
let activeCollector = null;
|
|
47
|
+
/** @internal Called by MotionValue implementations from `get()`. */
|
|
48
|
+
export function reportMotionValueRead(value) {
|
|
49
|
+
if (activeCollector !== null)
|
|
50
|
+
activeCollector.recordUniqueIntoPrefilledSlots(value);
|
|
51
|
+
}
|
|
52
|
+
export function collectMotionValueReads(compute) {
|
|
53
|
+
const previous = activeCollector;
|
|
54
|
+
const collector = new ActiveMotionValueReadCollector();
|
|
55
|
+
activeCollector = collector;
|
|
56
|
+
try {
|
|
57
|
+
return { result: compute(), reads: collector.sealReads() };
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
activeCollector = previous;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// A hostile array can lie about `length`, grow during iterator traversal, or expose a length that
|
|
3
|
+
// JavaScript cannot allocate. Every public/supplying boundary therefore captures the cardinality ONCE
|
|
4
|
+
// before it allocates or reads an index. Sparse slots stay sparse so the owning schema can name them.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MAX_CAPTURED_ARRAY_LENGTH = void 0;
|
|
7
|
+
exports.capturedArrayDescription = capturedArrayDescription;
|
|
8
|
+
exports.captureBoundedArray = captureBoundedArray;
|
|
9
|
+
exports.captureBoundedCubicBezier = captureBoundedCubicBezier;
|
|
10
|
+
exports.MAX_CAPTURED_ARRAY_LENGTH = 100_000;
|
|
11
|
+
function hasSafeArrayLength(value) {
|
|
12
|
+
return (typeof value === 'number' &&
|
|
13
|
+
Number.isSafeInteger(value) &&
|
|
14
|
+
value >= 0 &&
|
|
15
|
+
value <= exports.MAX_CAPTURED_ARRAY_LENGTH);
|
|
16
|
+
}
|
|
17
|
+
/** A safe error value that never retains a hostile array/proxy for a formatter to reread. */
|
|
18
|
+
function capturedArrayDescription(capture) {
|
|
19
|
+
if (capture.kind === 'not-array')
|
|
20
|
+
return 'a non-array value';
|
|
21
|
+
if (capture.kind === 'invalid-length') {
|
|
22
|
+
const { length } = capture;
|
|
23
|
+
const rendered = length === null ||
|
|
24
|
+
typeof length === 'undefined' ||
|
|
25
|
+
typeof length === 'boolean' ||
|
|
26
|
+
typeof length === 'number' ||
|
|
27
|
+
typeof length === 'bigint' ||
|
|
28
|
+
typeof length === 'string' ||
|
|
29
|
+
typeof length === 'symbol'
|
|
30
|
+
? String(length)
|
|
31
|
+
: `a ${typeof length} value`;
|
|
32
|
+
return `an array reporting length ${rendered}`;
|
|
33
|
+
}
|
|
34
|
+
return `an array (length ${capture.values.length})`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Capture a real array's bounded shape without iteration. Length is read exactly once, then each OWN
|
|
38
|
+
* member is read exactly once. Dependency faults from `length`, `hasOwn`, or an index are intentionally
|
|
39
|
+
* unguarded so their identity survives; malformed cardinality is represented safely for the caller's
|
|
40
|
+
* typed refusal channel.
|
|
41
|
+
*/
|
|
42
|
+
function captureBoundedArray(value) {
|
|
43
|
+
if (!Array.isArray(value))
|
|
44
|
+
return { kind: 'not-array' };
|
|
45
|
+
const length = value.length;
|
|
46
|
+
if (!hasSafeArrayLength(length))
|
|
47
|
+
return { kind: 'invalid-length', length };
|
|
48
|
+
const captured = [];
|
|
49
|
+
captured.length = length;
|
|
50
|
+
for (let index = 0; index < length; index++) {
|
|
51
|
+
if (Object.hasOwn(value, index))
|
|
52
|
+
captured[index] = value[index];
|
|
53
|
+
}
|
|
54
|
+
return { kind: 'captured', values: Object.freeze(captured) };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Capture the only nested timing-array shape the legal schema admits: a four-coordinate
|
|
58
|
+
* cubic-bezier tuple. Its cardinality is checked before reading any member, so malformed
|
|
59
|
+
* nested arrays cannot turn an outer easing list into quadratic work.
|
|
60
|
+
*/
|
|
61
|
+
function captureBoundedCubicBezier(value) {
|
|
62
|
+
if (!Array.isArray(value))
|
|
63
|
+
return { kind: 'not-array' };
|
|
64
|
+
const length = value.length;
|
|
65
|
+
if (!hasSafeArrayLength(length) || length !== 4)
|
|
66
|
+
return { kind: 'invalid-length', length };
|
|
67
|
+
const captured = [];
|
|
68
|
+
captured.length = 4;
|
|
69
|
+
for (let index = 0; index < 4; index++) {
|
|
70
|
+
if (Object.hasOwn(value, index))
|
|
71
|
+
captured[index] = value[index];
|
|
72
|
+
}
|
|
73
|
+
return { kind: 'captured', values: Object.freeze(captured) };
|
|
74
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const MAX_CAPTURED_ARRAY_LENGTH = 100000;
|
|
2
|
+
export type BoundedArrayCapture = {
|
|
3
|
+
readonly kind: 'not-array';
|
|
4
|
+
} | {
|
|
5
|
+
readonly kind: 'invalid-length';
|
|
6
|
+
readonly length: unknown;
|
|
7
|
+
} | {
|
|
8
|
+
readonly kind: 'captured';
|
|
9
|
+
readonly values: readonly unknown[];
|
|
10
|
+
};
|
|
11
|
+
/** A safe error value that never retains a hostile array/proxy for a formatter to reread. */
|
|
12
|
+
export declare function capturedArrayDescription(capture: BoundedArrayCapture): string;
|
|
13
|
+
/**
|
|
14
|
+
* Capture a real array's bounded shape without iteration. Length is read exactly once, then each OWN
|
|
15
|
+
* member is read exactly once. Dependency faults from `length`, `hasOwn`, or an index are intentionally
|
|
16
|
+
* unguarded so their identity survives; malformed cardinality is represented safely for the caller's
|
|
17
|
+
* typed refusal channel.
|
|
18
|
+
*/
|
|
19
|
+
export declare function captureBoundedArray(value: unknown): BoundedArrayCapture;
|
|
20
|
+
/**
|
|
21
|
+
* Capture the only nested timing-array shape the legal schema admits: a four-coordinate
|
|
22
|
+
* cubic-bezier tuple. Its cardinality is checked before reading any member, so malformed
|
|
23
|
+
* nested arrays cannot turn an outer easing list into quadratic work.
|
|
24
|
+
*/
|
|
25
|
+
export declare function captureBoundedCubicBezier(value: unknown): BoundedArrayCapture;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const MAX_CAPTURED_ARRAY_LENGTH = 100000;
|
|
2
|
+
export type BoundedArrayCapture = {
|
|
3
|
+
readonly kind: 'not-array';
|
|
4
|
+
} | {
|
|
5
|
+
readonly kind: 'invalid-length';
|
|
6
|
+
readonly length: unknown;
|
|
7
|
+
} | {
|
|
8
|
+
readonly kind: 'captured';
|
|
9
|
+
readonly values: readonly unknown[];
|
|
10
|
+
};
|
|
11
|
+
/** A safe error value that never retains a hostile array/proxy for a formatter to reread. */
|
|
12
|
+
export declare function capturedArrayDescription(capture: BoundedArrayCapture): string;
|
|
13
|
+
/**
|
|
14
|
+
* Capture a real array's bounded shape without iteration. Length is read exactly once, then each OWN
|
|
15
|
+
* member is read exactly once. Dependency faults from `length`, `hasOwn`, or an index are intentionally
|
|
16
|
+
* unguarded so their identity survives; malformed cardinality is represented safely for the caller's
|
|
17
|
+
* typed refusal channel.
|
|
18
|
+
*/
|
|
19
|
+
export declare function captureBoundedArray(value: unknown): BoundedArrayCapture;
|
|
20
|
+
/**
|
|
21
|
+
* Capture the only nested timing-array shape the legal schema admits: a four-coordinate
|
|
22
|
+
* cubic-bezier tuple. Its cardinality is checked before reading any member, so malformed
|
|
23
|
+
* nested arrays cannot turn an outer easing list into quadratic work.
|
|
24
|
+
*/
|
|
25
|
+
export declare function captureBoundedCubicBezier(value: unknown): BoundedArrayCapture;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// A hostile array can lie about `length`, grow during iterator traversal, or expose a length that
|
|
2
|
+
// JavaScript cannot allocate. Every public/supplying boundary therefore captures the cardinality ONCE
|
|
3
|
+
// before it allocates or reads an index. Sparse slots stay sparse so the owning schema can name them.
|
|
4
|
+
export const MAX_CAPTURED_ARRAY_LENGTH = 100_000;
|
|
5
|
+
function hasSafeArrayLength(value) {
|
|
6
|
+
return (typeof value === 'number' &&
|
|
7
|
+
Number.isSafeInteger(value) &&
|
|
8
|
+
value >= 0 &&
|
|
9
|
+
value <= MAX_CAPTURED_ARRAY_LENGTH);
|
|
10
|
+
}
|
|
11
|
+
/** A safe error value that never retains a hostile array/proxy for a formatter to reread. */
|
|
12
|
+
export function capturedArrayDescription(capture) {
|
|
13
|
+
if (capture.kind === 'not-array')
|
|
14
|
+
return 'a non-array value';
|
|
15
|
+
if (capture.kind === 'invalid-length') {
|
|
16
|
+
const { length } = capture;
|
|
17
|
+
const rendered = length === null ||
|
|
18
|
+
typeof length === 'undefined' ||
|
|
19
|
+
typeof length === 'boolean' ||
|
|
20
|
+
typeof length === 'number' ||
|
|
21
|
+
typeof length === 'bigint' ||
|
|
22
|
+
typeof length === 'string' ||
|
|
23
|
+
typeof length === 'symbol'
|
|
24
|
+
? String(length)
|
|
25
|
+
: `a ${typeof length} value`;
|
|
26
|
+
return `an array reporting length ${rendered}`;
|
|
27
|
+
}
|
|
28
|
+
return `an array (length ${capture.values.length})`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Capture a real array's bounded shape without iteration. Length is read exactly once, then each OWN
|
|
32
|
+
* member is read exactly once. Dependency faults from `length`, `hasOwn`, or an index are intentionally
|
|
33
|
+
* unguarded so their identity survives; malformed cardinality is represented safely for the caller's
|
|
34
|
+
* typed refusal channel.
|
|
35
|
+
*/
|
|
36
|
+
export function captureBoundedArray(value) {
|
|
37
|
+
if (!Array.isArray(value))
|
|
38
|
+
return { kind: 'not-array' };
|
|
39
|
+
const length = value.length;
|
|
40
|
+
if (!hasSafeArrayLength(length))
|
|
41
|
+
return { kind: 'invalid-length', length };
|
|
42
|
+
const captured = [];
|
|
43
|
+
captured.length = length;
|
|
44
|
+
for (let index = 0; index < length; index++) {
|
|
45
|
+
if (Object.hasOwn(value, index))
|
|
46
|
+
captured[index] = value[index];
|
|
47
|
+
}
|
|
48
|
+
return { kind: 'captured', values: Object.freeze(captured) };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Capture the only nested timing-array shape the legal schema admits: a four-coordinate
|
|
52
|
+
* cubic-bezier tuple. Its cardinality is checked before reading any member, so malformed
|
|
53
|
+
* nested arrays cannot turn an outer easing list into quadratic work.
|
|
54
|
+
*/
|
|
55
|
+
export function captureBoundedCubicBezier(value) {
|
|
56
|
+
if (!Array.isArray(value))
|
|
57
|
+
return { kind: 'not-array' };
|
|
58
|
+
const length = value.length;
|
|
59
|
+
if (!hasSafeArrayLength(length) || length !== 4)
|
|
60
|
+
return { kind: 'invalid-length', length };
|
|
61
|
+
const captured = [];
|
|
62
|
+
captured.length = 4;
|
|
63
|
+
for (let index = 0; index < 4; index++) {
|
|
64
|
+
if (Object.hasOwn(value, index))
|
|
65
|
+
captured[index] = value[index];
|
|
66
|
+
}
|
|
67
|
+
return { kind: 'captured', values: Object.freeze(captured) };
|
|
68
|
+
}
|