@forgeax/engine-ecs 0.0.0-dev.8d955ade1c79
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 +202 -0
- package/README.md +278 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__fixtures__/field-shape-fixture.d.ts +16 -0
- package/dist/__fixtures__/field-shape-fixture.d.ts.map +1 -0
- package/dist/__tests__/__fixtures__/inspect-scripts.snapshot.d.ts +3 -0
- package/dist/__tests__/__fixtures__/inspect-scripts.snapshot.d.ts.map +1 -0
- package/dist/__tests__/archetype.unit.test.d.ts +2 -0
- package/dist/__tests__/archetype.unit.test.d.ts.map +1 -0
- package/dist/__tests__/array-handle-element.test-d.d.ts +2 -0
- package/dist/__tests__/array-handle-element.test-d.d.ts.map +1 -0
- package/dist/__tests__/buffer-array-vocab.test-d.d.ts +2 -0
- package/dist/__tests__/buffer-array-vocab.test-d.d.ts.map +1 -0
- package/dist/__tests__/buffer-field-input-widen.unit.test.d.ts +2 -0
- package/dist/__tests__/buffer-field-input-widen.unit.test.d.ts.map +1 -0
- package/dist/__tests__/buffer-pool.test-d.d.ts +2 -0
- package/dist/__tests__/buffer-pool.test-d.d.ts.map +1 -0
- package/dist/__tests__/change-detection.unit.test.d.ts +2 -0
- package/dist/__tests__/change-detection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/command-buffer.test.d.ts +2 -0
- package/dist/__tests__/command-buffer.test.d.ts.map +1 -0
- package/dist/__tests__/commands-deferred.unit.test.d.ts +2 -0
- package/dist/__tests__/commands-deferred.unit.test.d.ts.map +1 -0
- package/dist/__tests__/component-catalog.test.d.ts +2 -0
- package/dist/__tests__/component-catalog.test.d.ts.map +1 -0
- package/dist/__tests__/component-definition-identity.unit.test.d.ts +2 -0
- package/dist/__tests__/component-definition-identity.unit.test.d.ts.map +1 -0
- package/dist/__tests__/component-nan-preflight.unit.test.d.ts +2 -0
- package/dist/__tests__/component-nan-preflight.unit.test.d.ts.map +1 -0
- package/dist/__tests__/component-schema.test-d.d.ts +2 -0
- package/dist/__tests__/component-schema.test-d.d.ts.map +1 -0
- package/dist/__tests__/component-schema.test.d.ts +2 -0
- package/dist/__tests__/component-schema.test.d.ts.map +1 -0
- package/dist/__tests__/define-component-options.test-d.d.ts +2 -0
- package/dist/__tests__/define-component-options.test-d.d.ts.map +1 -0
- package/dist/__tests__/deprecated-vocab-rejection.unit.test.d.ts +2 -0
- package/dist/__tests__/deprecated-vocab-rejection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/despawn-all.unit.test.d.ts +2 -0
- package/dist/__tests__/despawn-all.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ecs-core-reduction.characterization.test.d.ts +2 -0
- package/dist/__tests__/ecs-core-reduction.characterization.test.d.ts.map +1 -0
- package/dist/__tests__/ecs-core-reduction.perf.test.d.ts +2 -0
- package/dist/__tests__/ecs-core-reduction.perf.test.d.ts.map +1 -0
- package/dist/__tests__/ecs-error-code-31.test-d.d.ts +2 -0
- package/dist/__tests__/ecs-error-code-31.test-d.d.ts.map +1 -0
- package/dist/__tests__/entity-disabling.unit.test.d.ts +2 -0
- package/dist/__tests__/entity-disabling.unit.test.d.ts.map +1 -0
- package/dist/__tests__/entity-handle-codec-parity.unit.test.d.ts +2 -0
- package/dist/__tests__/entity-handle-codec-parity.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors.unit.test.d.ts +2 -0
- package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-conflict-boundary.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-conflict-boundary.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-shared-storage.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-shared-storage.unit.test.d.ts.map +1 -0
- package/dist/__tests__/externalization-portable-value.unit.test.d.ts +2 -0
- package/dist/__tests__/externalization-portable-value.unit.test.d.ts.map +1 -0
- package/dist/__tests__/externalization-projection.unit.test.d.ts +2 -0
- package/dist/__tests__/externalization-projection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/externalization-render-read-lease.test-d.d.ts +2 -0
- package/dist/__tests__/externalization-render-read-lease.test-d.d.ts.map +1 -0
- package/dist/__tests__/externalization-render-read-lease.unit.test.d.ts +2 -0
- package/dist/__tests__/externalization-render-read-lease.unit.test.d.ts.map +1 -0
- package/dist/__tests__/externalization-root-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/externalization-root-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/field-enum-labels-reflection.test.d.ts +2 -0
- package/dist/__tests__/field-enum-labels-reflection.test.d.ts.map +1 -0
- package/dist/__tests__/field-shape-fixture.test.d.ts +2 -0
- package/dist/__tests__/field-shape-fixture.test.d.ts.map +1 -0
- package/dist/__tests__/field-transient-reflection.test.d.ts +2 -0
- package/dist/__tests__/field-transient-reflection.test.d.ts.map +1 -0
- package/dist/__tests__/field-value-type.test-d.d.ts +2 -0
- package/dist/__tests__/field-value-type.test-d.d.ts.map +1 -0
- package/dist/__tests__/fixed-update.unit.test.d.ts +2 -0
- package/dist/__tests__/fixed-update.unit.test.d.ts.map +1 -0
- package/dist/__tests__/handle.test-d.d.ts +2 -0
- package/dist/__tests__/handle.test-d.d.ts.map +1 -0
- package/dist/__tests__/hierarchy.unit.test.d.ts +2 -0
- package/dist/__tests__/hierarchy.unit.test.d.ts.map +1 -0
- package/dist/__tests__/managed-array-vocab.test-d.d.ts +2 -0
- package/dist/__tests__/managed-array-vocab.test-d.d.ts.map +1 -0
- package/dist/__tests__/minimal-example.test-d.d.ts +2 -0
- package/dist/__tests__/minimal-example.test-d.d.ts.map +1 -0
- package/dist/__tests__/name-read-types.test-d.d.ts +2 -0
- package/dist/__tests__/name-read-types.test-d.d.ts.map +1 -0
- package/dist/__tests__/plugin-service.test.d.ts +2 -0
- package/dist/__tests__/plugin-service.test.d.ts.map +1 -0
- package/dist/__tests__/query-combinations.unit.test.d.ts +2 -0
- package/dist/__tests__/query-combinations.unit.test.d.ts.map +1 -0
- package/dist/__tests__/query-light-extract-flat-column-ratio.perf.test.d.ts +2 -0
- package/dist/__tests__/query-light-extract-flat-column-ratio.perf.test.d.ts.map +1 -0
- package/dist/__tests__/query-storage-trends.perf.test.d.ts +2 -0
- package/dist/__tests__/query-storage-trends.perf.test.d.ts.map +1 -0
- package/dist/__tests__/query-trs-flat-column-ratio.perf.test.d.ts +2 -0
- package/dist/__tests__/query-trs-flat-column-ratio.perf.test.d.ts.map +1 -0
- package/dist/__tests__/query.unit.test.d.ts +2 -0
- package/dist/__tests__/query.unit.test.d.ts.map +1 -0
- package/dist/__tests__/relationship-index.perf.test.d.ts +2 -0
- package/dist/__tests__/relationship-index.perf.test.d.ts.map +1 -0
- package/dist/__tests__/relationship-index.test.d.ts +2 -0
- package/dist/__tests__/relationship-index.test.d.ts.map +1 -0
- package/dist/__tests__/scalar-field-type-owner.test-d.d.ts +2 -0
- package/dist/__tests__/scalar-field-type-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/schedule-determinism.unit.test.d.ts +2 -0
- package/dist/__tests__/schedule-determinism.unit.test.d.ts.map +1 -0
- package/dist/__tests__/schedule-inspection.unit.test.d.ts +2 -0
- package/dist/__tests__/schedule-inspection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/schedule-scope-error.unit.test.d.ts +2 -0
- package/dist/__tests__/schedule-scope-error.unit.test.d.ts.map +1 -0
- package/dist/__tests__/schedule-token.unit.test.d.ts +2 -0
- package/dist/__tests__/schedule-token.unit.test.d.ts.map +1 -0
- package/dist/__tests__/schema-vocab.test-d.d.ts +2 -0
- package/dist/__tests__/schema-vocab.test-d.d.ts.map +1 -0
- package/dist/__tests__/serialization.unit.test.d.ts +2 -0
- package/dist/__tests__/serialization.unit.test.d.ts.map +1 -0
- package/dist/__tests__/shared-kernel-eligibility.test-d.d.ts +2 -0
- package/dist/__tests__/shared-kernel-eligibility.test-d.d.ts.map +1 -0
- package/dist/__tests__/shared-kernel-eligibility.unit.test.d.ts +2 -0
- package/dist/__tests__/shared-kernel-eligibility.unit.test.d.ts.map +1 -0
- package/dist/__tests__/shared-ref-store.unit.test.d.ts +2 -0
- package/dist/__tests__/shared-ref-store.unit.test.d.ts.map +1 -0
- package/dist/__tests__/sparse-tag.unit.test.d.ts +2 -0
- package/dist/__tests__/sparse-tag.unit.test.d.ts.map +1 -0
- package/dist/__tests__/spawn-field-error.test-d.d.ts +2 -0
- package/dist/__tests__/spawn-field-error.test-d.d.ts.map +1 -0
- package/dist/__tests__/stale-error.test-d.d.ts +2 -0
- package/dist/__tests__/stale-error.test-d.d.ts.map +1 -0
- package/dist/__tests__/string-view-symmetry.test-d.d.ts +2 -0
- package/dist/__tests__/string-view-symmetry.test-d.d.ts.map +1 -0
- package/dist/__tests__/string-vocab.test-d.d.ts +2 -0
- package/dist/__tests__/string-vocab.test-d.d.ts.map +1 -0
- package/dist/__tests__/structure-epoch-boundaries.unit.test.d.ts +2 -0
- package/dist/__tests__/structure-epoch-boundaries.unit.test.d.ts.map +1 -0
- package/dist/__tests__/structure-epoch.unit.test.d.ts +2 -0
- package/dist/__tests__/structure-epoch.unit.test.d.ts.map +1 -0
- package/dist/__tests__/table-archetype-mapping.unit.test.d.ts +2 -0
- package/dist/__tests__/table-archetype-mapping.unit.test.d.ts.map +1 -0
- package/dist/__tests__/time.unit.test.d.ts +2 -0
- package/dist/__tests__/time.unit.test.d.ts.map +1 -0
- package/dist/__tests__/types.test-d.d.ts +2 -0
- package/dist/__tests__/types.test-d.d.ts.map +1 -0
- package/dist/__tests__/unique-ref-store.unit.test.d.ts +2 -0
- package/dist/__tests__/unique-ref-store.unit.test.d.ts.map +1 -0
- package/dist/__tests__/utils/handle-numeric.d.ts +14 -0
- package/dist/__tests__/utils/handle-numeric.d.ts.map +1 -0
- package/dist/__tests__/validation-root-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/validation-root-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/visibility-atomic-write.unit.test.d.ts +2 -0
- package/dist/__tests__/visibility-atomic-write.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world-change-journal.unit.test.d.ts +2 -0
- package/dist/__tests__/world-change-journal.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world-characterization.unit.test.d.ts +2 -0
- package/dist/__tests__/world-characterization.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world-consumer-inference.test-d.d.ts +2 -0
- package/dist/__tests__/world-consumer-inference.test-d.d.ts.map +1 -0
- package/dist/__tests__/world-health.contract.test.d.ts +2 -0
- package/dist/__tests__/world-health.contract.test.d.ts.map +1 -0
- package/dist/__tests__/world-inspection.test.d.ts +2 -0
- package/dist/__tests__/world-inspection.test.d.ts.map +1 -0
- package/dist/__tests__/world-internal-identity.unit.test.d.ts +2 -0
- package/dist/__tests__/world-internal-identity.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world-managed-roundtrip.unit.test.d.ts +2 -0
- package/dist/__tests__/world-managed-roundtrip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world-scheduling.m4.test.d.ts +2 -0
- package/dist/__tests__/world-scheduling.m4.test.d.ts.map +1 -0
- package/dist/__tests__/world-unique-roundtrip.unit.test.d.ts +2 -0
- package/dist/__tests__/world-unique-roundtrip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/world.test-d.d.ts +2 -0
- package/dist/__tests__/world.test-d.d.ts.map +1 -0
- package/dist/buffer-pool.d.ts +109 -0
- package/dist/buffer-pool.d.ts.map +1 -0
- package/dist/commands.d.ts +93 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/component-default-fallback.d.ts +88 -0
- package/dist/component-default-fallback.d.ts.map +1 -0
- package/dist/component-schema.d.ts +34 -0
- package/dist/component-schema.d.ts.map +1 -0
- package/dist/component-value-validate.d.ts +29 -0
- package/dist/component-value-validate.d.ts.map +1 -0
- package/dist/component.d.ts +581 -0
- package/dist/component.d.ts.map +1 -0
- package/dist/entity-handle.d.ts +68 -0
- package/dist/entity-handle.d.ts.map +1 -0
- package/dist/entity.d.ts +86 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/errors/query-and-component-errors.d.ts +132 -0
- package/dist/errors/query-and-component-errors.d.ts.map +1 -0
- package/dist/errors/relationship-errors.d.ts +112 -0
- package/dist/errors/relationship-errors.d.ts.map +1 -0
- package/dist/errors/sprite-and-shared-errors.d.ts +107 -0
- package/dist/errors/sprite-and-shared-errors.d.ts.map +1 -0
- package/dist/errors/validation-errors.d.ts +229 -0
- package/dist/errors/validation-errors.d.ts.map +1 -0
- package/dist/errors.d.ts +763 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/execution/shared-kernel.d.ts +106 -0
- package/dist/execution/shared-kernel.d.ts.map +1 -0
- package/dist/externalization/index.d.ts +31 -0
- package/dist/externalization/index.d.ts.map +1 -0
- package/dist/externalization/index.mjs +298 -0
- package/dist/externalization/index.mjs.map +1 -0
- package/dist/index.d.ts +261 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +7422 -0
- package/dist/index.mjs.map +1 -0
- package/dist/internal.mjs +479 -0
- package/dist/internal.mjs.map +1 -0
- package/dist/plugin-service.d.ts +11 -0
- package/dist/plugin-service.d.ts.map +1 -0
- package/dist/projection/index.d.ts +103 -0
- package/dist/projection/index.d.ts.map +1 -0
- package/dist/projection/index.mjs +615 -0
- package/dist/projection/index.mjs.map +1 -0
- package/dist/query/query.d.ts +58 -0
- package/dist/query/query.d.ts.map +1 -0
- package/dist/relationship-index.d.ts +81 -0
- package/dist/relationship-index.d.ts.map +1 -0
- package/dist/resource.d.ts +70 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/schedule-token.d.ts +16 -0
- package/dist/schedule-token.d.ts.map +1 -0
- package/dist/schedule.d.ts +271 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/shared-ref-store.d.ts +144 -0
- package/dist/shared-ref-store.d.ts.map +1 -0
- package/dist/shared.d.ts +3 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.mjs +364 -0
- package/dist/shared.mjs.map +1 -0
- package/dist/storage/archetype-graph.d.ts +54 -0
- package/dist/storage/archetype-graph.d.ts.map +1 -0
- package/dist/storage/archetype.d.ts +23 -0
- package/dist/storage/archetype.d.ts.map +1 -0
- package/dist/storage/change-detection.d.ts +40 -0
- package/dist/storage/change-detection.d.ts.map +1 -0
- package/dist/storage/column.d.ts +111 -0
- package/dist/storage/column.d.ts.map +1 -0
- package/dist/storage/table.d.ts +29 -0
- package/dist/storage/table.d.ts.map +1 -0
- package/dist/time.d.ts +57 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/unique-ref-store.d.ts +105 -0
- package/dist/unique-ref-store.d.ts.map +1 -0
- package/dist/world-change-journal.d.ts +34 -0
- package/dist/world-change-journal.d.ts.map +1 -0
- package/dist/world-component-access.d.ts +265 -0
- package/dist/world-component-access.d.ts.map +1 -0
- package/dist/world-component-storage.d.ts +282 -0
- package/dist/world-component-storage.d.ts.map +1 -0
- package/dist/world-core.d.ts +39 -0
- package/dist/world-core.d.ts.map +1 -0
- package/dist/world-entity-lifecycle.d.ts +30 -0
- package/dist/world-entity-lifecycle.d.ts.map +1 -0
- package/dist/world-internal.d.ts +14 -0
- package/dist/world-internal.d.ts.map +1 -0
- package/dist/world-scheduling.d.ts +30 -0
- package/dist/world-scheduling.d.ts.map +1 -0
- package/dist/world.d.ts +620 -0
- package/dist/world.d.ts.map +1 -0
- package/package.json +77 -0
- package/src/__fixtures__/field-shape-fixture.ts +30 -0
- package/src/__tests__/__fixtures__/inspect-scripts.snapshot.ts +2 -0
- package/src/__tests__/archetype.unit.test.ts +1837 -0
- package/src/__tests__/array-handle-element.test-d.ts +34 -0
- package/src/__tests__/buffer-array-vocab.test-d.ts +53 -0
- package/src/__tests__/buffer-field-input-widen.unit.test.ts +99 -0
- package/src/__tests__/buffer-pool.test-d.ts +32 -0
- package/src/__tests__/change-detection.unit.test.ts +86 -0
- package/src/__tests__/command-buffer.test.ts +179 -0
- package/src/__tests__/commands-deferred.unit.test.ts +68 -0
- package/src/__tests__/component-catalog.test.ts +59 -0
- package/src/__tests__/component-definition-identity.unit.test.ts +24 -0
- package/src/__tests__/component-nan-preflight.unit.test.ts +186 -0
- package/src/__tests__/component-schema.test-d.ts +119 -0
- package/src/__tests__/component-schema.test.ts +55 -0
- package/src/__tests__/define-component-options.test-d.ts +35 -0
- package/src/__tests__/deprecated-vocab-rejection.unit.test.ts +59 -0
- package/src/__tests__/despawn-all.unit.test.ts +17 -0
- package/src/__tests__/ecs-core-reduction.characterization.test.ts +130 -0
- package/src/__tests__/ecs-core-reduction.perf.test.ts +260 -0
- package/src/__tests__/ecs-error-code-31.test-d.ts +42 -0
- package/src/__tests__/entity-disabling.unit.test.ts +37 -0
- package/src/__tests__/entity-handle-codec-parity.unit.test.ts +126 -0
- package/src/__tests__/errors.unit.test.ts +1456 -0
- package/src/__tests__/execution-conflict-boundary.unit.test.ts +123 -0
- package/src/__tests__/execution-shared-storage.unit.test.ts +40 -0
- package/src/__tests__/externalization-portable-value.unit.test.ts +275 -0
- package/src/__tests__/externalization-projection.unit.test.ts +325 -0
- package/src/__tests__/externalization-render-read-lease.test-d.ts +30 -0
- package/src/__tests__/externalization-render-read-lease.unit.test.ts +79 -0
- package/src/__tests__/externalization-root-surface.unit.test.ts +33 -0
- package/src/__tests__/field-enum-labels-reflection.test.ts +66 -0
- package/src/__tests__/field-shape-fixture.test.ts +61 -0
- package/src/__tests__/field-transient-reflection.test.ts +59 -0
- package/src/__tests__/field-value-type.test-d.ts +57 -0
- package/src/__tests__/fixed-update.unit.test.ts +78 -0
- package/src/__tests__/handle.test-d.ts +86 -0
- package/src/__tests__/hierarchy.unit.test.ts +2534 -0
- package/src/__tests__/managed-array-vocab.test-d.ts +137 -0
- package/src/__tests__/minimal-example.test-d.ts +42 -0
- package/src/__tests__/name-read-types.test-d.ts +72 -0
- package/src/__tests__/plugin-service.test.ts +13 -0
- package/src/__tests__/query-combinations.unit.test.ts +42 -0
- package/src/__tests__/query-light-extract-flat-column-ratio.perf.test.ts +252 -0
- package/src/__tests__/query-storage-trends.perf.test.ts +104 -0
- package/src/__tests__/query-trs-flat-column-ratio.perf.test.ts +194 -0
- package/src/__tests__/query.unit.test.ts +196 -0
- package/src/__tests__/relationship-index.perf.test.ts +126 -0
- package/src/__tests__/relationship-index.test.ts +70 -0
- package/src/__tests__/scalar-field-type-owner.test-d.ts +71 -0
- package/src/__tests__/schedule-determinism.unit.test.ts +36 -0
- package/src/__tests__/schedule-inspection.unit.test.ts +81 -0
- package/src/__tests__/schedule-scope-error.unit.test.ts +63 -0
- package/src/__tests__/schedule-token.unit.test.ts +54 -0
- package/src/__tests__/schema-vocab.test-d.ts +103 -0
- package/src/__tests__/serialization.unit.test.ts +2740 -0
- package/src/__tests__/shared-kernel-eligibility.test-d.ts +18 -0
- package/src/__tests__/shared-kernel-eligibility.unit.test.ts +78 -0
- package/src/__tests__/shared-ref-store.unit.test.ts +646 -0
- package/src/__tests__/sparse-tag.unit.test.ts +201 -0
- package/src/__tests__/spawn-field-error.test-d.ts +41 -0
- package/src/__tests__/stale-error.test-d.ts +208 -0
- package/src/__tests__/string-view-symmetry.test-d.ts +26 -0
- package/src/__tests__/string-vocab.test-d.ts +49 -0
- package/src/__tests__/structure-epoch-boundaries.unit.test.ts +39 -0
- package/src/__tests__/structure-epoch.unit.test.ts +48 -0
- package/src/__tests__/table-archetype-mapping.unit.test.ts +78 -0
- package/src/__tests__/time.unit.test.ts +79 -0
- package/src/__tests__/types.test-d.ts +38 -0
- package/src/__tests__/unique-ref-store.unit.test.ts +348 -0
- package/src/__tests__/utils/handle-numeric.ts +30 -0
- package/src/__tests__/validation-root-surface.unit.test.ts +15 -0
- package/src/__tests__/visibility-atomic-write.unit.test.ts +82 -0
- package/src/__tests__/world-change-journal.unit.test.ts +97 -0
- package/src/__tests__/world-characterization.unit.test.ts +408 -0
- package/src/__tests__/world-consumer-inference.test-d.ts +16 -0
- package/src/__tests__/world-health.contract.test.ts +87 -0
- package/src/__tests__/world-inspection.test.ts +37 -0
- package/src/__tests__/world-internal-identity.unit.test.ts +17 -0
- package/src/__tests__/world-managed-roundtrip.unit.test.ts +98 -0
- package/src/__tests__/world-scheduling.m4.test.ts +52 -0
- package/src/__tests__/world-unique-roundtrip.unit.test.ts +357 -0
- package/src/__tests__/world.test-d.ts +156 -0
- package/src/buffer-pool.ts +357 -0
- package/src/commands.ts +644 -0
- package/src/component-default-fallback.ts +236 -0
- package/src/component-schema.ts +92 -0
- package/src/component-value-validate.ts +135 -0
- package/src/component.ts +1268 -0
- package/src/entity-handle.ts +104 -0
- package/src/entity.ts +140 -0
- package/src/errors/query-and-component-errors.ts +200 -0
- package/src/errors/relationship-errors.ts +198 -0
- package/src/errors/sprite-and-shared-errors.ts +231 -0
- package/src/errors/validation-errors.ts +506 -0
- package/src/errors.ts +1464 -0
- package/src/execution/shared-kernel.ts +392 -0
- package/src/externalization/index.ts +170 -0
- package/src/index.ts +414 -0
- package/src/plugin-service.ts +35 -0
- package/src/projection/index.ts +318 -0
- package/src/query/query.ts +606 -0
- package/src/relationship-index.ts +175 -0
- package/src/resource.ts +106 -0
- package/src/schedule-token.ts +40 -0
- package/src/schedule.ts +846 -0
- package/src/shared-ref-store.ts +418 -0
- package/src/shared.ts +23 -0
- package/src/storage/archetype-graph.ts +166 -0
- package/src/storage/archetype.ts +78 -0
- package/src/storage/change-detection.ts +231 -0
- package/src/storage/column.ts +247 -0
- package/src/storage/table.ts +153 -0
- package/src/time.ts +122 -0
- package/src/unique-ref-store.ts +278 -0
- package/src/world-change-journal.ts +84 -0
- package/src/world-component-access.ts +1421 -0
- package/src/world-component-storage.ts +1178 -0
- package/src/world-core.ts +74 -0
- package/src/world-entity-lifecycle.ts +539 -0
- package/src/world-internal.ts +75 -0
- package/src/world-scheduling.ts +499 -0
- package/src/world.ts +1399 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { componentId, defineComponent } from '../component';
|
|
3
|
+
import { Entity } from '../entity';
|
|
4
|
+
import type { EntityHandle } from '../entity-handle';
|
|
5
|
+
import { appendArchetypeRow, growArchetype, removeArchetypeRow } from '../storage/archetype';
|
|
6
|
+
import { createArchetypeGraph, getOrCreateArchetype } from '../storage/archetype-graph';
|
|
7
|
+
import { appendTableRow, growTable, removeTableRow } from '../storage/table';
|
|
8
|
+
import { World } from '../world';
|
|
9
|
+
|
|
10
|
+
describe('Table and Archetype ownership', () => {
|
|
11
|
+
it('stores component columns only on Table and logical membership only on Archetype', () => {
|
|
12
|
+
const Position = defineComponent('TableOwnerPosition', { x: 'f32' });
|
|
13
|
+
const graph = createArchetypeGraph();
|
|
14
|
+
const archetype = getOrCreateArchetype(graph, [componentId(Position)], [Position]);
|
|
15
|
+
const table = graph.tables[archetype.tableId];
|
|
16
|
+
if (table === undefined) throw new Error('table missing');
|
|
17
|
+
|
|
18
|
+
expect(table.storage.has(componentId(Entity))).toBe(true);
|
|
19
|
+
expect(table.storage.has(componentId(Position))).toBe(true);
|
|
20
|
+
expect(archetype.rows).toBeInstanceOf(Uint32Array);
|
|
21
|
+
expect('storage' in archetype).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('grows logical and physical capacities independently', () => {
|
|
25
|
+
const Position = defineComponent('IndependentCapacityPosition', { x: 'f32' });
|
|
26
|
+
const graph = createArchetypeGraph();
|
|
27
|
+
const archetype = getOrCreateArchetype(graph, [componentId(Position)], [Position]);
|
|
28
|
+
const table = graph.tables[archetype.tableId];
|
|
29
|
+
if (table === undefined) throw new Error('table missing');
|
|
30
|
+
const archetypeCapacity = archetype.capacity;
|
|
31
|
+
const tableCapacity = table.capacity;
|
|
32
|
+
|
|
33
|
+
growArchetype(archetype, archetypeCapacity * 2);
|
|
34
|
+
expect(archetype.capacity).toBe(archetypeCapacity * 2);
|
|
35
|
+
expect(table.capacity).toBe(tableCapacity);
|
|
36
|
+
|
|
37
|
+
growTable(table, tableCapacity * 2);
|
|
38
|
+
expect(table.capacity).toBe(tableCapacity * 2);
|
|
39
|
+
expect(archetype.capacity).toBe(archetypeCapacity * 2);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('keeps archetypeRow to tableRow explicit across independent swap-pop', () => {
|
|
43
|
+
const Position = defineComponent('SwapMappingPosition', { x: 'f32' });
|
|
44
|
+
const graph = createArchetypeGraph();
|
|
45
|
+
const archetype = getOrCreateArchetype(graph, [componentId(Position)], [Position]);
|
|
46
|
+
const table = graph.tables[archetype.tableId];
|
|
47
|
+
if (table === undefined) throw new Error('table missing');
|
|
48
|
+
const first = 0x01000001 as EntityHandle;
|
|
49
|
+
const second = 0x02000002 as EntityHandle;
|
|
50
|
+
const firstTableRow = appendTableRow(table, first);
|
|
51
|
+
const secondTableRow = appendTableRow(table, second);
|
|
52
|
+
appendArchetypeRow(archetype, firstTableRow);
|
|
53
|
+
appendArchetypeRow(archetype, secondTableRow);
|
|
54
|
+
|
|
55
|
+
expect(removeArchetypeRow(archetype, 0)).toEqual({ movedTableRow: 1, newRow: 0 });
|
|
56
|
+
expect(archetype.rows[0]).toBe(1);
|
|
57
|
+
expect(removeTableRow(table, 0)).toEqual({ movedEntity: second, newRow: 0 });
|
|
58
|
+
expect(table.storage.get(componentId(Entity))?.fields.get('self')?.view[0]).toBe(second);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('world migration and despawn maintain both mapping layers', () => {
|
|
62
|
+
const Position = defineComponent('WorldMappingPosition', { x: 'f32' });
|
|
63
|
+
const Selected = defineComponent('WorldMappingSelected', {});
|
|
64
|
+
const world = new World();
|
|
65
|
+
const first = world.spawn({ component: Position, data: { x: 1 } }).unwrap();
|
|
66
|
+
const second = world.spawn({ component: Position, data: { x: 2 } }).unwrap();
|
|
67
|
+
|
|
68
|
+
world.addComponent(first, { component: Selected, data: {} }).unwrap();
|
|
69
|
+
expect(world.get(first, Position).unwrap().x).toBe(1);
|
|
70
|
+
expect(world.get(second, Position).unwrap().x).toBe(2);
|
|
71
|
+
|
|
72
|
+
world.despawn(first).unwrap();
|
|
73
|
+
expect(world.get(second, Entity).unwrap().self).toBe(second);
|
|
74
|
+
expect([...world.query({ read: [Position] }).unwrap()].map((row) => row.entity)).toEqual([
|
|
75
|
+
second,
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { FixedUpdate, Update } from '../schedule-token';
|
|
3
|
+
import { FixedTime, Time } from '../time';
|
|
4
|
+
import { World } from '../world';
|
|
5
|
+
|
|
6
|
+
describe('World time resources', () => {
|
|
7
|
+
it('rejects negative, NaN, and infinite deltas without mutating time', () => {
|
|
8
|
+
const world = new World();
|
|
9
|
+
const before = world.getResource(Time);
|
|
10
|
+
|
|
11
|
+
for (const delta of [-1, Number.NaN, Number.POSITIVE_INFINITY]) {
|
|
12
|
+
const result = world.update(delta);
|
|
13
|
+
expect(result.ok).toBe(false);
|
|
14
|
+
if (!result.ok) {
|
|
15
|
+
expect(result.error.code).toBe('time-delta-invalid');
|
|
16
|
+
expect(result.error.expected).toBe('a finite delta greater than or equal to 0');
|
|
17
|
+
expect(result.error.hint).toContain('world.update');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
expect(world.getResource(Time)).toEqual(before);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('treats an omitted delta as zero while still running Update', () => {
|
|
25
|
+
const world = new World();
|
|
26
|
+
let updates = 0;
|
|
27
|
+
world.addSystem(Update, { name: 'update', queries: [], fn: () => updates++ });
|
|
28
|
+
|
|
29
|
+
expect(world.update().ok).toBe(true);
|
|
30
|
+
expect(world.getResource(Time)).toMatchObject({ delta: 0, elapsed: 0 });
|
|
31
|
+
expect(world.getResource(FixedTime).tick).toBe(0);
|
|
32
|
+
expect(updates).toBe(1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('advances Time by an explicit positive delta', () => {
|
|
36
|
+
const world = new World();
|
|
37
|
+
|
|
38
|
+
expect(world.update(0.025).ok).toBe(true);
|
|
39
|
+
expect(world.getResource(Time)).toMatchObject({ delta: 0.025, elapsed: 0.025 });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('rejects an incoherent policy before writing protected resources', () => {
|
|
43
|
+
const world = new World({
|
|
44
|
+
time: { fixedDeltaSeconds: 1 / 60, maxStepsPerUpdate: 4, maxDeltaSeconds: 0.05 },
|
|
45
|
+
});
|
|
46
|
+
const beforeTime = world.getResource(Time);
|
|
47
|
+
const beforeFixed = world.getResource(FixedTime);
|
|
48
|
+
|
|
49
|
+
const result = world.update(0.01);
|
|
50
|
+
|
|
51
|
+
expect(result.ok).toBe(false);
|
|
52
|
+
if (!result.ok) {
|
|
53
|
+
expect(result.error.code).toBe('time-config-invalid');
|
|
54
|
+
expect(result.error.expected).toContain('maxDeltaSeconds >=');
|
|
55
|
+
expect(result.error.hint).toContain('maxDeltaSeconds');
|
|
56
|
+
}
|
|
57
|
+
expect(world.getResource(Time)).toEqual(beforeTime);
|
|
58
|
+
expect(world.getResource(FixedTime)).toEqual(beforeFixed);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('uses the clamped Time delta as the fixed accumulator input', () => {
|
|
62
|
+
const world = new World();
|
|
63
|
+
let fixedRuns = 0;
|
|
64
|
+
world.addSystem(Update, { name: 'observe-time', queries: [], fn: () => {} });
|
|
65
|
+
world.addSystem(FixedUpdate, { name: 'fixed', queries: [], fn: () => fixedRuns++ });
|
|
66
|
+
|
|
67
|
+
expect(world.update(5).ok).toBe(true);
|
|
68
|
+
const time = world.getResource(Time);
|
|
69
|
+
const fixed = world.getResource(FixedTime);
|
|
70
|
+
expect(time.delta).toBe(0.1);
|
|
71
|
+
expect(fixed).toMatchObject({
|
|
72
|
+
delta: 1 / 60,
|
|
73
|
+
maxStepsPerUpdate: 4,
|
|
74
|
+
droppedUpdates: 1,
|
|
75
|
+
});
|
|
76
|
+
expect(fixedRuns).toBe(4);
|
|
77
|
+
expect(fixed.droppedSeconds).toBeCloseTo(1 / 60, 12);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
2
|
+
import { defineComponent } from '../component';
|
|
3
|
+
import type { Query, QueryDescriptor, QueryRow, QuerySpan } from '../query/query';
|
|
4
|
+
import { World } from '../world';
|
|
5
|
+
|
|
6
|
+
const Position = defineComponent('QueryTypePosition', { x: 'f32', y: 'f32' });
|
|
7
|
+
const Velocity = defineComponent('QueryTypeVelocity', { x: 'f32' });
|
|
8
|
+
|
|
9
|
+
describe('Query public type inference', () => {
|
|
10
|
+
it('infers row read, write, and optional roles', () => {
|
|
11
|
+
const world = new World();
|
|
12
|
+
const query = world.query({ read: [Velocity], write: [Position] }).unwrap();
|
|
13
|
+
expectTypeOf(query).toMatchTypeOf<
|
|
14
|
+
Query<readonly [typeof Velocity], readonly [typeof Position], readonly []>
|
|
15
|
+
>();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('projects scalar row and column shapes', () => {
|
|
19
|
+
const row = null as unknown as QueryRow<
|
|
20
|
+
readonly [typeof Velocity],
|
|
21
|
+
readonly [typeof Position],
|
|
22
|
+
readonly []
|
|
23
|
+
>;
|
|
24
|
+
const span = null as unknown as QuerySpan<
|
|
25
|
+
readonly [typeof Velocity],
|
|
26
|
+
readonly [typeof Position]
|
|
27
|
+
>;
|
|
28
|
+
expectTypeOf(row.get(Velocity).x).toBeNumber();
|
|
29
|
+
expectTypeOf(row.mut(Position).x).toBeNumber();
|
|
30
|
+
expectTypeOf(span.get(Velocity).x).toEqualTypeOf<Float32Array>();
|
|
31
|
+
expectTypeOf(span.mut(Position).x).toEqualTypeOf<Float32Array>();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('keeps descriptor access tuples literal', () => {
|
|
35
|
+
const descriptor = { read: [Position], write: [Velocity] } satisfies QueryDescriptor;
|
|
36
|
+
expectTypeOf(descriptor.read).toEqualTypeOf<(typeof Position)[]>();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// feat-20260614-ecs-managed-lifecycle-ssot M1 t-w1: store-unit tests for
|
|
2
|
+
// UniqueRefStore.release order + throw-safety + slot reuse.
|
|
3
|
+
//
|
|
4
|
+
// These tests drive UniqueRefStore directly (no World) per research
|
|
5
|
+
// Finding 1.5: the regression repro path is achievable with a spy callback,
|
|
6
|
+
// no Rapier / World fixture required. Verifies AC-01, AC-02, AC-06, AC-07
|
|
7
|
+
// (requirements §3.1, §5).
|
|
8
|
+
//
|
|
9
|
+
// TDD red -> green:
|
|
10
|
+
// - (a) order assertion + (b) throw-safety: red before w2, green after w2.
|
|
11
|
+
// - (c) slot reuse: green after w7 (M3 deletes the gen-255 retirement that
|
|
12
|
+
// would otherwise force nextSlot to grow during 1000 iters on a single
|
|
13
|
+
// slot). Documented inline so reviewer can correlate the carryover.
|
|
14
|
+
|
|
15
|
+
import { handleGeneration, handleSlot, pack, unwrapHandle } from '@forgeax/engine-types';
|
|
16
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
17
|
+
import { UniqueRefStaleError } from '../errors';
|
|
18
|
+
import { UniqueRefStore } from '../unique-ref-store';
|
|
19
|
+
|
|
20
|
+
describe('UniqueRefStore release ordering + throw-safety (feat-20260614 M1)', () => {
|
|
21
|
+
it('AC-01: releaseCallbacks entry is removed BEFORE onRelease fires', () => {
|
|
22
|
+
const store = new UniqueRefStore();
|
|
23
|
+
let observedHasCallback: boolean | null = null;
|
|
24
|
+
|
|
25
|
+
// Spy reads the private releaseCallbacks Map at the moment cb fires.
|
|
26
|
+
// After the M1 fix (w2) the entry is deleted before invocation, so the
|
|
27
|
+
// observation is `false`. Before the fix the entry is still live so
|
|
28
|
+
// observation is `true` -> assertion fails (red).
|
|
29
|
+
const onRelease = vi.fn((_payload: { id: number }) => {
|
|
30
|
+
// biome-ignore lint/suspicious/noExplicitAny: targeted private read for the order assertion
|
|
31
|
+
const internalCallbacks = (store as any).releaseCallbacks as Map<number, unknown>;
|
|
32
|
+
// biome-ignore lint/suspicious/noExplicitAny: handle raw u32 read for the lookup
|
|
33
|
+
const raw = (store as any).payloads as Map<number, unknown>;
|
|
34
|
+
// The handle is a u32 brand. Re-derive raw via the live keys snapshot
|
|
35
|
+
// since the handle isn't directly in scope inside the cb closure.
|
|
36
|
+
void raw;
|
|
37
|
+
// Use the callbacks map's keys: at the moment of cb invocation, the
|
|
38
|
+
// entry MUST already be gone -> map size 0 (only one alloc was made).
|
|
39
|
+
observedHasCallback = internalCallbacks.size > 0;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const handle = store.alloc('Test', { id: 7 }, onRelease);
|
|
43
|
+
const result = store.release(handle);
|
|
44
|
+
|
|
45
|
+
expect(result.ok).toBe(true);
|
|
46
|
+
expect(onRelease).toHaveBeenCalledTimes(1);
|
|
47
|
+
expect(observedHasCallback).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('AC-02 + AC-06: throwing onRelease re-throws once; second release returns Stale (gen incremented, M4)', () => {
|
|
51
|
+
const store = new UniqueRefStore();
|
|
52
|
+
const onRelease = vi.fn((_payload: { id: number }) => {
|
|
53
|
+
throw new Error('intentional cleanup failure');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const handle = store.alloc('Test', { id: 9 }, onRelease);
|
|
57
|
+
|
|
58
|
+
// First release must propagate the throw (no try/finally swallowing).
|
|
59
|
+
expect(() => store.release(handle)).toThrow('intentional cleanup failure');
|
|
60
|
+
expect(onRelease).toHaveBeenCalledTimes(1);
|
|
61
|
+
|
|
62
|
+
// After M4 gen increment, the store gen is now 1 while handle gen is 0.
|
|
63
|
+
// Second release returns stale (gen mismatch), not double-release.
|
|
64
|
+
const second = store.release(handle);
|
|
65
|
+
expect(second.ok).toBe(false);
|
|
66
|
+
if (!second.ok) {
|
|
67
|
+
expect(second.error).toBeInstanceOf(UniqueRefStaleError);
|
|
68
|
+
}
|
|
69
|
+
expect(onRelease).toHaveBeenCalledTimes(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('AC-07: 1000-iteration alloc/release loop reuses slots (no unbounded growth)', () => {
|
|
73
|
+
const store = new UniqueRefStore();
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < 1000; i++) {
|
|
76
|
+
const handle = store.alloc('Test', { id: i });
|
|
77
|
+
const result = store.release(handle);
|
|
78
|
+
expect(result.ok).toBe(true);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
expect(store._liveCount()).toBe(0);
|
|
82
|
+
|
|
83
|
+
// nextSlot is private. After M3 (w7) deletes the gen-255 retirement path
|
|
84
|
+
// a single slot recycles forever and nextSlot stays at 2. Before w7,
|
|
85
|
+
// gen-255 retirement forces ~4 fresh slots over 1000 iters (256 per slot
|
|
86
|
+
// before retirement). The bound below stays true in both regimes and
|
|
87
|
+
// tightens after M3; the inline comment is the bridge between this test
|
|
88
|
+
// and M3's removal of gen retirement (research Finding 1.4).
|
|
89
|
+
// biome-ignore lint/suspicious/noExplicitAny: targeted private read for slot-reuse verification
|
|
90
|
+
const nextSlot = (store as any).nextSlot as number;
|
|
91
|
+
expect(nextSlot).toBeLessThan(10);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ─── w6 M3: gen welding alloc tests ─────────────────────────────────────
|
|
96
|
+
// feat-20260623-asset-handle-generation M3 — alloc embeds generation into
|
|
97
|
+
// the returned handle via codec.pack(slot, gen). First alloc gen=0 (AC-06);
|
|
98
|
+
// slot 0 sentinel stays pack(0,0)===0 and isLive(0)===false (R6).
|
|
99
|
+
// M3 scope: gen welding only, no gen increment on release (that's M4).
|
|
100
|
+
// Reused slots always get gen=0 in M3.
|
|
101
|
+
describe('w6 M3 UniqueRefStore: gen welding on alloc', () => {
|
|
102
|
+
it('AC-06: first alloc gen=0 => handleSlot matches raw slot', () => {
|
|
103
|
+
const store = new UniqueRefStore();
|
|
104
|
+
const handle = store.alloc('TestAsset', { id: 1 });
|
|
105
|
+
|
|
106
|
+
const slot = handleSlot(handle);
|
|
107
|
+
const gen = handleGeneration(handle);
|
|
108
|
+
|
|
109
|
+
expect(gen).toBe(0);
|
|
110
|
+
// UniqueRefStore.nextSlot starts at 1, so the first alloc should be slot 1
|
|
111
|
+
expect(slot).toBe(1);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('AC-05: pack(slot,0)===slot for slot 0-5 (sentinel + typical slots)', () => {
|
|
115
|
+
for (let s = 0; s <= 5; s++) {
|
|
116
|
+
expect(pack(s, 0)).toBe(s);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('R6: slot 0 sentinel — pack(0,0)===0 and isLive(0)===false', () => {
|
|
121
|
+
expect(pack(0, 0)).toBe(0);
|
|
122
|
+
|
|
123
|
+
// isLive should return false for any handle whose raw value is 0.
|
|
124
|
+
// TypeScript disallows constructing Handle<T, 'unique'> with literal 0
|
|
125
|
+
// from outside toUnique, but isLive reads the raw value — passing
|
|
126
|
+
// a brand-cast 0 tests the sentinel path.
|
|
127
|
+
const store = new UniqueRefStore();
|
|
128
|
+
// biome-ignore lint/suspicious/noExplicitAny: sentinel test pattern
|
|
129
|
+
expect(store.isLive(0 as any)).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('alloc welds gen into handle — gen extractable via handleGeneration', () => {
|
|
133
|
+
// After alloc gen welding, handleGeneration returns the gen embedded
|
|
134
|
+
// during alloc. In M3, gen is always 0 because release does not yet
|
|
135
|
+
// increment _generations (M4). But the pack(slot,gen) -> handleGeneration
|
|
136
|
+
// round-trip must work.
|
|
137
|
+
const store = new UniqueRefStore();
|
|
138
|
+
const h = store.alloc('TestAsset', { id: 1 });
|
|
139
|
+
expect(handleGeneration(h)).toBe(0);
|
|
140
|
+
expect(handleSlot(h)).toBe(1);
|
|
141
|
+
expect(pack(handleSlot(h), handleGeneration(h))).toBe(unwrapHandle(h));
|
|
142
|
+
// Not using unwrapHandle directly — using pack+handleSlot+handleGeneration
|
|
143
|
+
// as indirect verification.
|
|
144
|
+
// biome-ignore lint/suspicious/noExplicitAny: access the raw brand value
|
|
145
|
+
expect(h as any as number).toBe(pack(1, 0));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('resolve works correctly after gen-welded alloc (gen increments on release, M4)', () => {
|
|
149
|
+
// Alloc + release + re-alloc: the second handle resolves to the
|
|
150
|
+
// second payload. After M4, release increments gen, re-alloc gets gen=1.
|
|
151
|
+
const store = new UniqueRefStore();
|
|
152
|
+
const h1 = store.alloc('MeshAsset', { mesh: 'cube' });
|
|
153
|
+
expect(handleGeneration(h1)).toBe(0);
|
|
154
|
+
|
|
155
|
+
expect(store.release(h1).ok).toBe(true);
|
|
156
|
+
|
|
157
|
+
const h2 = store.alloc('MeshAsset', { mesh: 'sphere' });
|
|
158
|
+
// M4: release incremented gen to 1, so re-alloc gets gen=1
|
|
159
|
+
expect(handleGeneration(h2)).toBe(1);
|
|
160
|
+
|
|
161
|
+
const r = store.resolve(h2);
|
|
162
|
+
expect(r.ok).toBe(true);
|
|
163
|
+
if (r.ok) {
|
|
164
|
+
expect(r.value).toEqual({ mesh: 'sphere' });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('alloc with onRelease fires after gen-welded release', () => {
|
|
169
|
+
const cb = vi.fn();
|
|
170
|
+
const store = new UniqueRefStore();
|
|
171
|
+
const h = store.alloc('Asset', { key: 1 }, cb);
|
|
172
|
+
expect(store.release(h).ok).toBe(true);
|
|
173
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
174
|
+
expect(cb).toHaveBeenCalledWith({ key: 1 });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('R6: slot 0 sentinel isLive false — alloc never produces slot 0', () => {
|
|
178
|
+
const store = new UniqueRefStore();
|
|
179
|
+
// Alloc 100 handles and release them — slot 0 should never appear
|
|
180
|
+
// because nextSlot starts at 1 and freeSlots only contain released
|
|
181
|
+
// slots >= 1.
|
|
182
|
+
const handles: ReturnType<typeof store.alloc>[] = [];
|
|
183
|
+
for (let i = 0; i < 100; i++) {
|
|
184
|
+
handles.push(store.alloc('Test', { id: i }));
|
|
185
|
+
}
|
|
186
|
+
for (const h of handles) {
|
|
187
|
+
expect(handleSlot(h)).toBeGreaterThanOrEqual(1);
|
|
188
|
+
expect(store.release(h).ok).toBe(true);
|
|
189
|
+
}
|
|
190
|
+
// All slots recycled — still no slot 0
|
|
191
|
+
const h = store.alloc('Test', { id: 999 });
|
|
192
|
+
expect(handleSlot(h)).toBeGreaterThanOrEqual(1);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('M4: alloc after release reuses slot with gen 1 (gen increments on release)', () => {
|
|
196
|
+
const store = new UniqueRefStore();
|
|
197
|
+
const h1 = store.alloc('MeshAsset', { mesh: 'cube' });
|
|
198
|
+
void handleSlot(h1); // probe slot
|
|
199
|
+
expect(store.release(h1).ok).toBe(true);
|
|
200
|
+
|
|
201
|
+
const h2 = store.alloc('MeshAsset', { mesh: 'sphere' });
|
|
202
|
+
expect(handleGeneration(h2)).toBe(1);
|
|
203
|
+
|
|
204
|
+
const r2 = store.resolve(h2);
|
|
205
|
+
expect(r2.ok).toBe(true);
|
|
206
|
+
if (r2.ok) expect(r2.value).toEqual({ mesh: 'sphere' });
|
|
207
|
+
|
|
208
|
+
// h1 (gen=0) is stale now
|
|
209
|
+
const r1 = store.resolve(h1);
|
|
210
|
+
expect(r1.ok).toBe(false);
|
|
211
|
+
if (!r1.ok) expect(r1.error.code).toBe('unique-ref-stale');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ─── w9 M4: stale detection unit tests (red phase) ─────────────────────
|
|
216
|
+
// feat-20260623-asset-handle-generation M4 — gen comparison on
|
|
217
|
+
// resolve/release (UniqueRefStore has no retain). RED phase: gen increment
|
|
218
|
+
// on release is NOT implemented yet, so stale detections return ok.
|
|
219
|
+
describe('w9 M4 UniqueRefStore: stale detection (resolve/release + retire)', () => {
|
|
220
|
+
it('AC-01: stale resolve returns error with code unique-ref-stale', () => {
|
|
221
|
+
const store = new UniqueRefStore();
|
|
222
|
+
const h1 = store.alloc('MeshAsset', { mesh: 'cube' });
|
|
223
|
+
// Release bumps gen 0->1, then re-alloc gets gen=1.
|
|
224
|
+
expect(store.release(h1).ok).toBe(true);
|
|
225
|
+
|
|
226
|
+
const h2 = store.alloc('MeshAsset', { mesh: 'sphere' });
|
|
227
|
+
expect(handleGeneration(h2)).toBe(1);
|
|
228
|
+
|
|
229
|
+
// h1 (gen=0) is stale
|
|
230
|
+
const r = store.resolve(h1);
|
|
231
|
+
expect(r.ok).toBe(false);
|
|
232
|
+
if (!r.ok) {
|
|
233
|
+
expect(r.error.code).toBe('unique-ref-stale');
|
|
234
|
+
}
|
|
235
|
+
// h2 resolves normally
|
|
236
|
+
const r2 = store.resolve(h2);
|
|
237
|
+
expect(r2.ok).toBe(true);
|
|
238
|
+
if (r2.ok) {
|
|
239
|
+
expect(r2.value).toEqual({ mesh: 'sphere' });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('AC-01: stale resolve never returns a payload', () => {
|
|
244
|
+
const store = new UniqueRefStore();
|
|
245
|
+
const h1 = store.alloc('MeshAsset', { mesh: 'cube' });
|
|
246
|
+
expect(store.release(h1).ok).toBe(true);
|
|
247
|
+
store.alloc('MeshAsset', { mesh: 'sphere' });
|
|
248
|
+
|
|
249
|
+
const r = store.resolve(h1);
|
|
250
|
+
expect(r.ok).toBe(false);
|
|
251
|
+
if (!r.ok) {
|
|
252
|
+
// biome-ignore lint/suspicious/noExplicitAny: accessing value on Result union error branch to verify no payload leak
|
|
253
|
+
expect((r as any).value).toBeUndefined();
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('AC-03: stale release returns error, does NOT drop payload of new holder', () => {
|
|
258
|
+
const store = new UniqueRefStore();
|
|
259
|
+
const h1 = store.alloc('MeshAsset', { mesh: 'cube' });
|
|
260
|
+
expect(store.release(h1).ok).toBe(true);
|
|
261
|
+
|
|
262
|
+
const h2 = store.alloc('MeshAsset', { mesh: 'sphere' });
|
|
263
|
+
// h2 is live. Stale release of h1 must NOT drop h2's payload.
|
|
264
|
+
const r = store.release(h1);
|
|
265
|
+
expect(r.ok).toBe(false);
|
|
266
|
+
if (!r.ok) {
|
|
267
|
+
expect(r.error.code).toBe('unique-ref-stale');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// h2 is still live — resolve should succeed
|
|
271
|
+
const r2 = store.resolve(h2);
|
|
272
|
+
expect(r2.ok).toBe(true);
|
|
273
|
+
if (r2.ok) {
|
|
274
|
+
expect(r2.value).toEqual({ mesh: 'sphere' });
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('AC-03: stale release does not trigger onRelease callback', () => {
|
|
279
|
+
const cb = vi.fn();
|
|
280
|
+
const store = new UniqueRefStore();
|
|
281
|
+
const h1 = store.alloc('MeshAsset', { mesh: 'cube' }, cb);
|
|
282
|
+
expect(store.release(h1).ok).toBe(true);
|
|
283
|
+
|
|
284
|
+
// Re-alloc with new gen
|
|
285
|
+
store.alloc('MeshAsset', { mesh: 'sphere' });
|
|
286
|
+
|
|
287
|
+
// Stale release of h1 should NOT fire h2's callback
|
|
288
|
+
const r = store.release(h1);
|
|
289
|
+
expect(r.ok).toBe(false);
|
|
290
|
+
if (!r.ok) {
|
|
291
|
+
expect(r.error.code).toBe('unique-ref-stale');
|
|
292
|
+
}
|
|
293
|
+
expect(cb).toHaveBeenCalledTimes(1); // only from the first release (gen match)
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('AC-04: new handle after reuse resolves and releases normally', () => {
|
|
297
|
+
const store = new UniqueRefStore();
|
|
298
|
+
const h1 = store.alloc('MeshAsset', { mesh: 'cube' });
|
|
299
|
+
expect(store.release(h1).ok).toBe(true);
|
|
300
|
+
|
|
301
|
+
const h2 = store.alloc('MeshAsset', { mesh: 'sphere' });
|
|
302
|
+
expect(handleGeneration(h2)).toBe(1);
|
|
303
|
+
|
|
304
|
+
// resolve
|
|
305
|
+
const r = store.resolve(h2);
|
|
306
|
+
expect(r.ok).toBe(true);
|
|
307
|
+
if (r.ok) expect(r.value).toEqual({ mesh: 'sphere' });
|
|
308
|
+
|
|
309
|
+
// release
|
|
310
|
+
expect(store.release(h2).ok).toBe(true);
|
|
311
|
+
|
|
312
|
+
// h1 still stale
|
|
313
|
+
const rStale = store.resolve(h1);
|
|
314
|
+
expect(rStale.ok).toBe(false);
|
|
315
|
+
if (!rStale.ok) {
|
|
316
|
+
expect(rStale.error.code).toBe('unique-ref-stale');
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('AC-07: retire-on-255 — gen pushed past MAX_GEN then slot not recycled', () => {
|
|
321
|
+
const store = new UniqueRefStore();
|
|
322
|
+
const h = store.alloc('MeshAsset', { mesh: 'cube' });
|
|
323
|
+
const slot = handleSlot(h);
|
|
324
|
+
|
|
325
|
+
// gen=255 is still a usable handle under the new gen > MAX_GEN predicate.
|
|
326
|
+
// Bump _generations[slot] to 255 and push to freeSlots so the next alloc
|
|
327
|
+
// reuses it with gen=255.
|
|
328
|
+
// biome-ignore lint/suspicious/noExplicitAny: private mutation for retire edge boundary
|
|
329
|
+
(store as any)._generations[slot] = 255;
|
|
330
|
+
// biome-ignore lint/suspicious/noExplicitAny: push slot to free list so alloc reuses
|
|
331
|
+
(store as any).freeSlots.push(slot);
|
|
332
|
+
|
|
333
|
+
// Alloc reuses slot with gen=255
|
|
334
|
+
const h2 = store.alloc('MeshAsset', { mesh: 'sphere' });
|
|
335
|
+
expect(handleGeneration(h2)).toBe(255);
|
|
336
|
+
// Verify gen=255 is a usable handle — resolve succeeds.
|
|
337
|
+
const rResolve = store.resolve(h2);
|
|
338
|
+
expect(rResolve.ok).toBe(true);
|
|
339
|
+
|
|
340
|
+
// Release h2: gen matches, gen++ to 256 (>MAX_GEN), slot retired.
|
|
341
|
+
expect(store.release(h2).ok).toBe(true);
|
|
342
|
+
|
|
343
|
+
// Slot must not be recycled (retired)
|
|
344
|
+
// biome-ignore lint/suspicious/noExplicitAny: private read
|
|
345
|
+
const freeSlots: number[] = (store as any).freeSlots;
|
|
346
|
+
expect(freeSlots.indexOf(slot)).toBe(-1);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// @forgeax/engine-ecs — test fixture: brand-unwrap helper.
|
|
2
|
+
//
|
|
3
|
+
// @internal Test-only brand unwrapping for Object.is identity assertions
|
|
4
|
+
// across the family of phantom-branded numeric ids (Handle<T, M>, Entity,
|
|
5
|
+
// SceneInstanceId — all defined as `number & {brand}`). Charter P4
|
|
6
|
+
// (consistent abstraction) — collapses 42 historical brand-unwrap cast
|
|
7
|
+
// sites into one helper so AI users reading the test fixtures see a
|
|
8
|
+
// single intent marker rather than scattered casts. The helper is
|
|
9
|
+
// fixture-scope (lives under __tests__/utils, never re-exported from
|
|
10
|
+
// packages/ecs/src/index.ts) and must not appear in any production code
|
|
11
|
+
// path (AC-08 grep gate).
|
|
12
|
+
//
|
|
13
|
+
// Implementation uses `Number()` to preserve numeric identity while
|
|
14
|
+
// shedding the phantom brand; this avoids the legacy double-cast idiom
|
|
15
|
+
// and keeps the AC-07 grep gate clean even though this file lives
|
|
16
|
+
// inside packages/ecs/src/__tests__/.
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Unwrap a phantom-branded numeric id (`Handle<T, M>`, `Entity`,
|
|
20
|
+
* `SceneInstanceId`, ...) to its underlying `number` representation. Used
|
|
21
|
+
* by test fixtures that need to compare brand-erased numeric values via
|
|
22
|
+
* `Object.is` / `===`.
|
|
23
|
+
*
|
|
24
|
+
* The constraint `H extends number` keeps the helper aligned with the
|
|
25
|
+
* brand idiom (`number & {brand}`) used throughout the codebase without
|
|
26
|
+
* coupling to any specific brand class.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export const handleNumeric = <H extends number>(h: H): number => Number(h);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const rootSource = readFileSync(new URL('../index.ts', import.meta.url), 'utf8');
|
|
5
|
+
|
|
6
|
+
describe('ECS validation helpers stay owner-local', () => {
|
|
7
|
+
it('does not project write-path validators from the public root', async () => {
|
|
8
|
+
expect(rootSource).not.toContain('validateSharedFieldValues');
|
|
9
|
+
expect(rootSource).not.toContain('validateEnumFieldValues');
|
|
10
|
+
|
|
11
|
+
const root = (await import('../index')) as Record<string, unknown>;
|
|
12
|
+
expect(root).not.toHaveProperty('validateSharedFieldValues');
|
|
13
|
+
expect(root).not.toHaveProperty('validateEnumFieldValues');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { defineComponent } from '../component';
|
|
3
|
+
import { World } from '../world';
|
|
4
|
+
|
|
5
|
+
const VisibilityLike = defineComponent('M1VisibilityLikeAtomic', {
|
|
6
|
+
state: { type: 'enum', default: 0, labels: { inherited: 0, hidden: 1, visible: 2 } },
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const Position = defineComponent('M1VisibilityAtomicPosition', {
|
|
10
|
+
value: 'f32',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function expectInvalidValue(result: { ok: boolean; error?: unknown }, entity?: number): void {
|
|
14
|
+
expect(result.ok).toBe(false);
|
|
15
|
+
if (result.ok) return;
|
|
16
|
+
const error = result.error as {
|
|
17
|
+
code: string;
|
|
18
|
+
expected: string;
|
|
19
|
+
hint: string;
|
|
20
|
+
detail: {
|
|
21
|
+
entity: number | undefined;
|
|
22
|
+
component: string;
|
|
23
|
+
field: string;
|
|
24
|
+
received: unknown;
|
|
25
|
+
allowedValues: Readonly<Record<string, number>>;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
expect(error.code).toBe('component-field-invalid-value');
|
|
29
|
+
expect(error.expected).toContain('inherited');
|
|
30
|
+
expect(error.hint).toContain('VisibilityLike');
|
|
31
|
+
expect(error.detail).toEqual({
|
|
32
|
+
entity,
|
|
33
|
+
component: 'M1VisibilityLikeAtomic',
|
|
34
|
+
field: 'state',
|
|
35
|
+
received: 9,
|
|
36
|
+
allowedValues: { inherited: 0, hidden: 1, visible: 2 },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('M1 Visibility enum writes are atomic', () => {
|
|
41
|
+
it('rejects an invalid enum during spawn before creating the row', () => {
|
|
42
|
+
const world = new World();
|
|
43
|
+
|
|
44
|
+
const result = world.spawn({ component: VisibilityLike, data: { state: 9 } });
|
|
45
|
+
|
|
46
|
+
expectInvalidValue(result);
|
|
47
|
+
expect(world.spawn({ component: Position, data: { value: 1 } }).unwrap()).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('rejects an invalid enum during addComponent before archetype migration', () => {
|
|
51
|
+
const world = new World();
|
|
52
|
+
const entity = world.spawn({ component: Position, data: { value: 1 } }).unwrap();
|
|
53
|
+
|
|
54
|
+
const result = world.addComponent(entity, {
|
|
55
|
+
component: VisibilityLike,
|
|
56
|
+
data: { state: 9 },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expectInvalidValue(result, entity);
|
|
60
|
+
expect(world.get(entity, VisibilityLike).ok).toBe(false);
|
|
61
|
+
expect(world.get(entity, Position).unwrap().value).toBe(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('rejects an invalid enum during set and preserves the previous value', () => {
|
|
65
|
+
const world = new World();
|
|
66
|
+
const entity = world.spawn({ component: VisibilityLike, data: { state: 1 } }).unwrap();
|
|
67
|
+
|
|
68
|
+
const result = world.set(entity, VisibilityLike, { state: 9 });
|
|
69
|
+
|
|
70
|
+
expectInvalidValue(result, entity);
|
|
71
|
+
expect(world.get(entity, VisibilityLike).unwrap().state).toBe(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('accepts every declared enum value through all three write entries', () => {
|
|
75
|
+
const world = new World();
|
|
76
|
+
const entity = world.spawn({ component: VisibilityLike, data: { state: 0 } }).unwrap();
|
|
77
|
+
|
|
78
|
+
expect(world.set(entity, VisibilityLike, { state: 1 }).ok).toBe(true);
|
|
79
|
+
expect(world.addComponent(entity, { component: Position, data: { value: 2 } }).ok).toBe(true);
|
|
80
|
+
expect(world.get(entity, VisibilityLike).unwrap().state).toBe(1);
|
|
81
|
+
});
|
|
82
|
+
});
|