@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,1837 @@
|
|
|
1
|
+
// Consolidated by feat-20260609-test-pool-startup-reduction-merge-tiny-test-files
|
|
2
|
+
// biome-ignore-all lint/complexity/noUselessLoneBlockStatements: scope isolation between merged source files
|
|
3
|
+
//
|
|
4
|
+
// Source files (N=10):
|
|
5
|
+
// - packages/ecs/src/__tests__/archetype-storage.test.ts
|
|
6
|
+
// - packages/ecs/src/__tests__/buffer-pool.test.ts
|
|
7
|
+
// - packages/ecs/src/__tests__/buffer-vocab-fixup.test.ts
|
|
8
|
+
// - packages/ecs/src/__tests__/chained-add-component-column-loss.test.ts
|
|
9
|
+
// - packages/ecs/src/__tests__/column.test.ts
|
|
10
|
+
// - packages/ecs/src/__tests__/entity-column.test.ts
|
|
11
|
+
// - packages/ecs/src/__tests__/fixed-array-inline.test.ts
|
|
12
|
+
// - packages/ecs/src/__tests__/query-preregister.test.ts
|
|
13
|
+
// - packages/ecs/src/__tests__/query.test.ts
|
|
14
|
+
//
|
|
15
|
+
// Paradigm: each block-scoped describe('<source-filename>.test.ts', ...) preserves
|
|
16
|
+
// source as ancestorTitles[0]. Top-level imports merged + deduped.
|
|
17
|
+
|
|
18
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
19
|
+
import { BufferPool, SIZE_CLASSES } from '../buffer-pool';
|
|
20
|
+
import type { ComponentSchema, ScalarFieldType } from '../component';
|
|
21
|
+
import {
|
|
22
|
+
componentId,
|
|
23
|
+
componentSchema,
|
|
24
|
+
defineComponent,
|
|
25
|
+
fieldTypeToMetaKey,
|
|
26
|
+
isEntityField,
|
|
27
|
+
isManagedArrayField,
|
|
28
|
+
isManagedBufferField,
|
|
29
|
+
isManagedField,
|
|
30
|
+
TYPE_METADATA,
|
|
31
|
+
} from '../component';
|
|
32
|
+
import { Entity } from '../entity';
|
|
33
|
+
import type { EntityHandle } from '../entity-handle';
|
|
34
|
+
import type { Archetype } from '../storage/archetype';
|
|
35
|
+
import {
|
|
36
|
+
createArchetypeGraph,
|
|
37
|
+
getAddEdge,
|
|
38
|
+
getOrCreateArchetype,
|
|
39
|
+
getRemoveEdge,
|
|
40
|
+
} from '../storage/archetype-graph';
|
|
41
|
+
import { createColumn, growColumn, HAS_TRANSFER, isHotSchema } from '../storage/column';
|
|
42
|
+
import {
|
|
43
|
+
appendTableRow,
|
|
44
|
+
createTable as createArchetype,
|
|
45
|
+
growTable as growArchetype,
|
|
46
|
+
removeTableRow as removeEntity,
|
|
47
|
+
type Table,
|
|
48
|
+
} from '../storage/table';
|
|
49
|
+
import { World } from '../world';
|
|
50
|
+
|
|
51
|
+
import { worldInternal } from '../world-internal';
|
|
52
|
+
|
|
53
|
+
function tableColumns(table: Table) {
|
|
54
|
+
return new Map(
|
|
55
|
+
[...table.storage].map(([componentId, componentStorage]) => [
|
|
56
|
+
componentId,
|
|
57
|
+
componentStorage.fields,
|
|
58
|
+
]),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function appendEntity(table: Table, entity: number): number {
|
|
63
|
+
return appendTableRow(table, entity as EntityHandle);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
// ─── from archetype-storage.test.ts ───
|
|
68
|
+
/** Auto-incrementing archetype ID for test isolation. */
|
|
69
|
+
let testArchId = 0;
|
|
70
|
+
|
|
71
|
+
describe('archetype-storage.test.ts', () => {
|
|
72
|
+
describe('Archetype creation', () => {
|
|
73
|
+
it('creates archetype with sorted component-set key', () => {
|
|
74
|
+
const B = defineComponent('B', { b: { type: 'f32' } });
|
|
75
|
+
const A = defineComponent('A', { a: { type: 'f32' } });
|
|
76
|
+
const arch = createArchetype([B, A], testArchId++);
|
|
77
|
+
const ids = [componentId(Entity), componentId(A), componentId(B)].sort((a, b) => a - b);
|
|
78
|
+
expect(arch.key).toBe(ids.join('+'));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('creates archetype with correct initial capacity', () => {
|
|
82
|
+
const C = defineComponent('Cap', { x: { type: 'f32' } });
|
|
83
|
+
const arch = createArchetype([C], testArchId++);
|
|
84
|
+
expect(arch.capacity).toBeGreaterThanOrEqual(1);
|
|
85
|
+
expect(arch.size).toBe(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('creates columns for each component field', () => {
|
|
89
|
+
const Pos = defineComponent('Pos', { x: { type: 'f32' }, y: { type: 'f32' } });
|
|
90
|
+
const arch = createArchetype([Pos], testArchId++);
|
|
91
|
+
expect(tableColumns(arch).has(componentId(Pos))).toBe(true);
|
|
92
|
+
// biome-ignore lint/style/noNonNullAssertion: test setup guarantees componentId(Pos) exists in columns
|
|
93
|
+
const compCols = tableColumns(arch).get(componentId(Pos))!;
|
|
94
|
+
expect(compCols.has('x')).toBe(true);
|
|
95
|
+
expect(compCols.has('y')).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('tag component creates archetype with no data columns', () => {
|
|
99
|
+
const Tag = defineComponent('ArchetypeStorageTag', {});
|
|
100
|
+
const arch = createArchetype([Tag], testArchId++);
|
|
101
|
+
const compCols = tableColumns(arch).get(componentId(Tag));
|
|
102
|
+
expect(compCols === undefined || compCols.size === 0).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('appendEntity / removeEntity (swap-pop)', () => {
|
|
107
|
+
it('appends entity and increments size', () => {
|
|
108
|
+
const C = defineComponent('SwapTest', { v: { type: 'i32' } });
|
|
109
|
+
const arch = createArchetype([C], testArchId++);
|
|
110
|
+
const row = appendEntity(arch, 42);
|
|
111
|
+
expect(row).toBe(0);
|
|
112
|
+
expect(arch.size).toBe(1);
|
|
113
|
+
// Entity index stored in id=0 self column (not a separate entities array)
|
|
114
|
+
const selfVal = tableColumns(arch).get(componentId(Entity))?.get('self')?.view[0];
|
|
115
|
+
expect(selfVal).toBe(42);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('swap-pop removes entity correctly', () => {
|
|
119
|
+
const C = defineComponent('SwapPop', { v: { type: 'i32' } });
|
|
120
|
+
const arch = createArchetype([C], testArchId++);
|
|
121
|
+
|
|
122
|
+
appendEntity(arch, 10);
|
|
123
|
+
appendEntity(arch, 20);
|
|
124
|
+
appendEntity(arch, 30);
|
|
125
|
+
|
|
126
|
+
// biome-ignore lint/style/noNonNullAssertion: test setup guarantees componentId(C) exists in columns
|
|
127
|
+
const cols = tableColumns(arch).get(componentId(C))!;
|
|
128
|
+
// biome-ignore lint/style/noNonNullAssertion: test setup guarantees 'v' field exists
|
|
129
|
+
const vCol = cols.get('v')!;
|
|
130
|
+
vCol.view[0] = 100;
|
|
131
|
+
vCol.view[1] = 200;
|
|
132
|
+
vCol.view[2] = 300;
|
|
133
|
+
|
|
134
|
+
const swapped = removeEntity(arch, 0);
|
|
135
|
+
expect(arch.size).toBe(2);
|
|
136
|
+
|
|
137
|
+
// Entity identity now read from id=0 self column
|
|
138
|
+
const selfCol = tableColumns(arch).get(componentId(Entity))?.get('self');
|
|
139
|
+
expect(selfCol).toBeDefined();
|
|
140
|
+
// biome-ignore lint/style/noNonNullAssertion: guarded by expect
|
|
141
|
+
expect(selfCol!.view[0]! & 0xffffff).toBe(30);
|
|
142
|
+
expect(vCol.view[0]).toBe(300);
|
|
143
|
+
|
|
144
|
+
// biome-ignore lint/style/noNonNullAssertion: guarded by expect
|
|
145
|
+
expect(selfCol!.view[1]! & 0xffffff).toBe(20);
|
|
146
|
+
expect(vCol.view[1]).toBe(200);
|
|
147
|
+
|
|
148
|
+
expect(swapped).toEqual({ movedEntity: 30, newRow: 0 });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('swap-pop on last row does not swap', () => {
|
|
152
|
+
const C = defineComponent('LastRow', { v: { type: 'f32' } });
|
|
153
|
+
const arch = createArchetype([C], testArchId++);
|
|
154
|
+
appendEntity(arch, 10);
|
|
155
|
+
appendEntity(arch, 20);
|
|
156
|
+
|
|
157
|
+
const swapped = removeEntity(arch, 1);
|
|
158
|
+
expect(arch.size).toBe(1);
|
|
159
|
+
// Entity identity from self column
|
|
160
|
+
const selfVal = tableColumns(arch).get(componentId(Entity))?.get('self')?.view[0];
|
|
161
|
+
expect(selfVal).toBe(10);
|
|
162
|
+
expect(swapped).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('growArchetype', () => {
|
|
167
|
+
it('doubles capacity and preserves data', () => {
|
|
168
|
+
const C = defineComponent('Grow', { x: { type: 'f32' } });
|
|
169
|
+
const arch = createArchetype([C], testArchId++);
|
|
170
|
+
const initialCap = arch.capacity;
|
|
171
|
+
|
|
172
|
+
for (let i = 0; i < initialCap; i++) {
|
|
173
|
+
appendEntity(arch, i);
|
|
174
|
+
// biome-ignore lint/style/noNonNullAssertion: test setup guarantees componentId(C) exists in columns
|
|
175
|
+
const cols = tableColumns(arch).get(componentId(C))!;
|
|
176
|
+
// biome-ignore lint/style/noNonNullAssertion: field 'x' guaranteed to exist
|
|
177
|
+
cols.get('x')!.view[i] = i * 1.5;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
growArchetype(arch, initialCap * 2);
|
|
181
|
+
expect(arch.capacity).toBe(initialCap * 2);
|
|
182
|
+
|
|
183
|
+
// biome-ignore lint/style/noNonNullAssertion: test setup guarantees componentId(C) exists in columns
|
|
184
|
+
const cols = tableColumns(arch).get(componentId(C))!;
|
|
185
|
+
for (let i = 0; i < initialCap; i++) {
|
|
186
|
+
expect(cols.get('x')?.view[i]).toBeCloseTo(i * 1.5);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('increments version on grow', () => {
|
|
191
|
+
const C = defineComponent('GrowVer', { x: { type: 'f32' } });
|
|
192
|
+
const arch = createArchetype([C], testArchId++);
|
|
193
|
+
const v0 = arch.version;
|
|
194
|
+
growArchetype(arch, arch.capacity * 2);
|
|
195
|
+
expect(arch.version).toBe(v0 + 1);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('growArchetype is a no-op when targetCapacity <= current capacity', () => {
|
|
199
|
+
const C = defineComponent('GrowNoop', { x: { type: 'f32' } });
|
|
200
|
+
const arch = createArchetype([C], testArchId++);
|
|
201
|
+
const capBefore = arch.capacity;
|
|
202
|
+
const versionBefore = arch.version;
|
|
203
|
+
growArchetype(arch, capBefore);
|
|
204
|
+
expect(arch.capacity).toBe(capBefore);
|
|
205
|
+
expect(arch.version).toBe(versionBefore);
|
|
206
|
+
growArchetype(arch, capBefore - 1);
|
|
207
|
+
expect(arch.capacity).toBe(capBefore);
|
|
208
|
+
expect(arch.version).toBe(versionBefore);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('ArchetypeGraph', () => {
|
|
213
|
+
it('getOrCreateArchetype returns same archetype for same component set', () => {
|
|
214
|
+
const graph = createArchetypeGraph();
|
|
215
|
+
const A = defineComponent('GA', { a: { type: 'f32' } });
|
|
216
|
+
const B = defineComponent('GB', { b: { type: 'f32' } });
|
|
217
|
+
|
|
218
|
+
const arch1 = getOrCreateArchetype(graph, [componentId(A), componentId(B)], [A, B]);
|
|
219
|
+
const arch2 = getOrCreateArchetype(graph, [componentId(B), componentId(A)], [B, A]);
|
|
220
|
+
expect(arch1).toBe(arch2);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('getAddEdge caches edge (O(1) on second call)', () => {
|
|
224
|
+
const graph = createArchetypeGraph();
|
|
225
|
+
const A = defineComponent('EA', { a: { type: 'f32' } });
|
|
226
|
+
const B = defineComponent('EB', { b: { type: 'f32' } });
|
|
227
|
+
|
|
228
|
+
const archA = getOrCreateArchetype(graph, [componentId(A)], [A]);
|
|
229
|
+
const archAB1 = getAddEdge(graph, archA, componentId(B), B);
|
|
230
|
+
const archAB2 = getAddEdge(graph, archA, componentId(B), B);
|
|
231
|
+
expect(archAB1).toBe(archAB2);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('getRemoveEdge caches edge (O(1) on second call)', () => {
|
|
235
|
+
const graph = createArchetypeGraph();
|
|
236
|
+
const A = defineComponent('RA', { a: { type: 'f32' } });
|
|
237
|
+
const B = defineComponent('RB', { b: { type: 'f32' } });
|
|
238
|
+
|
|
239
|
+
const archAB = getOrCreateArchetype(graph, [componentId(A), componentId(B)], [A, B]);
|
|
240
|
+
const archA1 = getRemoveEdge(graph, archAB, componentId(B));
|
|
241
|
+
const archA2 = getRemoveEdge(graph, archAB, componentId(B));
|
|
242
|
+
expect(archA1).toBe(archA2);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('multi-component spawn targets correct archetype directly', () => {
|
|
246
|
+
const graph = createArchetypeGraph();
|
|
247
|
+
const A = defineComponent('MA', { a: { type: 'f32' } });
|
|
248
|
+
const B = defineComponent('MB', { b: { type: 'f32' } });
|
|
249
|
+
const C = defineComponent('MC', { c: { type: 'f32' } });
|
|
250
|
+
|
|
251
|
+
const arch = getOrCreateArchetype(
|
|
252
|
+
graph,
|
|
253
|
+
[componentId(A), componentId(B), componentId(C)],
|
|
254
|
+
[A, B, C],
|
|
255
|
+
);
|
|
256
|
+
const ids = [componentId(Entity), componentId(A), componentId(B), componentId(C)].sort(
|
|
257
|
+
(a, b) => a - b,
|
|
258
|
+
);
|
|
259
|
+
expect(arch.key).toBe(ids.join('+'));
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('increments global archetype generation on new archetype creation', () => {
|
|
263
|
+
const graph = createArchetypeGraph();
|
|
264
|
+
const gen0 = graph.generation;
|
|
265
|
+
const X = defineComponent('GenX', { x: { type: 'f32' } });
|
|
266
|
+
getOrCreateArchetype(graph, [componentId(X)], [X]);
|
|
267
|
+
expect(graph.generation).toBe(gen0 + 1);
|
|
268
|
+
|
|
269
|
+
getOrCreateArchetype(graph, [componentId(X)], [X]);
|
|
270
|
+
expect(graph.generation).toBe(gen0 + 1);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('M1 storage characterization', () => {
|
|
275
|
+
it('keeps row, capacity, version, and migration owners explicit', () => {
|
|
276
|
+
const graph = createArchetypeGraph();
|
|
277
|
+
const Position = defineComponent('M1StoragePosition', { x: 'f32', y: 'f32' });
|
|
278
|
+
const Tag = defineComponent('M1StorageTag', {});
|
|
279
|
+
const source = getOrCreateArchetype(graph, [componentId(Position)], [Position]);
|
|
280
|
+
const target = getAddEdge(graph, source, componentId(Tag), Tag);
|
|
281
|
+
const back = getRemoveEdge(graph, target, componentId(Tag));
|
|
282
|
+
const sourceTable = graph.tables[source.tableId];
|
|
283
|
+
if (sourceTable === undefined) throw new Error('source table missing');
|
|
284
|
+
|
|
285
|
+
expect(back).toBe(source);
|
|
286
|
+
expect(sourceTable.storage.has(componentId(Entity))).toBe(true);
|
|
287
|
+
expect(sourceTable.storage.has(componentId(Position))).toBe(true);
|
|
288
|
+
for (const { fields } of sourceTable.storage.values()) {
|
|
289
|
+
for (const column of fields.values()) {
|
|
290
|
+
expect(column.capacity).toBe(sourceTable.capacity);
|
|
291
|
+
expect(column.arity).toBe(1);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const initialVersion = sourceTable.version;
|
|
296
|
+
appendEntity(sourceTable, 7 as EntityHandle);
|
|
297
|
+
expect(sourceTable.size).toBe(1);
|
|
298
|
+
growArchetype(sourceTable, sourceTable.capacity * 2);
|
|
299
|
+
expect(sourceTable.version).toBe(initialVersion + 1);
|
|
300
|
+
expect(sourceTable.size).toBe(1);
|
|
301
|
+
for (const { fields } of sourceTable.storage.values()) {
|
|
302
|
+
for (const column of fields.values()) expect(column.capacity).toBe(sourceTable.capacity);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
{
|
|
310
|
+
// ─── from buffer-pool.test.ts ───
|
|
311
|
+
describe('buffer-pool.test.ts', () => {
|
|
312
|
+
describe('w10 - BufferPool size-class allocation', () => {
|
|
313
|
+
it('alloc(128) returns a view of byteLength === 128 (bucket 256B)', () => {
|
|
314
|
+
const pool = new BufferPool();
|
|
315
|
+
const r = pool.alloc(128);
|
|
316
|
+
expect(r.ok).toBe(true);
|
|
317
|
+
if (!r.ok) throw new Error('expected ok');
|
|
318
|
+
expect(r.value.view.byteLength).toBe(128);
|
|
319
|
+
expect(r.value.id).toBeGreaterThan(0);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('alloc(0) is legal and returns a zero-length view (no bucket)', () => {
|
|
323
|
+
const pool = new BufferPool();
|
|
324
|
+
const r = pool.alloc(0);
|
|
325
|
+
expect(r.ok).toBe(true);
|
|
326
|
+
if (!r.ok) throw new Error('expected ok');
|
|
327
|
+
expect(r.value.view.byteLength).toBe(0);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('release then alloc(128) reuses a slot from the free-list', () => {
|
|
331
|
+
const pool = new BufferPool();
|
|
332
|
+
const a = pool.alloc(128);
|
|
333
|
+
if (!a.ok) throw new Error('expected ok a');
|
|
334
|
+
const idA = a.value.id;
|
|
335
|
+
const rel = pool.release(idA);
|
|
336
|
+
expect(rel.ok).toBe(true);
|
|
337
|
+
const b = pool.alloc(128);
|
|
338
|
+
if (!b.ok) throw new Error('expected ok b');
|
|
339
|
+
expect(b.value.id).toBe(idA);
|
|
340
|
+
expect(b.value.view.byteLength).toBe(128);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe('w10 - BufferPool.grow (D-6 Result semantics, D-7 no-shrink)', () => {
|
|
345
|
+
it('grow preserves prior content byte-equal across same-bucket growth', () => {
|
|
346
|
+
const pool = new BufferPool();
|
|
347
|
+
const a = pool.alloc(128);
|
|
348
|
+
if (!a.ok) throw new Error('expected ok a');
|
|
349
|
+
const view0 = a.value.view;
|
|
350
|
+
for (let i = 0; i < 128; i++) view0[i] = (i * 7) & 0xff;
|
|
351
|
+
const r = pool.grow(a.value.id, 200);
|
|
352
|
+
expect(r.ok).toBe(true);
|
|
353
|
+
if (!r.ok) throw new Error('expected ok grow');
|
|
354
|
+
const view1 = r.value;
|
|
355
|
+
expect(view1.byteLength).toBe(200);
|
|
356
|
+
for (let i = 0; i < 128; i++) expect(view1[i]).toBe((i * 7) & 0xff);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('grow(view, byteLength === current) returns ok no-op view', () => {
|
|
360
|
+
const pool = new BufferPool();
|
|
361
|
+
const a = pool.alloc(128);
|
|
362
|
+
if (!a.ok) throw new Error('expected ok a');
|
|
363
|
+
const r = pool.grow(a.value.id, 128);
|
|
364
|
+
expect(r.ok).toBe(true);
|
|
365
|
+
if (!r.ok) throw new Error('expected ok grow');
|
|
366
|
+
expect(r.value.byteLength).toBe(128);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('grow(view, smaller) returns err(managed-buffer-shrink-not-supported)', () => {
|
|
370
|
+
const pool = new BufferPool();
|
|
371
|
+
const a = pool.alloc(128);
|
|
372
|
+
if (!a.ok) throw new Error('expected ok a');
|
|
373
|
+
const r = pool.grow(a.value.id, 100);
|
|
374
|
+
expect(r.ok).toBe(false);
|
|
375
|
+
if (r.ok) throw new Error('expected err grow');
|
|
376
|
+
expect(r.error.code).toBe('managed-buffer-shrink-not-supported');
|
|
377
|
+
if (r.error.code !== 'managed-buffer-shrink-not-supported') throw new Error('narrow');
|
|
378
|
+
expect(r.error.detail.requested).toBe(100);
|
|
379
|
+
expect(r.error.detail.current).toBe(128);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('cross-bucket grow (128 -> 4096) preserves prior content byte-equal', () => {
|
|
383
|
+
const pool = new BufferPool();
|
|
384
|
+
const a = pool.alloc(128);
|
|
385
|
+
if (!a.ok) throw new Error('expected ok a');
|
|
386
|
+
const view0 = a.value.view;
|
|
387
|
+
for (let i = 0; i < 128; i++) view0[i] = (i + 1) & 0xff;
|
|
388
|
+
const r = pool.grow(a.value.id, 4096);
|
|
389
|
+
expect(r.ok).toBe(true);
|
|
390
|
+
if (!r.ok) throw new Error('expected ok grow');
|
|
391
|
+
const view1 = r.value;
|
|
392
|
+
expect(view1.byteLength).toBe(4096);
|
|
393
|
+
for (let i = 0; i < 128; i++) expect(view1[i]).toBe((i + 1) & 0xff);
|
|
394
|
+
const refreshed = pool.view(a.value.id);
|
|
395
|
+
expect(refreshed.byteLength).toBe(4096);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('alloc(beyond max bucket) returns err(managed-buffer-out-of-bounds)', () => {
|
|
399
|
+
const pool = new BufferPool();
|
|
400
|
+
const top = SIZE_CLASSES[SIZE_CLASSES.length - 1];
|
|
401
|
+
if (top === undefined) throw new Error('SIZE_CLASSES empty');
|
|
402
|
+
const overshoot = top + 1;
|
|
403
|
+
const r = pool.alloc(overshoot);
|
|
404
|
+
expect(r.ok).toBe(false);
|
|
405
|
+
if (r.ok) throw new Error('expected err alloc');
|
|
406
|
+
expect(r.error.code).toBe('managed-buffer-out-of-bounds');
|
|
407
|
+
if (r.error.code !== 'managed-buffer-out-of-bounds') throw new Error('narrow');
|
|
408
|
+
expect(r.error.detail.index).toBe(overshoot);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('M4 prelude: distinct ids on consecutive alloc with no release in between', () => {
|
|
412
|
+
const pool = new BufferPool();
|
|
413
|
+
const a = pool.alloc(128);
|
|
414
|
+
const b = pool.alloc(128);
|
|
415
|
+
if (!a.ok || !b.ok) throw new Error('expected ok x2');
|
|
416
|
+
expect(a.value.id).not.toBe(b.value.id);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
{
|
|
423
|
+
// ─── from buffer-vocab-fixup.test.ts ───
|
|
424
|
+
describe('buffer-vocab-fixup.test.ts', () => {
|
|
425
|
+
describe('verify round 1 fix-up — B1: world.set on buffer<N> validates byteLength', () => {
|
|
426
|
+
it('returns Result.err with code "fixed-size-mismatch" when M < N', () => {
|
|
427
|
+
const Print = defineComponent('Print', { fp: { type: 'buffer<16>' } });
|
|
428
|
+
const w = new World();
|
|
429
|
+
const e = w.spawn({ component: Print, data: { fp: new Uint8Array(16) } }).unwrap();
|
|
430
|
+
const r = w.set(e, Print, { fp: new Uint8Array(12) });
|
|
431
|
+
expect(r.ok).toBe(false);
|
|
432
|
+
if (!r.ok) {
|
|
433
|
+
expect(r.error.code).toBe('fixed-size-mismatch');
|
|
434
|
+
if (r.error.code === 'fixed-size-mismatch') {
|
|
435
|
+
expect(r.error.detail).toEqual({ expected: 16, actual: 12 });
|
|
436
|
+
expect(r.error.hint).toContain('buffer<16>');
|
|
437
|
+
expect(r.error.hint).toContain('byteLength 12');
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('returns Result.err with code "fixed-size-mismatch" when M > N', () => {
|
|
443
|
+
const PrintBuf8 = defineComponent('PrintBuf8', { fp: { type: 'buffer<8>' } });
|
|
444
|
+
const w = new World();
|
|
445
|
+
const e = w.spawn({ component: PrintBuf8, data: { fp: new Uint8Array(8) } }).unwrap();
|
|
446
|
+
const r = w.set(e, PrintBuf8, { fp: new Uint8Array(20) });
|
|
447
|
+
expect(r.ok).toBe(false);
|
|
448
|
+
if (!r.ok) {
|
|
449
|
+
expect(r.error.code).toBe('fixed-size-mismatch');
|
|
450
|
+
if (r.error.code === 'fixed-size-mismatch') {
|
|
451
|
+
expect(r.error.detail).toEqual({ expected: 8, actual: 20 });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('matching byteLength still succeeds and writes through to world.get snapshot', () => {
|
|
457
|
+
const Print = defineComponent('PrintBuf4', { fp: { type: 'buffer<4>' } });
|
|
458
|
+
const w = new World();
|
|
459
|
+
const e = w.spawn({ component: Print, data: { fp: new Uint8Array(4) } }).unwrap();
|
|
460
|
+
const r = w.set(e, Print, { fp: new Uint8Array([1, 2, 3, 4]) });
|
|
461
|
+
expect(r.ok).toBe(true);
|
|
462
|
+
const snapshot = w.get(e, Print).unwrap().fp;
|
|
463
|
+
expect(snapshot.byteLength).toBe(4);
|
|
464
|
+
expect(Array.from(snapshot)).toEqual([1, 2, 3, 4]);
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
describe('verify round 1 fix-up — B2: bare "buffer" (variable) spawn + set round-trip', () => {
|
|
469
|
+
it('spawn with payload bytes round-trips through world.get without data loss', () => {
|
|
470
|
+
const Blob = defineComponent('BlobVocab', { b: { type: 'buffer' } });
|
|
471
|
+
const w = new World();
|
|
472
|
+
const payload = new Uint8Array([7, 8, 9]);
|
|
473
|
+
const e = w.spawn({ component: Blob, data: { b: payload } }).unwrap();
|
|
474
|
+
const snapshot = w.get(e, Blob).unwrap().b;
|
|
475
|
+
expect(snapshot.byteLength).toBe(3);
|
|
476
|
+
expect(Array.from(snapshot)).toEqual([7, 8, 9]);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('world.set after spawn replaces variable-buffer bytes (no fixed-size constraint)', () => {
|
|
480
|
+
const Blob = defineComponent('BlobVocab2', { b: { type: 'buffer' } });
|
|
481
|
+
const w = new World();
|
|
482
|
+
const e = w.spawn({ component: Blob, data: { b: new Uint8Array([1]) } }).unwrap();
|
|
483
|
+
const r = w.set(e, Blob, { b: new Uint8Array([10, 20, 30, 40, 50]) });
|
|
484
|
+
expect(r.ok).toBe(true);
|
|
485
|
+
const snapshot = w.get(e, Blob).unwrap().b;
|
|
486
|
+
expect(snapshot.byteLength).toBe(5);
|
|
487
|
+
expect(Array.from(snapshot)).toEqual([10, 20, 30, 40, 50]);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('spawn with an empty Uint8Array yields a zero-length live view (no error)', () => {
|
|
491
|
+
const Blob = defineComponent('BlobVocab3', { b: { type: 'buffer' } });
|
|
492
|
+
const w = new World();
|
|
493
|
+
const e = w.spawn({ component: Blob, data: { b: new Uint8Array(0) } }).unwrap();
|
|
494
|
+
const snapshot = w.get(e, Blob).unwrap().b;
|
|
495
|
+
expect(snapshot.byteLength).toBe(0);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('ManagedBufferOutOfBoundsError hint references the new buffer keyword forms (no legacy "buffer:<bytes>")', () => {
|
|
499
|
+
const Blob = defineComponent('BlobVocab4', { b: { type: 'buffer' } });
|
|
500
|
+
const w = new World();
|
|
501
|
+
const oversized = new Uint8Array(262_145);
|
|
502
|
+
const result = w.spawn({ component: Blob, data: { b: oversized } });
|
|
503
|
+
expect(result.ok).toBe(true);
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
{
|
|
510
|
+
// ─── from chained-add-component-column-loss.test.ts ───
|
|
511
|
+
describe('chained-add-component-column-loss.test.ts', () => {
|
|
512
|
+
describe('chained addComponent preserves earlier-added column data (F-2)', () => {
|
|
513
|
+
it('two chained single addComponent on a >=2-component entity preserves both columns', () => {
|
|
514
|
+
const C = defineComponent('C', { a: { type: 'u32' } });
|
|
515
|
+
const D = defineComponent('D', { b: { type: 'u32' } });
|
|
516
|
+
const Base1 = defineComponent('Base1', { s: { type: 'string' } });
|
|
517
|
+
const Base2 = defineComponent('Base2', { x: { type: 'f32' } });
|
|
518
|
+
|
|
519
|
+
const w = new World();
|
|
520
|
+
|
|
521
|
+
const e = w
|
|
522
|
+
.spawn({ component: Base1, data: { s: 'hi' } }, { component: Base2, data: { x: 1.5 } })
|
|
523
|
+
.unwrap();
|
|
524
|
+
|
|
525
|
+
w.addComponent(e, { component: C, data: { a: 1033 } }).unwrap();
|
|
526
|
+
w.addComponent(e, { component: D, data: { b: 55 } }).unwrap();
|
|
527
|
+
|
|
528
|
+
const cGet = w.get(e, C);
|
|
529
|
+
const dGet = w.get(e, D);
|
|
530
|
+
if (!cGet.ok) throw new Error('expected ok get C');
|
|
531
|
+
if (!dGet.ok) throw new Error('expected ok get D');
|
|
532
|
+
expect(cGet.value.a).toBe(1033);
|
|
533
|
+
expect(dGet.value.b).toBe(55);
|
|
534
|
+
|
|
535
|
+
const b1 = w.get(e, Base1);
|
|
536
|
+
const b2 = w.get(e, Base2);
|
|
537
|
+
if (!b1.ok || !b2.ok) throw new Error('expected ok get base');
|
|
538
|
+
expect(b1.value.s).toBe('hi');
|
|
539
|
+
expect(b2.value.x).toBeCloseTo(1.5);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('single-component base + two chained adds (registration adds-first)', () => {
|
|
543
|
+
const C = defineComponent('SC', { a: { type: 'u32' } });
|
|
544
|
+
const D = defineComponent('SD', { b: { type: 'u32' } });
|
|
545
|
+
const Base = defineComponent('SBase', { s: { type: 'string' } });
|
|
546
|
+
|
|
547
|
+
const w = new World();
|
|
548
|
+
|
|
549
|
+
const e = w.spawn({ component: Base, data: { s: 'hi' } }).unwrap();
|
|
550
|
+
w.addComponent(e, { component: C, data: { a: 1033 } }).unwrap();
|
|
551
|
+
w.addComponent(e, { component: D, data: { b: 55 } }).unwrap();
|
|
552
|
+
|
|
553
|
+
const cGet = w.get(e, C);
|
|
554
|
+
if (!cGet.ok) throw new Error('expected ok get C');
|
|
555
|
+
expect(cGet.value.a).toBe(1033);
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
{
|
|
562
|
+
// ─── from column.test.ts ───
|
|
563
|
+
describe('column.test.ts', () => {
|
|
564
|
+
describe('Column.arity', () => {
|
|
565
|
+
it('scalar column arity defaults to 1 (AC-01)', () => {
|
|
566
|
+
const col = createColumn('f32', 64);
|
|
567
|
+
expect(col.arity).toBe(1);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('all 11 scalar field types default arity to 1', () => {
|
|
571
|
+
const types: ScalarFieldType[] = [
|
|
572
|
+
'f32',
|
|
573
|
+
'f64',
|
|
574
|
+
'i32',
|
|
575
|
+
'u32',
|
|
576
|
+
'i16',
|
|
577
|
+
'u16',
|
|
578
|
+
'i8',
|
|
579
|
+
'u8',
|
|
580
|
+
'bool',
|
|
581
|
+
'enum',
|
|
582
|
+
'ref',
|
|
583
|
+
];
|
|
584
|
+
for (const t of types) {
|
|
585
|
+
const col = createColumn(t, 16);
|
|
586
|
+
expect(col.arity, `Column.arity for ${t} should be 1`).toBe(1);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it('empty column (capacity=0) growColumn does not divide by zero', () => {
|
|
591
|
+
const col = createColumn('f32', 0, 1);
|
|
592
|
+
expect(col.capacity).toBe(0);
|
|
593
|
+
expect(col.arity).toBe(1);
|
|
594
|
+
const grown = growColumn(col, 8);
|
|
595
|
+
expect(grown.capacity).toBe(8);
|
|
596
|
+
expect(grown.arity).toBe(1);
|
|
597
|
+
expect(grown.buffer.byteLength).toBe(32);
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
describe('createColumn', () => {
|
|
602
|
+
it('creates a column with correct capacity', () => {
|
|
603
|
+
const col = createColumn('f32', 64);
|
|
604
|
+
expect(col.capacity).toBe(64);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('creates an ArrayBuffer of correct byte length', () => {
|
|
608
|
+
const col = createColumn('f32', 64);
|
|
609
|
+
expect(col.buffer.byteLength).toBe(64 * 4);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('creates a TypedArray view of correct type for f32', () => {
|
|
613
|
+
const col = createColumn('f32', 16);
|
|
614
|
+
expect(col.view).toBeInstanceOf(Float32Array);
|
|
615
|
+
expect(col.view.length).toBe(16);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('creates correct TypedArray view for each of the 11 scalar types', () => {
|
|
619
|
+
const typeToView: Record<ScalarFieldType, unknown> = {
|
|
620
|
+
f32: Float32Array,
|
|
621
|
+
f64: Float64Array,
|
|
622
|
+
i32: Int32Array,
|
|
623
|
+
u32: Uint32Array,
|
|
624
|
+
i16: Int16Array,
|
|
625
|
+
u16: Uint16Array,
|
|
626
|
+
i8: Int8Array,
|
|
627
|
+
u8: Uint8Array,
|
|
628
|
+
bool: Uint8Array,
|
|
629
|
+
enum: Uint32Array,
|
|
630
|
+
ref: Uint32Array,
|
|
631
|
+
};
|
|
632
|
+
for (const [fieldType, ViewCtor] of Object.entries(typeToView)) {
|
|
633
|
+
const col = createColumn(fieldType as ScalarFieldType, 8);
|
|
634
|
+
// biome-ignore lint/complexity/noBannedTypes: ViewCtor is a constructor from Object.entries, Function is the correct generic type here
|
|
635
|
+
expect(col.view).toBeInstanceOf(ViewCtor as Function);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('each field has an independent ArrayBuffer (not shared)', () => {
|
|
640
|
+
const col1 = createColumn('f32', 16);
|
|
641
|
+
const col2 = createColumn('f32', 16);
|
|
642
|
+
expect(col1.buffer).not.toBe(col2.buffer);
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
describe('growColumn', () => {
|
|
647
|
+
it('grows an ArrayBuffer column when SharedArrayBuffer is unavailable', () => {
|
|
648
|
+
const col = createColumn('f32', 4);
|
|
649
|
+
col.view[0] = 7;
|
|
650
|
+
vi.stubGlobal('SharedArrayBuffer', undefined);
|
|
651
|
+
try {
|
|
652
|
+
const grown = growColumn(col, 8);
|
|
653
|
+
expect(grown.view[0]).toBe(7);
|
|
654
|
+
expect(grown.capacity).toBe(8);
|
|
655
|
+
} finally {
|
|
656
|
+
vi.unstubAllGlobals();
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('grows column to new capacity with data intact', () => {
|
|
661
|
+
const col = createColumn('f32', 4);
|
|
662
|
+
col.view[0] = 1.5;
|
|
663
|
+
col.view[1] = 2.5;
|
|
664
|
+
col.view[2] = 3.5;
|
|
665
|
+
|
|
666
|
+
const grown = growColumn(col, 8);
|
|
667
|
+
expect(grown.capacity).toBe(8);
|
|
668
|
+
expect(grown.view[0]).toBeCloseTo(1.5);
|
|
669
|
+
expect(grown.view[1]).toBeCloseTo(2.5);
|
|
670
|
+
expect(grown.view[2]).toBeCloseTo(3.5);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('old column is not affected by grow (fallback path)', () => {
|
|
674
|
+
if (HAS_TRANSFER) {
|
|
675
|
+
const col = createColumn('i32', 4);
|
|
676
|
+
col.view[0] = 42;
|
|
677
|
+
const grown = growColumn(col, 8);
|
|
678
|
+
expect(grown.view[0]).toBe(42);
|
|
679
|
+
expect(col.buffer.byteLength).toBe(0);
|
|
680
|
+
} else {
|
|
681
|
+
const col = createColumn('i32', 4);
|
|
682
|
+
col.view[0] = 42;
|
|
683
|
+
const grown = growColumn(col, 8);
|
|
684
|
+
grown.view[0] = 99;
|
|
685
|
+
expect(col.view[0]).toBe(42);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('works for all scalar field types', () => {
|
|
690
|
+
const types: ScalarFieldType[] = [
|
|
691
|
+
'f32',
|
|
692
|
+
'f64',
|
|
693
|
+
'i32',
|
|
694
|
+
'u32',
|
|
695
|
+
'i16',
|
|
696
|
+
'u16',
|
|
697
|
+
'i8',
|
|
698
|
+
'u8',
|
|
699
|
+
'bool',
|
|
700
|
+
'enum',
|
|
701
|
+
'ref',
|
|
702
|
+
];
|
|
703
|
+
for (const t of types) {
|
|
704
|
+
const col = createColumn(t, 4);
|
|
705
|
+
col.view[0] = 1;
|
|
706
|
+
const grown = growColumn(col, 8);
|
|
707
|
+
expect(grown.view[0]).toBe(1);
|
|
708
|
+
expect(grown.capacity).toBe(8);
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
describe('growColumn transfer() / fallback equivalence', () => {
|
|
714
|
+
function fallbackGrow(
|
|
715
|
+
col: {
|
|
716
|
+
buffer: ArrayBufferLike;
|
|
717
|
+
fieldType: ScalarFieldType;
|
|
718
|
+
capacity: number;
|
|
719
|
+
view: { BYTES_PER_ELEMENT: number };
|
|
720
|
+
},
|
|
721
|
+
newCapacity: number,
|
|
722
|
+
) {
|
|
723
|
+
const byteLen = col.view.BYTES_PER_ELEMENT * newCapacity;
|
|
724
|
+
const buf = new ArrayBuffer(byteLen);
|
|
725
|
+
new Uint8Array(buf).set(new Uint8Array(col.buffer));
|
|
726
|
+
return buf;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
it('both paths produce identical data for f32', () => {
|
|
730
|
+
const col = createColumn('f32', 4);
|
|
731
|
+
col.view[0] = 1.5;
|
|
732
|
+
col.view[1] = -3.14;
|
|
733
|
+
col.view[2] = 0;
|
|
734
|
+
col.view[3] = 999.999;
|
|
735
|
+
|
|
736
|
+
const fallbackBuf = fallbackGrow(col, 8);
|
|
737
|
+
|
|
738
|
+
const grown = growColumn(col, 8);
|
|
739
|
+
const grownBytes = new Uint8Array(grown.buffer, 0, 16);
|
|
740
|
+
const fallbackBytes = new Uint8Array(fallbackBuf, 0, 16);
|
|
741
|
+
expect(Array.from(grownBytes)).toEqual(Array.from(fallbackBytes));
|
|
742
|
+
expect(grown.view[0]).toBeCloseTo(1.5);
|
|
743
|
+
expect(grown.view[1]).toBeCloseTo(-3.14);
|
|
744
|
+
expect(grown.view[3]).toBeCloseTo(999.999);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('both paths produce identical data for i32', () => {
|
|
748
|
+
const col = createColumn('i32', 4);
|
|
749
|
+
col.view[0] = -2147483648;
|
|
750
|
+
col.view[1] = 2147483647;
|
|
751
|
+
col.view[2] = 0;
|
|
752
|
+
col.view[3] = 42;
|
|
753
|
+
|
|
754
|
+
const fallbackBuf = fallbackGrow(col, 8);
|
|
755
|
+
|
|
756
|
+
const grown = growColumn(col, 8);
|
|
757
|
+
const grownBytes = new Uint8Array(grown.buffer, 0, 16);
|
|
758
|
+
const fallbackBytes = new Uint8Array(fallbackBuf, 0, 16);
|
|
759
|
+
expect(Array.from(grownBytes)).toEqual(Array.from(fallbackBytes));
|
|
760
|
+
expect(grown.view[0]).toBe(-2147483648);
|
|
761
|
+
expect(grown.view[1]).toBe(2147483647);
|
|
762
|
+
expect(grown.view[3]).toBe(42);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('both paths produce identical data for u8 (bool backing)', () => {
|
|
766
|
+
const col = createColumn('u8', 4);
|
|
767
|
+
col.view[0] = 0;
|
|
768
|
+
col.view[1] = 1;
|
|
769
|
+
col.view[2] = 255;
|
|
770
|
+
col.view[3] = 128;
|
|
771
|
+
|
|
772
|
+
const fallbackBuf = fallbackGrow(col, 16);
|
|
773
|
+
|
|
774
|
+
const grown = growColumn(col, 16);
|
|
775
|
+
const grownBytes = new Uint8Array(grown.buffer, 0, 4);
|
|
776
|
+
const fallbackBytes = new Uint8Array(fallbackBuf, 0, 4);
|
|
777
|
+
expect(Array.from(grownBytes)).toEqual(Array.from(fallbackBytes));
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it('data is not lost after growing to 2x, 4x, 8x capacity', () => {
|
|
781
|
+
let col = createColumn('f64', 2);
|
|
782
|
+
col.view[0] = Math.PI;
|
|
783
|
+
col.view[1] = Math.E;
|
|
784
|
+
|
|
785
|
+
col = growColumn(col, 4);
|
|
786
|
+
expect(col.view[0]).toBeCloseTo(Math.PI, 14);
|
|
787
|
+
expect(col.view[1]).toBeCloseTo(Math.E, 14);
|
|
788
|
+
|
|
789
|
+
col = growColumn(col, 8);
|
|
790
|
+
expect(col.view[0]).toBeCloseTo(Math.PI, 14);
|
|
791
|
+
expect(col.view[1]).toBeCloseTo(Math.E, 14);
|
|
792
|
+
|
|
793
|
+
col = growColumn(col, 16);
|
|
794
|
+
expect(col.view[0]).toBeCloseTo(Math.PI, 14);
|
|
795
|
+
expect(col.view[1]).toBeCloseTo(Math.E, 14);
|
|
796
|
+
expect(col.capacity).toBe(16);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it('new capacity region is zeroed', () => {
|
|
800
|
+
const col = createColumn('i32', 4);
|
|
801
|
+
col.view[0] = 100;
|
|
802
|
+
col.view[1] = 200;
|
|
803
|
+
|
|
804
|
+
const grown = growColumn(col, 8);
|
|
805
|
+
for (let i = 4; i < 8; i++) {
|
|
806
|
+
expect(grown.view[i]).toBe(0);
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it('HAS_TRANSFER is a boolean', () => {
|
|
811
|
+
expect(typeof HAS_TRANSFER).toBe('boolean');
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
describe('growColumn stride-N data fidelity (AC-05)', () => {
|
|
816
|
+
const ARITY = 16;
|
|
817
|
+
const PATTERN = Array.from({ length: ARITY }, (_, i) => i * 1.5 + 0.1);
|
|
818
|
+
|
|
819
|
+
function writeRow0(col: ReturnType<typeof createColumn>, vals: ReadonlyArray<number>) {
|
|
820
|
+
for (let i = 0; i < vals.length; i++) {
|
|
821
|
+
const v = vals[i];
|
|
822
|
+
if (v === undefined) throw new Error('unexpected');
|
|
823
|
+
col.view[i] = v;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function expectRow0Unchanged(col: ReturnType<typeof createColumn>) {
|
|
828
|
+
for (let i = 0; i < ARITY; i++) {
|
|
829
|
+
const expected = PATTERN[i];
|
|
830
|
+
if (expected === undefined) throw new Error('unexpected');
|
|
831
|
+
expect(col.view[i], `row[0][${i}] after capacity ${col.capacity}`).toBeCloseTo(
|
|
832
|
+
expected,
|
|
833
|
+
6,
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
it('grow 0->4->8->16->32->64 preserves row 0 stride-16 data', () => {
|
|
839
|
+
let col = createColumn('f32', 0, ARITY);
|
|
840
|
+
expect(col.arity).toBe(ARITY);
|
|
841
|
+
expect(col.capacity).toBe(0);
|
|
842
|
+
|
|
843
|
+
const stages = [4, 8, 16, 32, 64];
|
|
844
|
+
for (const cap of stages) {
|
|
845
|
+
col = growColumn(col, cap);
|
|
846
|
+
expect(col.capacity).toBe(cap);
|
|
847
|
+
expect(col.arity).toBe(ARITY);
|
|
848
|
+
|
|
849
|
+
if (cap === 4) {
|
|
850
|
+
writeRow0(col, PATTERN);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
expectRow0Unchanged(col);
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it('grow from nonzero capacity preserves row 0 for arity=16', () => {
|
|
858
|
+
let col = createColumn('f32', 8, ARITY);
|
|
859
|
+
writeRow0(col, PATTERN);
|
|
860
|
+
|
|
861
|
+
const stages = [16, 32, 64, 128];
|
|
862
|
+
for (const cap of stages) {
|
|
863
|
+
col = growColumn(col, cap);
|
|
864
|
+
expect(col.capacity).toBe(cap);
|
|
865
|
+
expect(col.arity).toBe(ARITY);
|
|
866
|
+
expectRow0Unchanged(col);
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
it('grow from capacity 1 preserves arity=4 f64 row', () => {
|
|
871
|
+
const f64pattern = [Math.PI, Math.E, Math.LN2, Math.SQRT2];
|
|
872
|
+
let col = createColumn('f64', 1, 4);
|
|
873
|
+
expect(col.arity).toBe(4);
|
|
874
|
+
for (let i = 0; i < 4; i++) {
|
|
875
|
+
const v = f64pattern[i];
|
|
876
|
+
if (v === undefined) throw new Error('unexpected');
|
|
877
|
+
col.view[i] = v;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const stages = [2, 4, 8, 16, 32];
|
|
881
|
+
for (const cap of stages) {
|
|
882
|
+
col = growColumn(col, cap);
|
|
883
|
+
for (let i = 0; i < 4; i++) {
|
|
884
|
+
const v = f64pattern[i];
|
|
885
|
+
if (v === undefined) throw new Error('unexpected');
|
|
886
|
+
expect(col.view[i], `f64[${i}] at capacity ${cap}`).toBeCloseTo(v, 14);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it('new capacity tail rows are zeroed for arity>1', () => {
|
|
892
|
+
const AR = 3;
|
|
893
|
+
const col = createColumn('u32', 2, AR);
|
|
894
|
+
col.view[0] = 0xdeadbeef;
|
|
895
|
+
col.view[1] = 0xcafebabe;
|
|
896
|
+
col.view[2] = 0x12345678;
|
|
897
|
+
|
|
898
|
+
const grown = growColumn(col, 4);
|
|
899
|
+
for (let i = 3; i < 6; i++) {
|
|
900
|
+
expect(grown.view[i], `tail row[${i}]`).toBe(0);
|
|
901
|
+
}
|
|
902
|
+
expect(grown.view[0]).toBe(0xdeadbeef);
|
|
903
|
+
expect(grown.view[1]).toBe(0xcafebabe);
|
|
904
|
+
expect(grown.view[2]).toBe(0x12345678);
|
|
905
|
+
});
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
describe('isHotSchema', () => {
|
|
909
|
+
it('returns true for all-scalar schema', () => {
|
|
910
|
+
const schema: ComponentSchema = { x: 'f32', y: 'f32', z: 'f32' };
|
|
911
|
+
expect(isHotSchema(schema)).toBe(true);
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
it('returns true for single scalar field', () => {
|
|
915
|
+
expect(isHotSchema({ hp: 'i32' })).toBe(true);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
it('returns true for all 11 scalar types mixed', () => {
|
|
919
|
+
const schema: ComponentSchema = {
|
|
920
|
+
a: 'f32',
|
|
921
|
+
b: 'f64',
|
|
922
|
+
c: 'i32',
|
|
923
|
+
d: 'u32',
|
|
924
|
+
e: 'i16',
|
|
925
|
+
f: 'u16',
|
|
926
|
+
g: 'i8',
|
|
927
|
+
h: 'u8',
|
|
928
|
+
i: 'bool',
|
|
929
|
+
j: 'enum',
|
|
930
|
+
k: 'ref',
|
|
931
|
+
};
|
|
932
|
+
expect(isHotSchema(schema)).toBe(true);
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
it('returns true for empty schema (tag component)', () => {
|
|
936
|
+
expect(isHotSchema({})).toBe(true);
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
it('returns false for schema with unsupported field type (cold-table path)', () => {
|
|
940
|
+
const nonScalarSchema = { data: 'string' as unknown as 'f32' };
|
|
941
|
+
expect(isHotSchema(nonScalarSchema)).toBe(false);
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
{
|
|
948
|
+
// ─── from entity-column.test.ts ───
|
|
949
|
+
let dummyCounter = 0;
|
|
950
|
+
function defineSpawnComponent(prefix: string) {
|
|
951
|
+
dummyCounter += 1;
|
|
952
|
+
return defineComponent(`${prefix}_${dummyCounter}`, { x: 'f32', y: 'f32' });
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function archetypeOf(world: World, e: EntityHandle): Table {
|
|
956
|
+
const graph = (world as unknown as { graph: { archetypes: Archetype[]; tables: Table[] } })
|
|
957
|
+
.graph;
|
|
958
|
+
const records = (world as unknown as { records: { archetypeId: number }[] }).records;
|
|
959
|
+
const indexSlot = (e as unknown as number) & 0xffffff;
|
|
960
|
+
const archId = records[indexSlot]?.archetypeId ?? -1;
|
|
961
|
+
const arch = graph.archetypes[archId];
|
|
962
|
+
if (!arch) throw new Error('archetype not found for entity');
|
|
963
|
+
const table = graph.tables[arch.tableId];
|
|
964
|
+
if (!table) throw new Error('table not found for entity');
|
|
965
|
+
return table;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
describe('entity-column.test.ts', () => {
|
|
969
|
+
describe('Entity id=0 structural guarantee (AC-01)', () => {
|
|
970
|
+
it('assigns the Entity component id=0', () => {
|
|
971
|
+
expect(componentId(Entity)).toBe(0);
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
it('names the single field self with the entity field type', () => {
|
|
975
|
+
expect(Entity.name).toBe('Entity');
|
|
976
|
+
expect(componentSchema(Entity)).toEqual({ self: 'entity' });
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
describe('Entity barrel forced-registration fail-fast (AC-01)', () => {
|
|
981
|
+
afterEach(() => {
|
|
982
|
+
vi.resetModules();
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
it('throws at barrel eval when a component registers before Entity', async () => {
|
|
986
|
+
vi.resetModules();
|
|
987
|
+
const { defineComponent: freshDefine } = await import('../component');
|
|
988
|
+
freshDefine('TamperOrderDummy', { x: 'f32' });
|
|
989
|
+
// feat-20260611 D-9: assertion text switched from `expected id=0` to
|
|
990
|
+
// `ESSENTIAL_COMPONENT_IDS invariant violated` (SSOT-driven contract).
|
|
991
|
+
await expect(import('../index')).rejects.toThrow(/ESSENTIAL_COMPONENT_IDS/);
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
it('does not throw at barrel eval under the normal import order', async () => {
|
|
995
|
+
vi.resetModules();
|
|
996
|
+
await expect(import('../index')).resolves.toBeDefined();
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
describe('archetype unconditionally carries the id=0 column (AC-02)', () => {
|
|
1001
|
+
it('includes the Entity column in a single-component spawn archetype', () => {
|
|
1002
|
+
const world = new World();
|
|
1003
|
+
const Position = defineSpawnComponent('AC02_Pos');
|
|
1004
|
+
const e = world.spawn({ component: Position, data: { x: 1, y: 2 } }).unwrap();
|
|
1005
|
+
const arch = archetypeOf(world, e);
|
|
1006
|
+
expect(arch.storage.has(componentId(Entity))).toBe(true);
|
|
1007
|
+
expect(arch.storage.get(componentId(Entity))?.fields.has('self')).toBe(true);
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
it('includes the Entity column in a bare (zero-component) spawn archetype', () => {
|
|
1011
|
+
const world = new World();
|
|
1012
|
+
const e = world.spawn().unwrap();
|
|
1013
|
+
const arch = archetypeOf(world, e);
|
|
1014
|
+
expect(arch.storage.has(componentId(Entity))).toBe(true);
|
|
1015
|
+
expect(arch.storage.get(componentId(Entity))?.fields.has('self')).toBe(true);
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
it('includes the Entity column after addComponent migration', () => {
|
|
1019
|
+
const world = new World();
|
|
1020
|
+
const A = defineSpawnComponent('AC02_A');
|
|
1021
|
+
const B = defineSpawnComponent('AC02_B');
|
|
1022
|
+
const e = world.spawn({ component: A, data: { x: 0, y: 0 } }).unwrap();
|
|
1023
|
+
world.addComponent(e, { component: B, data: { x: 0, y: 0 } }).unwrap();
|
|
1024
|
+
const arch = archetypeOf(world, e);
|
|
1025
|
+
expect(arch.storage.has(componentId(Entity))).toBe(true);
|
|
1026
|
+
expect(arch.storage.get(componentId(Entity))?.fields.has('self')).toBe(true);
|
|
1027
|
+
});
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
describe('world.get(e, Entity).self carries the row handle (AC-03)', () => {
|
|
1031
|
+
it('reads the full packed handle off the id=0 column for a single-component entity', () => {
|
|
1032
|
+
const world = new World();
|
|
1033
|
+
const Position = defineSpawnComponent('AC03_Pos');
|
|
1034
|
+
const e = world.spawn({ component: Position, data: { x: 1, y: 2 } }).unwrap();
|
|
1035
|
+
const got = world.get(e, Entity).unwrap();
|
|
1036
|
+
const self: EntityHandle | null = got.self;
|
|
1037
|
+
expect(self).toBe(e);
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
it('reads the handle off a bare (zero-component) entity', () => {
|
|
1041
|
+
const world = new World();
|
|
1042
|
+
const e = world.spawn().unwrap();
|
|
1043
|
+
expect(world.get(e, Entity).unwrap().self).toBe(e);
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
it('preserves the handle across an addComponent archetype migration', () => {
|
|
1047
|
+
const world = new World();
|
|
1048
|
+
const A = defineSpawnComponent('AC03_A');
|
|
1049
|
+
const B = defineSpawnComponent('AC03_B');
|
|
1050
|
+
const e = world.spawn({ component: A, data: { x: 0, y: 0 } }).unwrap();
|
|
1051
|
+
world.addComponent(e, { component: B, data: { x: 0, y: 0 } }).unwrap();
|
|
1052
|
+
expect(world.get(e, Entity).unwrap().self).toBe(e);
|
|
1053
|
+
});
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
describe('world.get(e, Entity) as a liveness probe (AC-04)', () => {
|
|
1057
|
+
it('returns stale-entity for a despawned handle', () => {
|
|
1058
|
+
const world = new World();
|
|
1059
|
+
const Position = defineSpawnComponent('AC04_Pos');
|
|
1060
|
+
const e = world.spawn({ component: Position, data: { x: 0, y: 0 } }).unwrap();
|
|
1061
|
+
world.despawn(e).unwrap();
|
|
1062
|
+
const r = world.get(e, Entity);
|
|
1063
|
+
expect(r.ok).toBe(false);
|
|
1064
|
+
if (!r.ok) {
|
|
1065
|
+
expect(r.error.code).toBe('stale-entity');
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
describe('removeComponent(e, Entity) is rejected (AC-05)', () => {
|
|
1071
|
+
it('returns remove-essential-component and leaves the Entity column intact', () => {
|
|
1072
|
+
const world = new World();
|
|
1073
|
+
const Position = defineSpawnComponent('AC05_Pos');
|
|
1074
|
+
const e = world.spawn({ component: Position, data: { x: 0, y: 0 } }).unwrap();
|
|
1075
|
+
const r = world.removeComponent(e, Entity);
|
|
1076
|
+
expect(r.ok).toBe(false);
|
|
1077
|
+
if (!r.ok) {
|
|
1078
|
+
expect(r.error.code).toBe('remove-essential-component');
|
|
1079
|
+
if (r.error.code === 'remove-essential-component') {
|
|
1080
|
+
expect(r.error.detail.componentName).toBe('Entity');
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const arch = archetypeOf(world, e);
|
|
1084
|
+
expect(arch.storage.has(componentId(Entity))).toBe(true);
|
|
1085
|
+
expect(world.get(e, Entity).unwrap().self).toBe(e);
|
|
1086
|
+
});
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
{
|
|
1092
|
+
// ─── from fixed-array-inline.test.ts ───
|
|
1093
|
+
describe('fixed-array-inline.test.ts', () => {
|
|
1094
|
+
describe('fixed array<f32, 16> inline column (AC-02 / w5)', () => {
|
|
1095
|
+
it('spawn->get round-trips all 16 f32 elements', () => {
|
|
1096
|
+
const Mat = defineComponent('Mat16', { m: 'array<f32, 16>' });
|
|
1097
|
+
const world = new World();
|
|
1098
|
+
const values = Float32Array.from({ length: 16 }, (_, i) => (i + 1) * 1.5);
|
|
1099
|
+
const e = world.spawn({ component: Mat, data: { m: values } }).unwrap();
|
|
1100
|
+
|
|
1101
|
+
const read = world.get(e, Mat).unwrap().m;
|
|
1102
|
+
expect(read).toBeInstanceOf(Float32Array);
|
|
1103
|
+
expect(read.length).toBe(16);
|
|
1104
|
+
for (let i = 0; i < 16; i++) {
|
|
1105
|
+
expect(read[i]).toBe(values[i]);
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
// Regression (downstream template integration #2): the plain-JS-array
|
|
1110
|
+
// convenience path used to pack elements by byte SIZE, so a 4-byte f32
|
|
1111
|
+
// went through setUint32 -- storing `1` as the integer bits 0x00000001
|
|
1112
|
+
// (reads back ~1.4e-45 ~ 0). AnimationPlayer.speeds=[1] froze on frame 0.
|
|
1113
|
+
// Packing must dispatch on the declared element TYPE, not byte size.
|
|
1114
|
+
it('plain-JS-array write to array<f32,4> stores f32 VALUES, not u32 bits', () => {
|
|
1115
|
+
const C = defineComponent('PlainF32Fixed', { v: 'array<f32, 4>' });
|
|
1116
|
+
const world = new World();
|
|
1117
|
+
const e = world.spawn({ component: C, data: { v: [1, 0.5, -2, 3.25] } }).unwrap();
|
|
1118
|
+
|
|
1119
|
+
const read = world.get(e, C).unwrap().v;
|
|
1120
|
+
expect(read).toBeInstanceOf(Float32Array);
|
|
1121
|
+
expect(read[0]).toBe(1);
|
|
1122
|
+
expect(read[1]).toBe(0.5);
|
|
1123
|
+
expect(read[2]).toBe(-2);
|
|
1124
|
+
expect(read[3]).toBe(3.25);
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
it('plain-JS-array write to variable array<f32> stores f32 VALUES', () => {
|
|
1128
|
+
const C = defineComponent('PlainF32Var', { v: 'array<f32>' });
|
|
1129
|
+
const world = new World();
|
|
1130
|
+
const e = world.spawn({ component: C, data: { v: [1, 2.5] } }).unwrap();
|
|
1131
|
+
|
|
1132
|
+
const read = world.get(e, C).unwrap().v;
|
|
1133
|
+
expect(read).toBeInstanceOf(Float32Array);
|
|
1134
|
+
expect(read.length).toBe(2);
|
|
1135
|
+
expect(read[0]).toBe(1);
|
|
1136
|
+
expect(read[1]).toBe(2.5);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
// Control: u32 elements are genuinely integers -- the type-keyed packer
|
|
1140
|
+
// must keep storing them verbatim (no regression in the handle/u32 path).
|
|
1141
|
+
it('plain-JS-array write to array<u32,3> still stores integer values verbatim', () => {
|
|
1142
|
+
const C = defineComponent('PlainU32Fixed', { v: 'array<u32, 3>' });
|
|
1143
|
+
const world = new World();
|
|
1144
|
+
const e = world.spawn({ component: C, data: { v: [7, 42, 1000] } }).unwrap();
|
|
1145
|
+
|
|
1146
|
+
const read = world.get(e, C).unwrap().v;
|
|
1147
|
+
expect(read).toBeInstanceOf(Uint32Array);
|
|
1148
|
+
expect(read[0]).toBe(7);
|
|
1149
|
+
expect(read[1]).toBe(42);
|
|
1150
|
+
expect(read[2]).toBe(1000);
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
describe('fixed buffer<64> inline column (AC-03 / w6)', () => {
|
|
1155
|
+
it('spawn->get round-trips all 64 bytes', () => {
|
|
1156
|
+
const Blob = defineComponent('Blob64', { b: 'buffer<64>' });
|
|
1157
|
+
const world = new World();
|
|
1158
|
+
const bytes = Uint8Array.from({ length: 64 }, (_, i) => (i * 7 + 3) & 0xff);
|
|
1159
|
+
const e = world.spawn({ component: Blob, data: { b: bytes } }).unwrap();
|
|
1160
|
+
|
|
1161
|
+
const read = world.get(e, Blob).unwrap().b;
|
|
1162
|
+
expect(read).toBeInstanceOf(Uint8Array);
|
|
1163
|
+
expect(read.length).toBe(64);
|
|
1164
|
+
for (let i = 0; i < 64; i++) {
|
|
1165
|
+
expect(read[i]).toBe(bytes[i]);
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
describe('fixed array<entity, 4> inline column (AC-04 / w7)', () => {
|
|
1171
|
+
it('spawn->get reinterprets the 4 entity ids as Uint32Array', () => {
|
|
1172
|
+
const Refs = defineComponent('EntRefs4', { targets: 'array<entity, 4>' });
|
|
1173
|
+
const world = new World();
|
|
1174
|
+
const Tag = defineComponent('FixedArrTag', {});
|
|
1175
|
+
const t0 = world.spawn({ component: Tag, data: {} }).unwrap();
|
|
1176
|
+
const t1 = world.spawn({ component: Tag, data: {} }).unwrap();
|
|
1177
|
+
const t2 = world.spawn({ component: Tag, data: {} }).unwrap();
|
|
1178
|
+
const t3 = world.spawn({ component: Tag, data: {} }).unwrap();
|
|
1179
|
+
const ids = Uint32Array.of(t0 as number, t1 as number, t2 as number, t3 as number);
|
|
1180
|
+
|
|
1181
|
+
const holder = world.spawn({ component: Refs, data: { targets: ids } }).unwrap();
|
|
1182
|
+
const read = world.get(holder, Refs).unwrap().targets;
|
|
1183
|
+
expect(read).toBeInstanceOf(Uint32Array);
|
|
1184
|
+
expect(read.length).toBe(4);
|
|
1185
|
+
expect(read[0]).toBe(t0 as number);
|
|
1186
|
+
expect(read[1]).toBe(t1 as number);
|
|
1187
|
+
expect(read[2]).toBe(t2 as number);
|
|
1188
|
+
expect(read[3]).toBe(t3 as number);
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
it('_getArrayView serves a non-f32 element type after the f32 gate is removed', () => {
|
|
1192
|
+
const Refs = defineComponent('EntRefsView4', { targets: 'array<entity, 4>' });
|
|
1193
|
+
const world = new World();
|
|
1194
|
+
const Tag = defineComponent('TagView', {});
|
|
1195
|
+
const t0 = world.spawn({ component: Tag, data: {} }).unwrap();
|
|
1196
|
+
const t1 = world.spawn({ component: Tag, data: {} }).unwrap();
|
|
1197
|
+
const ids = Uint32Array.of(t0 as number, t1 as number, 0, 0);
|
|
1198
|
+
const holder = world.spawn({ component: Refs, data: { targets: ids } }).unwrap();
|
|
1199
|
+
|
|
1200
|
+
const view = world[worldInternal].getArrayView(holder, Refs, 'targets');
|
|
1201
|
+
expect(view).toBeDefined();
|
|
1202
|
+
expect(view?.length).toBe(4);
|
|
1203
|
+
expect(view?.[0]).toBe(t0 as number);
|
|
1204
|
+
expect(view?.[1]).toBe(t1 as number);
|
|
1205
|
+
});
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
describe('swap-remove migrates the whole stride-N block (AC-06 / w8)', () => {
|
|
1209
|
+
it('removing a middle row moves the last row block intact', () => {
|
|
1210
|
+
const Mat = defineComponent('Mat16Swap', { m: 'array<f32, 16>' });
|
|
1211
|
+
const world = new World();
|
|
1212
|
+
const row = (base: number) => Float32Array.from({ length: 16 }, (_, i) => base + i);
|
|
1213
|
+
const e0 = world.spawn({ component: Mat, data: { m: row(100) } }).unwrap();
|
|
1214
|
+
const e1 = world.spawn({ component: Mat, data: { m: row(200) } }).unwrap();
|
|
1215
|
+
const e2 = world.spawn({ component: Mat, data: { m: row(300) } }).unwrap();
|
|
1216
|
+
|
|
1217
|
+
world.despawn(e0);
|
|
1218
|
+
|
|
1219
|
+
const r2 = world.get(e2, Mat).unwrap().m;
|
|
1220
|
+
expect(r2.length).toBe(16);
|
|
1221
|
+
for (let i = 0; i < 16; i++) {
|
|
1222
|
+
expect(r2[i]).toBe(300 + i);
|
|
1223
|
+
}
|
|
1224
|
+
const r1 = world.get(e1, Mat).unwrap().m;
|
|
1225
|
+
for (let i = 0; i < 16; i++) {
|
|
1226
|
+
expect(r1[i]).toBe(200 + i);
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
describe('fixed-only component leaves BufferPool live-count at 0 (AC-07 / w9)', () => {
|
|
1232
|
+
const liveCount = (world: World): number =>
|
|
1233
|
+
(world as unknown as { bufferPool: { _liveCount(): number } }).bufferPool._liveCount();
|
|
1234
|
+
|
|
1235
|
+
it('array<f32,16>-only spawn->get->despawn never touches the pool', () => {
|
|
1236
|
+
const Mat = defineComponent('Mat16Pool', { m: 'array<f32, 16>' });
|
|
1237
|
+
const world = new World();
|
|
1238
|
+
expect(liveCount(world)).toBe(0);
|
|
1239
|
+
|
|
1240
|
+
const e = world
|
|
1241
|
+
.spawn({ component: Mat, data: { m: Float32Array.from({ length: 16 }, (_, i) => i) } })
|
|
1242
|
+
.unwrap();
|
|
1243
|
+
expect(liveCount(world)).toBe(0);
|
|
1244
|
+
|
|
1245
|
+
world.get(e, Mat).unwrap().m;
|
|
1246
|
+
expect(liveCount(world)).toBe(0);
|
|
1247
|
+
|
|
1248
|
+
world.despawn(e);
|
|
1249
|
+
expect(liveCount(world)).toBe(0);
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
it('buffer<64>-only spawn->get->despawn never touches the pool', () => {
|
|
1253
|
+
const Blob = defineComponent('Blob64Pool', { b: 'buffer<64>' });
|
|
1254
|
+
const world = new World();
|
|
1255
|
+
expect(liveCount(world)).toBe(0);
|
|
1256
|
+
|
|
1257
|
+
const e = world.spawn({ component: Blob, data: { b: new Uint8Array(64) } }).unwrap();
|
|
1258
|
+
expect(liveCount(world)).toBe(0);
|
|
1259
|
+
|
|
1260
|
+
world.get(e, Blob).unwrap().b;
|
|
1261
|
+
expect(liveCount(world)).toBe(0);
|
|
1262
|
+
|
|
1263
|
+
world.despawn(e);
|
|
1264
|
+
expect(liveCount(world)).toBe(0);
|
|
1265
|
+
});
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
describe('AC-08 variable buffer (no N) path post-inline (w15)', () => {
|
|
1269
|
+
const liveCount = (world: World): number =>
|
|
1270
|
+
(world as unknown as { bufferPool: { _liveCount(): number } }).bufferPool._liveCount();
|
|
1271
|
+
|
|
1272
|
+
it('variable buffer spawn->set->get round-trip works', () => {
|
|
1273
|
+
const C = defineComponent('VarBuf', { blob: 'buffer' });
|
|
1274
|
+
const world = new World();
|
|
1275
|
+
expect(liveCount(world)).toBe(0);
|
|
1276
|
+
|
|
1277
|
+
const payload = Uint8Array.of(0xaa, 0xbb, 0xcc, 0xdd);
|
|
1278
|
+
const e = world.spawn({ component: C, data: { blob: payload } }).unwrap();
|
|
1279
|
+
expect(liveCount(world)).toBe(1);
|
|
1280
|
+
|
|
1281
|
+
const snap = world.get(e, C).unwrap().blob;
|
|
1282
|
+
expect(snap).toBeInstanceOf(Uint8Array);
|
|
1283
|
+
expect(snap.length).toBe(4);
|
|
1284
|
+
expect(snap[0]).toBe(0xaa);
|
|
1285
|
+
expect(snap[3]).toBe(0xdd);
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
it('variable buffer grows via set with larger payload', () => {
|
|
1289
|
+
const C = defineComponent('VarBuf2', { blob: 'buffer' });
|
|
1290
|
+
const world = new World();
|
|
1291
|
+
const small = Uint8Array.of(0x01, 0x02);
|
|
1292
|
+
const e = world.spawn({ component: C, data: { blob: small } }).unwrap();
|
|
1293
|
+
|
|
1294
|
+
expect(world.get(e, C).unwrap().blob.length).toBe(2);
|
|
1295
|
+
|
|
1296
|
+
const large = new Uint8Array(32);
|
|
1297
|
+
for (let i = 0; i < 32; i++) large[i] = i + 0x10;
|
|
1298
|
+
world.set(e, C, { blob: large }).unwrap();
|
|
1299
|
+
|
|
1300
|
+
const snap = world.get(e, C).unwrap().blob;
|
|
1301
|
+
expect(snap.length).toBe(32);
|
|
1302
|
+
for (let i = 0; i < 32; i++) expect(snap[i]).toBe(i + 0x10);
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
it('variable buffer release path: despawn returns slot to pool', () => {
|
|
1306
|
+
const C = defineComponent('VarBuf3', { blob: 'buffer' });
|
|
1307
|
+
const world = new World();
|
|
1308
|
+
expect(liveCount(world)).toBe(0);
|
|
1309
|
+
|
|
1310
|
+
const e = world.spawn({ component: C, data: { blob: Uint8Array.of(0xfe) } }).unwrap();
|
|
1311
|
+
expect(liveCount(world)).toBe(1);
|
|
1312
|
+
|
|
1313
|
+
world.despawn(e).unwrap();
|
|
1314
|
+
expect(liveCount(world)).toBe(0);
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
it('variable + fixed coexistence: pool count only reflects variable fields', () => {
|
|
1318
|
+
const VarArr = defineComponent('VarArr', { data: 'array<f32>' });
|
|
1319
|
+
const FixedMat = defineComponent('FixedMat', { m: 'array<f32, 16>' });
|
|
1320
|
+
const world = new World();
|
|
1321
|
+
expect(liveCount(world)).toBe(0);
|
|
1322
|
+
|
|
1323
|
+
const e1 = world.spawn({ component: FixedMat, data: { m: new Float32Array(16) } }).unwrap();
|
|
1324
|
+
expect(liveCount(world)).toBe(0);
|
|
1325
|
+
|
|
1326
|
+
const e2 = world
|
|
1327
|
+
.spawn(
|
|
1328
|
+
{ component: FixedMat, data: { m: new Float32Array(16) } },
|
|
1329
|
+
{ component: VarArr, data: { data: new Float32Array(0) } },
|
|
1330
|
+
)
|
|
1331
|
+
.unwrap();
|
|
1332
|
+
expect(liveCount(world)).toBe(1);
|
|
1333
|
+
|
|
1334
|
+
world.despawn(e2).unwrap();
|
|
1335
|
+
expect(liveCount(world)).toBe(0);
|
|
1336
|
+
|
|
1337
|
+
world.despawn(e1).unwrap();
|
|
1338
|
+
expect(liveCount(world)).toBe(0);
|
|
1339
|
+
});
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
{
|
|
1345
|
+
// ─── from query-preregister.test.ts ───
|
|
1346
|
+
describe('foldEssentials.test.ts', () => {
|
|
1347
|
+
describe('ESSENTIAL_COMPONENT_IDS SSOT', () => {
|
|
1348
|
+
it('is a frozen readonly array containing componentId(Entity)', async () => {
|
|
1349
|
+
const mod = await import('../entity');
|
|
1350
|
+
const ESSENTIAL_COMPONENT_IDS = (
|
|
1351
|
+
mod as unknown as { ESSENTIAL_COMPONENT_IDS: ReadonlyArray<number> }
|
|
1352
|
+
).ESSENTIAL_COMPONENT_IDS;
|
|
1353
|
+
expect(Array.isArray(ESSENTIAL_COMPONENT_IDS)).toBe(true);
|
|
1354
|
+
expect(ESSENTIAL_COMPONENT_IDS.length).toBe(1);
|
|
1355
|
+
expect(ESSENTIAL_COMPONENT_IDS[0]).toBe(componentId(Entity));
|
|
1356
|
+
expect(Object.isFrozen(ESSENTIAL_COMPONENT_IDS)).toBe(true);
|
|
1357
|
+
});
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
describe('foldEssentials() helper', () => {
|
|
1361
|
+
it('input containing componentId(Entity) returns deduped copy', async () => {
|
|
1362
|
+
const mod = await import('../entity');
|
|
1363
|
+
const foldEssentials = (
|
|
1364
|
+
mod as unknown as { foldEssentials: (ids: ReadonlyArray<number>) => number[] }
|
|
1365
|
+
).foldEssentials;
|
|
1366
|
+
const input = [componentId(Entity), 2, 5];
|
|
1367
|
+
const out = foldEssentials(input);
|
|
1368
|
+
// componentId(Entity) appears exactly once
|
|
1369
|
+
expect(out.filter((id) => id === componentId(Entity)).length).toBe(1);
|
|
1370
|
+
// No id is dropped
|
|
1371
|
+
expect(out).toContain(2);
|
|
1372
|
+
expect(out).toContain(5);
|
|
1373
|
+
expect(out).toContain(componentId(Entity));
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
it('input not containing componentId(Entity) returns array prefixed with componentId(Entity)', async () => {
|
|
1377
|
+
const mod = await import('../entity');
|
|
1378
|
+
const foldEssentials = (
|
|
1379
|
+
mod as unknown as { foldEssentials: (ids: ReadonlyArray<number>) => number[] }
|
|
1380
|
+
).foldEssentials;
|
|
1381
|
+
const input = [2, 5, 7];
|
|
1382
|
+
const out = foldEssentials(input);
|
|
1383
|
+
expect(out.length).toBe(4);
|
|
1384
|
+
expect(out[0]).toBe(componentId(Entity));
|
|
1385
|
+
expect(out).toContain(2);
|
|
1386
|
+
expect(out).toContain(5);
|
|
1387
|
+
expect(out).toContain(7);
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
it('empty input returns [componentId(Entity)]', async () => {
|
|
1391
|
+
const mod = await import('../entity');
|
|
1392
|
+
const foldEssentials = (
|
|
1393
|
+
mod as unknown as { foldEssentials: (ids: ReadonlyArray<number>) => number[] }
|
|
1394
|
+
).foldEssentials;
|
|
1395
|
+
const out = foldEssentials([]);
|
|
1396
|
+
expect(out).toEqual([componentId(Entity)]);
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
it('does not mutate input array', async () => {
|
|
1400
|
+
const mod = await import('../entity');
|
|
1401
|
+
const foldEssentials = (
|
|
1402
|
+
mod as unknown as { foldEssentials: (ids: ReadonlyArray<number>) => number[] }
|
|
1403
|
+
).foldEssentials;
|
|
1404
|
+
const input = [2, 5];
|
|
1405
|
+
const inputSnapshot = [...input];
|
|
1406
|
+
const out = foldEssentials(input);
|
|
1407
|
+
expect(input).toEqual(inputSnapshot);
|
|
1408
|
+
// returned array must be a NEW array (different reference) when fold
|
|
1409
|
+
// had to insert componentId(Entity)
|
|
1410
|
+
expect(out).not.toBe(input);
|
|
1411
|
+
});
|
|
1412
|
+
});
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
{
|
|
1417
|
+
// ─── from entity-handle-rename-grep-gate.test.ts (feat-20260611 M-1 / w2) ───
|
|
1418
|
+
//
|
|
1419
|
+
// Grep gate verifying the type-space `Entity` -> `EntityHandle` rename
|
|
1420
|
+
// landed cleanly across `packages/ecs/src/`. Reads the actual source files
|
|
1421
|
+
// at test time (no mocking) so it directly enforces the source SSOT.
|
|
1422
|
+
//
|
|
1423
|
+
// Verifies:
|
|
1424
|
+
// 1. `entity.ts` exports `EntityHandle` (the renamed branded-number type)
|
|
1425
|
+
// and no longer exports `type Entity`.
|
|
1426
|
+
// 2. `entity.ts` keeps the value-space `Entity` const (id=0
|
|
1427
|
+
// component token) but does NOT re-export `type Entity` (the type-space
|
|
1428
|
+
// handle alias is now `EntityHandle`).
|
|
1429
|
+
//
|
|
1430
|
+
// Constraints:
|
|
1431
|
+
// - requirements §Component naming: id=0 component token is still named
|
|
1432
|
+
// `Entity` -- only the type-space branded handle is renamed
|
|
1433
|
+
// - requirements I-1: type Entity -> EntityHandle
|
|
1434
|
+
// - research F-5: TS rename refactor not usable due to namespace merging,
|
|
1435
|
+
// hence a grep-form gate
|
|
1436
|
+
//
|
|
1437
|
+
// Cross-package `: EntityHandle` type-annotation acceptance is verified by
|
|
1438
|
+
// `pnpm typecheck` at the milestone CI sweep level, not here.
|
|
1439
|
+
|
|
1440
|
+
describe('entity-handle-rename-grep-gate.test.ts', () => {
|
|
1441
|
+
describe('packages/ecs/src/entity-handle.ts', () => {
|
|
1442
|
+
it('exports type EntityHandle (renamed branded-number handle)', async () => {
|
|
1443
|
+
const fs = await import('node:fs/promises');
|
|
1444
|
+
const path = await import('node:path');
|
|
1445
|
+
const url = await import('node:url');
|
|
1446
|
+
const here = url.fileURLToPath(new URL('.', import.meta.url));
|
|
1447
|
+
const src = await fs.readFile(path.resolve(here, '..', 'entity-handle.ts'), 'utf8');
|
|
1448
|
+
// exactly one `export type EntityHandle =` declaration
|
|
1449
|
+
const handleDeclMatches = src.match(/^export type EntityHandle\b/gm) ?? [];
|
|
1450
|
+
expect(handleDeclMatches.length).toBe(1);
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
it('no longer exports `type Entity` (type-space rename complete)', async () => {
|
|
1454
|
+
const fs = await import('node:fs/promises');
|
|
1455
|
+
const path = await import('node:path');
|
|
1456
|
+
const url = await import('node:url');
|
|
1457
|
+
const here = url.fileURLToPath(new URL('.', import.meta.url));
|
|
1458
|
+
const src = await fs.readFile(path.resolve(here, '..', 'entity-handle.ts'), 'utf8');
|
|
1459
|
+
// Match `export type Entity` not followed by `Handle` -- catches the
|
|
1460
|
+
// pre-rename declaration but not the post-rename `EntityHandle`.
|
|
1461
|
+
const entityTypeDeclMatches = src.match(/^export type Entity\b(?!Handle)/gm) ?? [];
|
|
1462
|
+
expect(entityTypeDeclMatches.length).toBe(0);
|
|
1463
|
+
});
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
describe('packages/ecs/src/entity.ts', () => {
|
|
1467
|
+
it('keeps value-space Entity (id=0 component token)', async () => {
|
|
1468
|
+
const fs = await import('node:fs/promises');
|
|
1469
|
+
const path = await import('node:path');
|
|
1470
|
+
const url = await import('node:url');
|
|
1471
|
+
const here = url.fileURLToPath(new URL('.', import.meta.url));
|
|
1472
|
+
const src = await fs.readFile(path.resolve(here, '..', 'entity.ts'), 'utf8');
|
|
1473
|
+
// The value-space token survives; Biome may wrap the call across lines.
|
|
1474
|
+
expect(/^export const Entity = defineComponent\(\s*['"]Entity['"]/m.test(src)).toBe(true);
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
it('does not re-export `type Entity` alias (type-space lives in entity-handle.ts as EntityHandle)', async () => {
|
|
1478
|
+
const fs = await import('node:fs/promises');
|
|
1479
|
+
const path = await import('node:path');
|
|
1480
|
+
const url = await import('node:url');
|
|
1481
|
+
const here = url.fileURLToPath(new URL('.', import.meta.url));
|
|
1482
|
+
const src = await fs.readFile(path.resolve(here, '..', 'entity.ts'), 'utf8');
|
|
1483
|
+
const entityTypeDeclMatches = src.match(/^export type Entity\b(?!Handle)/gm) ?? [];
|
|
1484
|
+
expect(entityTypeDeclMatches.length).toBe(0);
|
|
1485
|
+
});
|
|
1486
|
+
});
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
{
|
|
1491
|
+
// ─── from swap-pop-self-column-removeEdge-filter.test.ts (feat-20260611-ecs-storage-naming-ssot M-2 / w4) ───
|
|
1492
|
+
//
|
|
1493
|
+
// Verifies two changes that landed in M-2:
|
|
1494
|
+
// 1. removeEntity swap-pop reads entity index from id=0 `self` column
|
|
1495
|
+
// (columns.get(0)?.get('self')?.view[lastRow] & 0xffffff) instead of
|
|
1496
|
+
// arch.entities[lastRow] — equivalence proven by research F-3.
|
|
1497
|
+
// 2. getRemoveEdge uses src.components.filter(c => componentId(c) !== removedId) to
|
|
1498
|
+
// rebuild the component list after componentRegistry is deleted.
|
|
1499
|
+
//
|
|
1500
|
+
// TDD red mechanism (pre-w5):
|
|
1501
|
+
// (a) The test corrupted arch.entities[lastRow] so pre-w5 removeEntity
|
|
1502
|
+
// would return the wrong value. Post-w5, removeEntity reads the
|
|
1503
|
+
// self column directly.
|
|
1504
|
+
// (b) The test verified graph.componentRegistry existence then verified
|
|
1505
|
+
// getRemoveEdge still works with only src.components.filter.
|
|
1506
|
+
|
|
1507
|
+
describe('swap-pop-self-column-removeEdge-filter.test.ts', () => {
|
|
1508
|
+
let testArchId = 0;
|
|
1509
|
+
|
|
1510
|
+
describe('removeEntity swap-pop reads entity from id=0 self column', () => {
|
|
1511
|
+
it('2+ entities swap-pop: removed entity index comes from self column, not entities array', () => {
|
|
1512
|
+
const C = defineComponent('SwapSelfCol', { v: { type: 'i32' } });
|
|
1513
|
+
const arch = createArchetype([C], testArchId++);
|
|
1514
|
+
|
|
1515
|
+
// Populate 3 entities. appendEntity writes to the id=0 self column.
|
|
1516
|
+
appendEntity(arch, 0x42);
|
|
1517
|
+
appendEntity(arch, 0x77);
|
|
1518
|
+
appendEntity(arch, 0xff);
|
|
1519
|
+
|
|
1520
|
+
const selfCol = tableColumns(arch).get(componentId(Entity))?.get('self');
|
|
1521
|
+
expect(selfCol).toBeDefined();
|
|
1522
|
+
// Verify self column still has correct value
|
|
1523
|
+
// biome-ignore lint/style/noNonNullAssertion: guarded by expect above
|
|
1524
|
+
expect(selfCol!.view[2]! & 0xffffff).toBe(0xff);
|
|
1525
|
+
|
|
1526
|
+
const swapped = removeEntity(arch, 0);
|
|
1527
|
+
|
|
1528
|
+
// After w5: movedEntity comes from self column (lower 24 bits)
|
|
1529
|
+
if (swapped !== null) {
|
|
1530
|
+
expect(swapped.movedEntity).toBe(0xff);
|
|
1531
|
+
expect(swapped.newRow).toBe(0);
|
|
1532
|
+
} else {
|
|
1533
|
+
expect.unreachable('expected swap-pop for multi-entity archetype');
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
it('single entity last-row swap-pop returns null (row === lastRow, no entities read needed)', () => {
|
|
1538
|
+
const C = defineComponent('SwapSelfLastRow', { v: { type: 'f32' } });
|
|
1539
|
+
const arch = createArchetype([C], testArchId++);
|
|
1540
|
+
|
|
1541
|
+
appendEntity(arch, 0xab);
|
|
1542
|
+
|
|
1543
|
+
const swapped = removeEntity(arch, 0);
|
|
1544
|
+
expect(arch.size).toBe(0);
|
|
1545
|
+
expect(swapped).toBeNull();
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
it('swap-pop self column data correctly migrated (column-level swap)', () => {
|
|
1549
|
+
const C = defineComponent('SwapSelfColData', { v: { type: 'i32' } });
|
|
1550
|
+
const arch = createArchetype([C], testArchId++);
|
|
1551
|
+
|
|
1552
|
+
appendEntity(arch, 0x10);
|
|
1553
|
+
appendEntity(arch, 0x20);
|
|
1554
|
+
appendEntity(arch, 0x30);
|
|
1555
|
+
|
|
1556
|
+
const selfCol = tableColumns(arch).get(componentId(Entity))?.get('self');
|
|
1557
|
+
// biome-ignore lint/style/noNonNullAssertion: test setup
|
|
1558
|
+
const vCol = tableColumns(arch).get(componentId(C))!.get('v')!;
|
|
1559
|
+
vCol.view[0] = 100;
|
|
1560
|
+
vCol.view[1] = 200;
|
|
1561
|
+
vCol.view[2] = 300;
|
|
1562
|
+
|
|
1563
|
+
// Remove row 1. Last row (2) swaps into row 1.
|
|
1564
|
+
const swapped = removeEntity(arch, 1);
|
|
1565
|
+
expect(arch.size).toBe(2);
|
|
1566
|
+
|
|
1567
|
+
if (swapped === null) {
|
|
1568
|
+
expect.unreachable('expected swap-pop');
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// GREEN (after w5): movedEntity = 0x30 from self column
|
|
1572
|
+
// RED (before w5): movedEntity = 0xcafe from corrupted arch.entities[2]
|
|
1573
|
+
expect(swapped.movedEntity).toBe(0x30);
|
|
1574
|
+
expect(swapped.newRow).toBe(1);
|
|
1575
|
+
|
|
1576
|
+
// self column swap-integrity: row 1 should now hold last-row's self value
|
|
1577
|
+
// biome-ignore lint/style/noNonNullAssertion: guarded by expect above
|
|
1578
|
+
expect(selfCol!.view[1]! & 0xffffff).toBe(0x30);
|
|
1579
|
+
// user column data also swapped
|
|
1580
|
+
expect(vCol.view[1]).toBe(300);
|
|
1581
|
+
});
|
|
1582
|
+
});
|
|
1583
|
+
|
|
1584
|
+
describe('getRemoveEdge uses src.components.filter after componentRegistry delete', () => {
|
|
1585
|
+
it('getRemoveEdge rebuilds component list from src.components.filter (not registry)', () => {
|
|
1586
|
+
const graph = createArchetypeGraph();
|
|
1587
|
+
const A = defineComponent('RemoveEdgeFilter_A', { a: { type: 'f32' } });
|
|
1588
|
+
const B = defineComponent('RemoveEdgeFilter_B', { b: { type: 'f32' } });
|
|
1589
|
+
const C = defineComponent('RemoveEdgeFilter_C', { c: { type: 'f32' } });
|
|
1590
|
+
|
|
1591
|
+
const archABC = getOrCreateArchetype(
|
|
1592
|
+
graph,
|
|
1593
|
+
[componentId(A), componentId(B), componentId(C)],
|
|
1594
|
+
[A, B, C],
|
|
1595
|
+
);
|
|
1596
|
+
|
|
1597
|
+
// After w5: componentRegistry field deleted; getRemoveEdge derives
|
|
1598
|
+
// component list from src.components.filter(c => componentId(c) !== removedId).
|
|
1599
|
+
const archAC = getRemoveEdge(graph, archABC, componentId(B));
|
|
1600
|
+
|
|
1601
|
+
const expectedIds = [componentId(Entity), componentId(A), componentId(C)].sort(
|
|
1602
|
+
(a, b) => a - b,
|
|
1603
|
+
);
|
|
1604
|
+
const actualComponentIds = archAC.components
|
|
1605
|
+
.map((c) => componentId(c))
|
|
1606
|
+
.sort((a, b) => a - b);
|
|
1607
|
+
expect(actualComponentIds).toEqual(expectedIds);
|
|
1608
|
+
|
|
1609
|
+
// Verify B is NOT present in the target
|
|
1610
|
+
expect(archAC.components.some((c) => componentId(c) === componentId(B))).toBe(false);
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
it('getRemoveEdge of last non-essential component returns archetype with only Entity', () => {
|
|
1614
|
+
const graph = createArchetypeGraph();
|
|
1615
|
+
const A = defineComponent('RemoveEdgeLast_A', { a: { type: 'f32' } });
|
|
1616
|
+
|
|
1617
|
+
const archA = getOrCreateArchetype(graph, [componentId(A)], [A]);
|
|
1618
|
+
const archEmpty = getRemoveEdge(graph, archA, componentId(A));
|
|
1619
|
+
|
|
1620
|
+
expect(archEmpty.components.length).toBe(1);
|
|
1621
|
+
expect(archEmpty.components[0]?.name).toBe('Entity');
|
|
1622
|
+
expect(archEmpty.components.map((c) => componentId(c))).toEqual([componentId(Entity)]);
|
|
1623
|
+
});
|
|
1624
|
+
});
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
{
|
|
1629
|
+
// ─── from isManaged-column-4-predicate-equivalence.test.ts (feat-20260611-ecs-storage-naming-ssot M-3 / w6) ───
|
|
1630
|
+
//
|
|
1631
|
+
// Verifies:
|
|
1632
|
+
// 1. Every TYPE_METADATA row has an `isManaged` boolean column.
|
|
1633
|
+
// 2. The 4 `isXxxField` predicates produce the same boolean result as the
|
|
1634
|
+
// new TYPE_METADATA[].isXxx single-column lookups.
|
|
1635
|
+
// 3. `isManagedField` recognizes `'string'` and `'unique<T>'` (both isManaged).
|
|
1636
|
+
//
|
|
1637
|
+
// tweak-20260612-ecs-concept-compression dropped the redundant per-vocab
|
|
1638
|
+
// managed-ref predicate column (100% identical to `isManaged`); the
|
|
1639
|
+
// equivalence assertion below now reads `isManaged` directly.
|
|
1640
|
+
//
|
|
1641
|
+
// Plan-strategy D-3 (approach A): TYPE_METADATA gains an `isManaged` column;
|
|
1642
|
+
// 4 predicates are 1:1 derivations from TYPE_METADATA columns, zero OR.
|
|
1643
|
+
//
|
|
1644
|
+
// Plan-strategy D-4: `isManagedBufferField` semantic widening accepted —
|
|
1645
|
+
// `buffer<abc>` resolves to metaKey 'buffer', `isBuffer` returns true; the
|
|
1646
|
+
// upstream `isSchemaVocabKeyword` gate already rejects malformed keywords.
|
|
1647
|
+
//
|
|
1648
|
+
// TDD: these tests are RED before w7 (isManaged column does not yet exist on
|
|
1649
|
+
// TypeMetadataRow). They turn GREEN after w7 adds the column + rewrites
|
|
1650
|
+
// predicates.
|
|
1651
|
+
|
|
1652
|
+
describe('isManaged-column-4-predicate-equivalence.test.ts', () => {
|
|
1653
|
+
describe('isManaged column exists on every TYPE_METADATA row', () => {
|
|
1654
|
+
it('every TYPE_METADATA row has a boolean isManaged property', () => {
|
|
1655
|
+
for (const [key, row] of Object.entries(TYPE_METADATA)) {
|
|
1656
|
+
expect(
|
|
1657
|
+
typeof (row as unknown as Record<string, unknown>).isManaged,
|
|
1658
|
+
`TYPE_METADATA['${key}'].isManaged must be boolean`,
|
|
1659
|
+
).toBe('boolean');
|
|
1660
|
+
}
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
it('isManaged === true exactly for the managed-ref vocab + scalar rows ("string" + "ref" + "shared")', () => {
|
|
1664
|
+
// feat-20260614-ecs-shared-component-and-unique-rename M3 (D-3): the
|
|
1665
|
+
// independent `'shared'` TYPE_METADATA row joins the managed-ref tier
|
|
1666
|
+
// alongside `'string'` (UniqueRefStore-backed) and `'ref'` (the
|
|
1667
|
+
// post-M2 `'unique<T>'` family). Each meta key carries its own
|
|
1668
|
+
// release-semantics dispatch (M4 wires the sub-branches).
|
|
1669
|
+
for (const [key, row] of Object.entries(TYPE_METADATA)) {
|
|
1670
|
+
const im = (row as unknown as Record<string, unknown>).isManaged as boolean;
|
|
1671
|
+
const expected = key === 'string' || key === 'ref' || key === 'shared';
|
|
1672
|
+
expect(im, `TYPE_METADATA['${key}'].isManaged`).toBe(expected);
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1676
|
+
it('"string" row has isManaged === true', () => {
|
|
1677
|
+
const stringRow = TYPE_METADATA.string;
|
|
1678
|
+
if (stringRow === undefined) throw new Error('string row missing');
|
|
1679
|
+
const im = (stringRow as unknown as Record<string, unknown>).isManaged;
|
|
1680
|
+
expect(im).toBe(true);
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
it('"ref" row has isManaged === true (shared scalar + vocab row)', () => {
|
|
1684
|
+
const refRow = TYPE_METADATA.ref;
|
|
1685
|
+
if (refRow === undefined) throw new Error('ref row missing');
|
|
1686
|
+
const im = (refRow as unknown as Record<string, unknown>).isManaged;
|
|
1687
|
+
expect(im).toBe(true);
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
it('non-managed rows (entity, buffer, array, scalars) have isManaged === false', () => {
|
|
1691
|
+
const nonManagedKeys = [
|
|
1692
|
+
'entity',
|
|
1693
|
+
'buffer',
|
|
1694
|
+
'array',
|
|
1695
|
+
'f32',
|
|
1696
|
+
'f64',
|
|
1697
|
+
'i32',
|
|
1698
|
+
'u32',
|
|
1699
|
+
'i16',
|
|
1700
|
+
'u16',
|
|
1701
|
+
'i8',
|
|
1702
|
+
'u8',
|
|
1703
|
+
'bool',
|
|
1704
|
+
'enum',
|
|
1705
|
+
];
|
|
1706
|
+
for (const key of nonManagedKeys) {
|
|
1707
|
+
const row = TYPE_METADATA[key];
|
|
1708
|
+
if (row === undefined) throw new Error(`row ${key} missing`);
|
|
1709
|
+
const im = (row as unknown as Record<string, unknown>).isManaged as boolean;
|
|
1710
|
+
expect(im, `TYPE_METADATA['${key}'].isManaged`).toBe(false);
|
|
1711
|
+
}
|
|
1712
|
+
});
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
describe('4 predicate equivalence (old impl vs new TYPE_METADATA column)', () => {
|
|
1716
|
+
// Legal SchemaFieldType samples: all 11 legacy scalars (keywords) +
|
|
1717
|
+
// 6 vocab-family parametric representatives. Every sample is a
|
|
1718
|
+
// syntactically valid keyword accepted by `defineComponent`.
|
|
1719
|
+
//
|
|
1720
|
+
// Deliberately excluded: 'buffer<0>' (invalid N, /^[1-9]\d*$/ rejects
|
|
1721
|
+
// 0), 'buffer<abc>' (non-integer N), 'handle<>' (empty tag). These
|
|
1722
|
+
// are never accepted by `defineComponent` (gated via `isSchemaVocabKeyword`),
|
|
1723
|
+
// so they never flow through the runtime predicates. The D-4 semantic
|
|
1724
|
+
// widening only affects invalid inputs — a dead path.
|
|
1725
|
+
const SAMPLE_FIELD_TYPES: string[] = [
|
|
1726
|
+
// 11 legacy scalar keys
|
|
1727
|
+
'f32',
|
|
1728
|
+
'f64',
|
|
1729
|
+
'i32',
|
|
1730
|
+
'u32',
|
|
1731
|
+
'i16',
|
|
1732
|
+
'u16',
|
|
1733
|
+
'i8',
|
|
1734
|
+
'u8',
|
|
1735
|
+
'bool',
|
|
1736
|
+
'enum',
|
|
1737
|
+
'ref',
|
|
1738
|
+
// 6 vocab-family parametric reps
|
|
1739
|
+
'entity',
|
|
1740
|
+
'string',
|
|
1741
|
+
'buffer',
|
|
1742
|
+
'buffer<64>',
|
|
1743
|
+
'unique<MaterialAsset>',
|
|
1744
|
+
'shared<MaterialAsset>',
|
|
1745
|
+
'array<f32>',
|
|
1746
|
+
'array<f32, 16>',
|
|
1747
|
+
'array<entity>',
|
|
1748
|
+
'array<entity, 4>',
|
|
1749
|
+
'array<shared<MaterialAsset>>',
|
|
1750
|
+
'unknown-field-type',
|
|
1751
|
+
];
|
|
1752
|
+
|
|
1753
|
+
it('isManagedField: equivalence between current impl and TYPE_METADATA[].isManaged', () => {
|
|
1754
|
+
for (const t of SAMPLE_FIELD_TYPES) {
|
|
1755
|
+
const oldResult = isManagedField(t);
|
|
1756
|
+
const metaKey = fieldTypeToMetaKey(t) ?? '';
|
|
1757
|
+
const newResult =
|
|
1758
|
+
((TYPE_METADATA[metaKey] as unknown as Record<string, unknown>)
|
|
1759
|
+
?.isManaged as boolean) ?? false;
|
|
1760
|
+
expect(newResult, `isManagedField('${t}'): old=${oldResult} new=${newResult}`).toBe(
|
|
1761
|
+
oldResult,
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
});
|
|
1765
|
+
|
|
1766
|
+
it('isManagedBufferField: equivalence between current impl and TYPE_METADATA[].isBuffer', () => {
|
|
1767
|
+
for (const t of SAMPLE_FIELD_TYPES) {
|
|
1768
|
+
const oldResult = isManagedBufferField(t);
|
|
1769
|
+
const metaKey = fieldTypeToMetaKey(t) ?? '';
|
|
1770
|
+
const newResult = TYPE_METADATA[metaKey]?.isBuffer ?? false;
|
|
1771
|
+
expect(newResult, `isManagedBufferField('${t}'): old=${oldResult} new=${newResult}`).toBe(
|
|
1772
|
+
oldResult,
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
it('isEntityField: equivalence between current impl and TYPE_METADATA[].isEntityRef', () => {
|
|
1778
|
+
for (const t of SAMPLE_FIELD_TYPES) {
|
|
1779
|
+
const oldResult = isEntityField(t);
|
|
1780
|
+
const metaKey = fieldTypeToMetaKey(t) ?? '';
|
|
1781
|
+
const newResult = TYPE_METADATA[metaKey]?.isEntityRef ?? false;
|
|
1782
|
+
expect(newResult, `isEntityField('${t}'): old=${oldResult} new=${newResult}`).toBe(
|
|
1783
|
+
oldResult,
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
it('isManagedArrayField: equivalence between current impl and TYPE_METADATA[].isArray', () => {
|
|
1789
|
+
for (const t of SAMPLE_FIELD_TYPES) {
|
|
1790
|
+
const oldResult = isManagedArrayField(t);
|
|
1791
|
+
const metaKey = fieldTypeToMetaKey(t) ?? '';
|
|
1792
|
+
const newResult = TYPE_METADATA[metaKey]?.isArray ?? false;
|
|
1793
|
+
expect(newResult, `isManagedArrayField('${t}'): old=${oldResult} new=${newResult}`).toBe(
|
|
1794
|
+
oldResult,
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
});
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
describe('isManagedBufferField semantic widening (D-4 acceptance)', () => {
|
|
1801
|
+
it("'buffer<abc>' maps to metaKey 'buffer' so isBuffer returns true", () => {
|
|
1802
|
+
// D-4 states this widening is acceptable: defineComponent rejects
|
|
1803
|
+
// 'buffer<abc>' via isSchemaVocabKeyword (non-integer N), so it
|
|
1804
|
+
// never reaches the runtime predicate. The column-based predicate
|
|
1805
|
+
// unavoidably returns true via metaKey 'buffer'.isBuffer === true.
|
|
1806
|
+
const metaKey = fieldTypeToMetaKey('buffer<abc>') ?? '';
|
|
1807
|
+
expect(metaKey).toBe('buffer');
|
|
1808
|
+
expect(TYPE_METADATA[metaKey]?.isBuffer ?? false).toBe(true);
|
|
1809
|
+
// The new column-based predicate returns true for this dead path
|
|
1810
|
+
// (old regex impl returned false). Accepted per D-4.
|
|
1811
|
+
expect(isManagedBufferField('buffer<abc>')).toBe(true);
|
|
1812
|
+
});
|
|
1813
|
+
});
|
|
1814
|
+
|
|
1815
|
+
describe('isManagedField coverage: string and ref<T>', () => {
|
|
1816
|
+
it("returns true for 'string' literal", () => {
|
|
1817
|
+
expect(isManagedField('string')).toBe(true);
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
it("returns true for 'unique<Foo>' template", () => {
|
|
1821
|
+
expect(isManagedField('unique<Foo>')).toBe(true);
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1824
|
+
it("returns true for 'shared<X>' (rc-tracked managed scalar)", () => {
|
|
1825
|
+
expect(isManagedField('shared<X>')).toBe(true);
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
it("returns false for 'entity', 'buffer', 'array<T>', and bare scalars", () => {
|
|
1829
|
+
expect(isManagedField('entity')).toBe(false);
|
|
1830
|
+
expect(isManagedField('buffer')).toBe(false);
|
|
1831
|
+
expect(isManagedField('buffer<64>')).toBe(false);
|
|
1832
|
+
expect(isManagedField('array<f32>')).toBe(false);
|
|
1833
|
+
expect(isManagedField('f32')).toBe(false);
|
|
1834
|
+
});
|
|
1835
|
+
});
|
|
1836
|
+
});
|
|
1837
|
+
}
|