@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,98 @@
|
|
|
1
|
+
// feat-20260614-ecs-shared-component-and-unique-rename M6 w52 (AC-31, R-14):
|
|
2
|
+
// write-barrier slot-range short-circuit.
|
|
3
|
+
//
|
|
4
|
+
// D-15: builtin asset handles (slot < BUILTIN_BASE, e.g. HANDLE_CUBE=1) are
|
|
5
|
+
// process-static and NEVER reference-counted. The ECS write barrier for
|
|
6
|
+
// `shared<T>` columns must short-circuit on builtin slots: retain/release on a
|
|
7
|
+
// builtin handle makes ZERO calls to World.sharedRefs and never surfaces a
|
|
8
|
+
// SharedRefReleasedError / BuiltinSlotNotOwnedError. R-14 requires the
|
|
9
|
+
// short-circuit cover BOTH the scalar `shared<T>` arm AND the
|
|
10
|
+
// `array<shared<T>>` element dispatch (single helper SSOT).
|
|
11
|
+
//
|
|
12
|
+
// User-tier handles (slot >= BUILTIN_BASE, minted by allocSharedRef) keep
|
|
13
|
+
// flowing through World.sharedRefs as before.
|
|
14
|
+
|
|
15
|
+
import { BUILTIN_BASE, toShared } from '@forgeax/engine-types';
|
|
16
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
17
|
+
import { type Component, defineComponent } from '../component';
|
|
18
|
+
import { World } from '../world';
|
|
19
|
+
|
|
20
|
+
// HANDLE_CUBE's runtime slot (1) — a builtin-tier handle constructed without
|
|
21
|
+
// importing @forgeax/engine-runtime (ecs must not depend on runtime).
|
|
22
|
+
const BUILTIN_HANDLE = toShared<'MeshAsset'>(1);
|
|
23
|
+
|
|
24
|
+
describe('M6 w52: write-barrier builtin-slot short-circuit (AC-31 / R-14)', () => {
|
|
25
|
+
it('scalar shared<T> field = builtin handle: spawn retains 0 calls + no error', () => {
|
|
26
|
+
const Holder = defineComponent('W52ScalarHolder', { asset: { type: 'shared<MeshAsset>' } });
|
|
27
|
+
const w = new World();
|
|
28
|
+
const retainSpy = vi.spyOn(w.sharedRefs, 'retain');
|
|
29
|
+
const releaseSpy = vi.spyOn(w.sharedRefs, 'release');
|
|
30
|
+
|
|
31
|
+
const e = w
|
|
32
|
+
.spawn({
|
|
33
|
+
component: Holder as unknown as Component,
|
|
34
|
+
data: { asset: BUILTIN_HANDLE } as never,
|
|
35
|
+
})
|
|
36
|
+
.unwrap();
|
|
37
|
+
// spawn write-barrier must short-circuit on the builtin slot.
|
|
38
|
+
expect(retainSpy).not.toHaveBeenCalled();
|
|
39
|
+
|
|
40
|
+
w.despawn(e).unwrap();
|
|
41
|
+
// despawn write-barrier must short-circuit on the builtin slot.
|
|
42
|
+
expect(releaseSpy).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('array<shared<T>> field = [builtin handle]: retain/release 0 calls, no SharedRefReleasedError (R-14)', () => {
|
|
46
|
+
const Holder = defineComponent('W52ArrayHolder', {
|
|
47
|
+
assets: { type: 'array<shared<MeshAsset>>' },
|
|
48
|
+
});
|
|
49
|
+
const w = new World();
|
|
50
|
+
const retainSpy = vi.spyOn(w.sharedRefs, 'retain');
|
|
51
|
+
const releaseSpy = vi.spyOn(w.sharedRefs, 'release');
|
|
52
|
+
|
|
53
|
+
const e = w
|
|
54
|
+
.spawn({
|
|
55
|
+
component: Holder as unknown as Component,
|
|
56
|
+
data: { assets: [BUILTIN_HANDLE, BUILTIN_HANDLE] } as never,
|
|
57
|
+
})
|
|
58
|
+
.unwrap();
|
|
59
|
+
expect(retainSpy).not.toHaveBeenCalled();
|
|
60
|
+
|
|
61
|
+
expect(() => w.despawn(e).unwrap()).not.toThrow();
|
|
62
|
+
expect(releaseSpy).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('user-tier scalar handle still flows through World.sharedRefs (retain on spawn)', () => {
|
|
66
|
+
const Holder = defineComponent('W52UserScalarHolder', {
|
|
67
|
+
asset: { type: 'shared<MeshAsset>' },
|
|
68
|
+
});
|
|
69
|
+
const w = new World();
|
|
70
|
+
const userHandle = w.allocSharedRef('MeshAsset', { kind: 'mesh' });
|
|
71
|
+
expect(userHandle).toBeGreaterThanOrEqual(BUILTIN_BASE);
|
|
72
|
+
const retainSpy = vi.spyOn(w.sharedRefs, 'retain');
|
|
73
|
+
|
|
74
|
+
w.spawn({
|
|
75
|
+
component: Holder as unknown as Component,
|
|
76
|
+
data: { asset: userHandle } as never,
|
|
77
|
+
}).unwrap();
|
|
78
|
+
// user-tier handle is retained by the write barrier (rc 1 -> 2).
|
|
79
|
+
expect(retainSpy).toHaveBeenCalled();
|
|
80
|
+
expect(w.sharedRefs.refcount(userHandle)).toBe(2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('user-tier array element still flows through World.sharedRefs (R-14 control)', () => {
|
|
84
|
+
const Holder = defineComponent('W52UserArrayHolder', {
|
|
85
|
+
assets: { type: 'array<shared<MeshAsset>>' },
|
|
86
|
+
});
|
|
87
|
+
const w = new World();
|
|
88
|
+
const userHandle = w.allocSharedRef('MeshAsset', { kind: 'mesh' });
|
|
89
|
+
const retainSpy = vi.spyOn(w.sharedRefs, 'retain');
|
|
90
|
+
|
|
91
|
+
w.spawn({
|
|
92
|
+
component: Holder as unknown as Component,
|
|
93
|
+
data: { assets: [userHandle] } as never,
|
|
94
|
+
}).unwrap();
|
|
95
|
+
expect(retainSpy).toHaveBeenCalled();
|
|
96
|
+
expect(w.sharedRefs.refcount(userHandle)).toBe(2);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { err } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { SystemFailedError } from '../errors';
|
|
4
|
+
import { FixedUpdate, Update, World } from '../index';
|
|
5
|
+
|
|
6
|
+
describe('M4 world scheduling failure boundary', () => {
|
|
7
|
+
it('stops the frame and poisons the World when a system throws', () => {
|
|
8
|
+
const world = new World();
|
|
9
|
+
world.addSystem(Update, {
|
|
10
|
+
name: 'thrower',
|
|
11
|
+
queries: [],
|
|
12
|
+
fn: () => {
|
|
13
|
+
throw new Error('system');
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const result = world.update(1 / 60);
|
|
18
|
+
expect(result.ok).toBe(false);
|
|
19
|
+
if (!result.ok) expect(result.error).toBeInstanceOf(SystemFailedError);
|
|
20
|
+
expect(world.execution.health).toBe('poisoned');
|
|
21
|
+
expect(world.update(1 / 60).ok).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('poisons a frame when a system returns an expected command failure', () => {
|
|
25
|
+
const world = new World();
|
|
26
|
+
const failure = new Error('command failure');
|
|
27
|
+
world.addSystem(Update, { name: 'result-failure', queries: [], fn: () => err(failure) });
|
|
28
|
+
|
|
29
|
+
const result = world.update();
|
|
30
|
+
expect(result.ok).toBe(false);
|
|
31
|
+
if (!result.ok) {
|
|
32
|
+
expect(result.error).toBeInstanceOf(SystemFailedError);
|
|
33
|
+
expect((result.error as SystemFailedError).detail.cause).toBe(failure);
|
|
34
|
+
}
|
|
35
|
+
expect(world.execution.health).toBe('poisoned');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('advances the clock only for a successful frame and keeps fixed order stable', () => {
|
|
39
|
+
const world = new World({ time: { fixedDeltaSeconds: 0.01, maxStepsPerUpdate: 4 } });
|
|
40
|
+
const order: string[] = [];
|
|
41
|
+
world.addSystem(Update, { name: 'update', queries: [], fn: () => order.push('update') });
|
|
42
|
+
world.addSystem(FixedUpdate, {
|
|
43
|
+
name: 'fixed',
|
|
44
|
+
queries: [],
|
|
45
|
+
fn: () => order.push('fixed'),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(world.update(0.02).ok).toBe(true);
|
|
49
|
+
expect(order).toEqual(['update', 'fixed', 'fixed']);
|
|
50
|
+
expect(world.getResource<{ elapsed: number }>('Time').elapsed).toBeCloseTo(0.02);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// feat-20260614-ecs-managed-lifecycle-ssot M2 t-w3: world-level round-trip
|
|
2
|
+
// net-zero matrix for managed-resource fields.
|
|
3
|
+
//
|
|
4
|
+
// Verifies AC-12 (requirements §5 / §8 boundary table): every release-dispatch
|
|
5
|
+
// path on World must net-zero the BufferPool and UniqueRefStore live counters
|
|
6
|
+
// after a spawn -> mutate -> tear-down cycle, for each managed-vocab keyword.
|
|
7
|
+
//
|
|
8
|
+
// Matrix: 4 vocab x 2 round shapes = 8 cases.
|
|
9
|
+
//
|
|
10
|
+
// Vocab dimensions (keyword family routed through releaseManagedFieldOnRow
|
|
11
|
+
// after w4):
|
|
12
|
+
// - 'string' -> UniqueRefStore handle (isManagedField arm)
|
|
13
|
+
// - 'unique<T>' -> UniqueRefStore handle (isManagedField arm)
|
|
14
|
+
// - 'buffer' variable -> BufferPool slot id (isManagedBufferField arm)
|
|
15
|
+
// - 'array<T>' variable-> BufferPool slot id (isManagedArrayField arm)
|
|
16
|
+
//
|
|
17
|
+
// Round shapes:
|
|
18
|
+
// (R1) spawn -> world.set(e, C, { f: v2 }) -> world.despawn(e)
|
|
19
|
+
// (R2) spawn -> world.removeComponent(e, C) -> world.addComponent(e, C, ...)
|
|
20
|
+
//
|
|
21
|
+
// R2 uses addComponent (not a second spawn) to stress the same release-dispatch
|
|
22
|
+
// path that releaseManagedRefsOnRow walks (research Finding 1.2 row 4 / plan-
|
|
23
|
+
// strategy §5.3 M2 key test).
|
|
24
|
+
//
|
|
25
|
+
// OOS-1: this test only exercises increment release on per-entity despawn /
|
|
26
|
+
// per-component remove; it does NOT call any World.clear/dispose path. OOS-2:
|
|
27
|
+
// SceneInstance state alloc/release at world.ts:3494/3522 are out of scope
|
|
28
|
+
// for the helper (D-5) and not exercised here.
|
|
29
|
+
//
|
|
30
|
+
// TDD positioning: the round-trip invariant is already in place pre-w4 because
|
|
31
|
+
// the dispatch is correct, only duplicated; w3 locks in the invariant before
|
|
32
|
+
// the SSOT collapse so w4/w5 reviewers see it stays green through the refactor.
|
|
33
|
+
|
|
34
|
+
import type { Handle } from '@forgeax/engine-types';
|
|
35
|
+
import { describe, expect, it } from 'vitest';
|
|
36
|
+
import type { BufferPool } from '../buffer-pool';
|
|
37
|
+
import { type Component, defineComponent } from '../component';
|
|
38
|
+
import type { SharedRefStore } from '../shared-ref-store';
|
|
39
|
+
import type { UniqueRefStore } from '../unique-ref-store';
|
|
40
|
+
import { World } from '../world';
|
|
41
|
+
|
|
42
|
+
interface MaterialAsset {
|
|
43
|
+
albedo: [number, number, number, number];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface WorldInternals {
|
|
47
|
+
uniqueRefs: UniqueRefStore;
|
|
48
|
+
sharedRefs: SharedRefStore;
|
|
49
|
+
bufferPool: BufferPool;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function refsOf(w: World): UniqueRefStore {
|
|
53
|
+
return (w as unknown as WorldInternals).uniqueRefs;
|
|
54
|
+
}
|
|
55
|
+
function poolOf(w: World): BufferPool {
|
|
56
|
+
return (w as unknown as WorldInternals).bufferPool;
|
|
57
|
+
}
|
|
58
|
+
function liveRefs(w: World): number {
|
|
59
|
+
return refsOf(w)._liveCount();
|
|
60
|
+
}
|
|
61
|
+
function liveSlots(w: World): number {
|
|
62
|
+
return poolOf(w)._liveCount();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Allocates a fresh ref<MaterialAsset> handle inside the same World so the
|
|
66
|
+
// round-trip ledger stays self-contained: every alloc must net-zero too.
|
|
67
|
+
function allocMat(
|
|
68
|
+
w: World,
|
|
69
|
+
albedo: [number, number, number, number],
|
|
70
|
+
): Handle<'MaterialAsset', 'unique'> {
|
|
71
|
+
return refsOf(w).alloc<'MaterialAsset', MaterialAsset>('MaterialAsset', { albedo });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface RoundCase {
|
|
75
|
+
vocab: 'string' | 'unique<T>' | 'buffer' | 'array<T>';
|
|
76
|
+
Component: Component;
|
|
77
|
+
v1: () => unknown;
|
|
78
|
+
v2: () => unknown;
|
|
79
|
+
v3: () => unknown;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Build the 4 vocab cases. Each defines a fresh component with one managed
|
|
83
|
+
// field of that vocab keyword; the value generators v1/v2/v3 produce
|
|
84
|
+
// distinct payloads so the set / addComponent paths actually mutate.
|
|
85
|
+
function buildCases(w: World): RoundCase[] {
|
|
86
|
+
const StrName = defineComponent('RtStrName', { value: { type: 'string' } });
|
|
87
|
+
const Mat = defineComponent('RtMat', { material: { type: 'unique<MaterialAsset>' } });
|
|
88
|
+
const Buf = defineComponent('RtBuf', { bytes: { type: 'buffer' } });
|
|
89
|
+
const Arr = defineComponent('RtArr', { ids: { type: 'array<u32>' } });
|
|
90
|
+
|
|
91
|
+
return [
|
|
92
|
+
{
|
|
93
|
+
vocab: 'string',
|
|
94
|
+
Component: StrName as unknown as Component,
|
|
95
|
+
v1: () => 'alpha',
|
|
96
|
+
v2: () => 'beta',
|
|
97
|
+
v3: () => 'gamma',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
vocab: 'unique<T>',
|
|
101
|
+
Component: Mat as unknown as Component,
|
|
102
|
+
// The ref<T> vocab requires alloc'ing a handle into the World's
|
|
103
|
+
// UniqueRefStore. The generators alloc inside the round (after the
|
|
104
|
+
// counter snapshot), so the closing despawn / set / removeComponent
|
|
105
|
+
// path must release them back to net-zero.
|
|
106
|
+
v1: () => allocMat(w, [1, 0, 0, 1]),
|
|
107
|
+
v2: () => allocMat(w, [0, 1, 0, 1]),
|
|
108
|
+
v3: () => allocMat(w, [0, 0, 1, 1]),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
vocab: 'buffer',
|
|
112
|
+
Component: Buf as unknown as Component,
|
|
113
|
+
v1: () => new Uint8Array([1, 2, 3, 4]),
|
|
114
|
+
v2: () => new Uint8Array([5, 6, 7, 8, 9]),
|
|
115
|
+
v3: () => new Uint8Array([10]),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
vocab: 'array<T>',
|
|
119
|
+
Component: Arr as unknown as Component,
|
|
120
|
+
v1: () => new Uint32Array([1, 2, 3]),
|
|
121
|
+
v2: () => new Uint32Array([4, 5, 6, 7]),
|
|
122
|
+
v3: () => new Uint32Array([8]),
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
describe('world managed round-trip net-zero matrix (feat-20260614 M2 / AC-12)', () => {
|
|
128
|
+
describe('R1: spawn -> set -> despawn', () => {
|
|
129
|
+
for (const vocab of ['string', 'unique<T>', 'buffer', 'array<T>'] as const) {
|
|
130
|
+
it(`${vocab}: live counters net-zero after spawn -> set -> despawn`, () => {
|
|
131
|
+
const w = new World();
|
|
132
|
+
const cases = buildCases(w);
|
|
133
|
+
const c = cases.find((cs) => cs.vocab === vocab);
|
|
134
|
+
if (!c) throw new Error('case missing');
|
|
135
|
+
|
|
136
|
+
// Snapshot counters BEFORE any value generation. ref<T>'s value
|
|
137
|
+
// generator alloc's into the World's UniqueRefStore inside the
|
|
138
|
+
// round, so the round's full release-dispatch must net-zero those
|
|
139
|
+
// back to 0 by the end of despawn.
|
|
140
|
+
const refsBefore = liveRefs(w);
|
|
141
|
+
const slotsBefore = liveSlots(w);
|
|
142
|
+
|
|
143
|
+
const e = w
|
|
144
|
+
.spawn({ component: c.Component, data: { [fieldOf(c)]: c.v1() } as never })
|
|
145
|
+
.unwrap();
|
|
146
|
+
w.set(e, c.Component, { [fieldOf(c)]: c.v2() } as never).unwrap();
|
|
147
|
+
w.despawn(e).unwrap();
|
|
148
|
+
|
|
149
|
+
expect(liveRefs(w)).toBe(refsBefore);
|
|
150
|
+
expect(liveSlots(w)).toBe(slotsBefore);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('R2: spawn -> removeComponent -> addComponent', () => {
|
|
156
|
+
for (const vocab of ['string', 'unique<T>', 'buffer', 'array<T>'] as const) {
|
|
157
|
+
it(`${vocab}: live counters net-zero after spawn -> removeComponent -> addComponent`, () => {
|
|
158
|
+
const w = new World();
|
|
159
|
+
const cases = buildCases(w);
|
|
160
|
+
const c = cases.find((cs) => cs.vocab === vocab);
|
|
161
|
+
if (!c) throw new Error('case missing');
|
|
162
|
+
|
|
163
|
+
// Snapshot before any v1/v3 alloc so ref<T>'s allocator hits land
|
|
164
|
+
// inside the round; the closing despawn must release both the
|
|
165
|
+
// v1-released-by-removeComponent and the v3-installed-by-addComponent
|
|
166
|
+
// payloads to net-zero the counters.
|
|
167
|
+
const refsBefore = liveRefs(w);
|
|
168
|
+
const slotsBefore = liveSlots(w);
|
|
169
|
+
|
|
170
|
+
const e = w
|
|
171
|
+
.spawn({ component: c.Component, data: { [fieldOf(c)]: c.v1() } as never })
|
|
172
|
+
.unwrap();
|
|
173
|
+
w.removeComponent(e, c.Component).unwrap();
|
|
174
|
+
w.addComponent(e, {
|
|
175
|
+
component: c.Component,
|
|
176
|
+
data: { [fieldOf(c)]: c.v3() } as never,
|
|
177
|
+
} as never).unwrap();
|
|
178
|
+
w.despawn(e).unwrap();
|
|
179
|
+
|
|
180
|
+
expect(liveRefs(w)).toBe(refsBefore);
|
|
181
|
+
expect(liveSlots(w)).toBe(slotsBefore);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
function fieldOf(c: RoundCase): string {
|
|
188
|
+
switch (c.vocab) {
|
|
189
|
+
case 'string':
|
|
190
|
+
return 'value';
|
|
191
|
+
case 'unique<T>':
|
|
192
|
+
return 'material';
|
|
193
|
+
case 'buffer':
|
|
194
|
+
return 'bytes';
|
|
195
|
+
case 'array<T>':
|
|
196
|
+
return 'ids';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
201
|
+
// M4 / w11 — array<shared<T>> element-level retain/release coverage
|
|
202
|
+
//
|
|
203
|
+
// Covers AC-09 (array<shared<T>> independent meta key + element-level
|
|
204
|
+
// retain/release) + AC-11 (archetype migration net-zero).
|
|
205
|
+
//
|
|
206
|
+
// `array<shared<T>>` differs from `array<handle<T>>` in that the ECS owns
|
|
207
|
+
// retain/release on each element; the holder writes the array, and the
|
|
208
|
+
// SharedRefStore refcount is the SSOT for liveness. Five scenarios cover
|
|
209
|
+
// the full element-level lifecycle:
|
|
210
|
+
// (S1) set elements → SharedRefStore.retain called once per element
|
|
211
|
+
// (S2) overwrite element set → prior elements release, new elements retain
|
|
212
|
+
// (S3) clear (set to empty array) → all current elements release
|
|
213
|
+
// (S4) archetype migration (add component triggers row move) → net-zero
|
|
214
|
+
// (S5) spawn → despawn → all array<shared<T>> elements release back to producer rc=1
|
|
215
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
interface SharedAsset {
|
|
218
|
+
tag: string;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function sharedRefsOf(w: World): SharedRefStore {
|
|
222
|
+
return (w as unknown as WorldInternals).sharedRefs;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function allocSharedAsset(w: World, tag: string): Handle<'SharedAsset', 'shared'> {
|
|
226
|
+
return w.allocSharedRef<'SharedAsset', SharedAsset>('SharedAsset', { tag });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
describe('array<shared<T>> element-level retain/release (feat-20260614 M4 / AC-09 / AC-11)', () => {
|
|
230
|
+
it('S1: set elements → each element retained (rc=2 = producer alloc-grant + ECS retain)', () => {
|
|
231
|
+
const w = new World();
|
|
232
|
+
const Holder = defineComponent('S1Holder', { assets: { type: 'array<shared<SharedAsset>>' } });
|
|
233
|
+
|
|
234
|
+
const h1 = allocSharedAsset(w, 'a');
|
|
235
|
+
const h2 = allocSharedAsset(w, 'b');
|
|
236
|
+
expect(sharedRefsOf(w).refcount(h1)).toBe(1);
|
|
237
|
+
expect(sharedRefsOf(w).refcount(h2)).toBe(1);
|
|
238
|
+
|
|
239
|
+
w.spawn({
|
|
240
|
+
component: Holder as unknown as Component,
|
|
241
|
+
data: { assets: [h1, h2] } as never,
|
|
242
|
+
}).unwrap();
|
|
243
|
+
|
|
244
|
+
// Each array element is retained once by the ECS write barrier.
|
|
245
|
+
expect(sharedRefsOf(w).refcount(h1)).toBe(2);
|
|
246
|
+
expect(sharedRefsOf(w).refcount(h2)).toBe(2);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('S2: overwrite array → prior elements released, new elements retained (net-zero rc on shared elements)', () => {
|
|
250
|
+
const w = new World();
|
|
251
|
+
const Holder = defineComponent('S2Holder', { assets: { type: 'array<shared<SharedAsset>>' } });
|
|
252
|
+
|
|
253
|
+
const h1 = allocSharedAsset(w, 'a');
|
|
254
|
+
const h2 = allocSharedAsset(w, 'b');
|
|
255
|
+
const h3 = allocSharedAsset(w, 'c');
|
|
256
|
+
|
|
257
|
+
const e = w
|
|
258
|
+
.spawn({ component: Holder as unknown as Component, data: { assets: [h1, h2] } as never })
|
|
259
|
+
.unwrap();
|
|
260
|
+
|
|
261
|
+
// After spawn: h1.rc=2, h2.rc=2, h3.rc=1.
|
|
262
|
+
expect(sharedRefsOf(w).refcount(h1)).toBe(2);
|
|
263
|
+
expect(sharedRefsOf(w).refcount(h2)).toBe(2);
|
|
264
|
+
expect(sharedRefsOf(w).refcount(h3)).toBe(1);
|
|
265
|
+
|
|
266
|
+
// Overwrite to [h3]. h1+h2 lose their ECS-retain, h3 gains it.
|
|
267
|
+
w.set(e, Holder as unknown as Component, { assets: [h3] } as never).unwrap();
|
|
268
|
+
|
|
269
|
+
expect(sharedRefsOf(w).refcount(h1)).toBe(1);
|
|
270
|
+
expect(sharedRefsOf(w).refcount(h2)).toBe(1);
|
|
271
|
+
expect(sharedRefsOf(w).refcount(h3)).toBe(2);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('S3: clear (set to empty array) → all current elements released', () => {
|
|
275
|
+
const w = new World();
|
|
276
|
+
const Holder = defineComponent('S3Holder', { assets: { type: 'array<shared<SharedAsset>>' } });
|
|
277
|
+
|
|
278
|
+
const h1 = allocSharedAsset(w, 'a');
|
|
279
|
+
const h2 = allocSharedAsset(w, 'b');
|
|
280
|
+
|
|
281
|
+
const e = w
|
|
282
|
+
.spawn({ component: Holder as unknown as Component, data: { assets: [h1, h2] } as never })
|
|
283
|
+
.unwrap();
|
|
284
|
+
|
|
285
|
+
expect(sharedRefsOf(w).refcount(h1)).toBe(2);
|
|
286
|
+
expect(sharedRefsOf(w).refcount(h2)).toBe(2);
|
|
287
|
+
|
|
288
|
+
w.set(e, Holder as unknown as Component, { assets: [] } as never).unwrap();
|
|
289
|
+
|
|
290
|
+
// ECS-retain released on both; producer alloc-grant remains.
|
|
291
|
+
expect(sharedRefsOf(w).refcount(h1)).toBe(1);
|
|
292
|
+
expect(sharedRefsOf(w).refcount(h2)).toBe(1);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('S4: archetype migration (addComponent triggers row move) → element rc net-zero across migration', () => {
|
|
296
|
+
const w = new World();
|
|
297
|
+
const Holder = defineComponent('S4Holder', { assets: { type: 'array<shared<SharedAsset>>' } });
|
|
298
|
+
const Tag = defineComponent('S4Tag', { v: { type: 'u32' } });
|
|
299
|
+
|
|
300
|
+
const h1 = allocSharedAsset(w, 'a');
|
|
301
|
+
const h2 = allocSharedAsset(w, 'b');
|
|
302
|
+
|
|
303
|
+
const e = w
|
|
304
|
+
.spawn({ component: Holder as unknown as Component, data: { assets: [h1, h2] } as never })
|
|
305
|
+
.unwrap();
|
|
306
|
+
|
|
307
|
+
const before1 = sharedRefsOf(w).refcount(h1);
|
|
308
|
+
const before2 = sharedRefsOf(w).refcount(h2);
|
|
309
|
+
expect(before1).toBe(2);
|
|
310
|
+
expect(before2).toBe(2);
|
|
311
|
+
|
|
312
|
+
// addComponent triggers archetype migration (row swap-pop into a new
|
|
313
|
+
// archetype). The shared-array column bytes move with the row; the
|
|
314
|
+
// element refcount must NOT shift — this is the net-zero invariant
|
|
315
|
+
// (AC-11). Migration copies the slot id, no new retain/release.
|
|
316
|
+
w.addComponent(e, {
|
|
317
|
+
component: Tag as unknown as Component,
|
|
318
|
+
data: { v: 7 } as never,
|
|
319
|
+
} as never).unwrap();
|
|
320
|
+
|
|
321
|
+
expect(sharedRefsOf(w).refcount(h1)).toBe(before1);
|
|
322
|
+
expect(sharedRefsOf(w).refcount(h2)).toBe(before2);
|
|
323
|
+
|
|
324
|
+
// removeComponent migrates back; still net-zero.
|
|
325
|
+
w.removeComponent(e, Tag as unknown as Component).unwrap();
|
|
326
|
+
expect(sharedRefsOf(w).refcount(h1)).toBe(before1);
|
|
327
|
+
expect(sharedRefsOf(w).refcount(h2)).toBe(before2);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('S5: spawn → despawn round-trip → all array<shared<T>> elements release back to producer rc=1', () => {
|
|
331
|
+
const w = new World();
|
|
332
|
+
const Holder = defineComponent('S5Holder', { assets: { type: 'array<shared<SharedAsset>>' } });
|
|
333
|
+
|
|
334
|
+
const h1 = allocSharedAsset(w, 'a');
|
|
335
|
+
const h2 = allocSharedAsset(w, 'b');
|
|
336
|
+
const h3 = allocSharedAsset(w, 'c');
|
|
337
|
+
|
|
338
|
+
const e = w
|
|
339
|
+
.spawn({
|
|
340
|
+
component: Holder as unknown as Component,
|
|
341
|
+
data: { assets: [h1, h2, h3] } as never,
|
|
342
|
+
})
|
|
343
|
+
.unwrap();
|
|
344
|
+
|
|
345
|
+
expect(sharedRefsOf(w).refcount(h1)).toBe(2);
|
|
346
|
+
expect(sharedRefsOf(w).refcount(h2)).toBe(2);
|
|
347
|
+
expect(sharedRefsOf(w).refcount(h3)).toBe(2);
|
|
348
|
+
|
|
349
|
+
w.despawn(e).unwrap();
|
|
350
|
+
|
|
351
|
+
// Despawn releases the ECS-retain on every element; producer alloc-grant
|
|
352
|
+
// (rc=1) survives so consumers (e.g. AssetRegistry) keep ownership.
|
|
353
|
+
expect(sharedRefsOf(w).refcount(h1)).toBe(1);
|
|
354
|
+
expect(sharedRefsOf(w).refcount(h2)).toBe(1);
|
|
355
|
+
expect(sharedRefsOf(w).refcount(h3)).toBe(1);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// feat-20260517-spawn-default-fallback / M2 / t6.
|
|
2
|
+
//
|
|
3
|
+
// Compile-time type assertions for the `ComponentData<S>['data']` surface
|
|
4
|
+
// after the M2 widening + the spawn mapped-tuple primary-inference probe.
|
|
5
|
+
//
|
|
6
|
+
// Three pinned invariants (Red until t9 lands):
|
|
7
|
+
//
|
|
8
|
+
// (a) ComponentData<S>['data'] === Partial<ShapeOf<S>> — AC-01
|
|
9
|
+
// The shared field bridge between `world.spawn` / `world.addComponent`
|
|
10
|
+
// / overrides; widening from ShapeOf<S> to Partial<ShapeOf<S>> is the
|
|
11
|
+
// single physical edit that takes the whole route off the ts(2739)
|
|
12
|
+
// cliff (AC-02).
|
|
13
|
+
//
|
|
14
|
+
// (b) `world.spawn({ component: Camera, data: { fov, aspect, near, far } })`
|
|
15
|
+
// compiles — AC-02
|
|
16
|
+
// hello-window:93 baseline. With ShapeOf<S> the same call reports
|
|
17
|
+
// ts(2739) "missing 5 properties"; with Partial<ShapeOf<S>> it
|
|
18
|
+
// compiles (and at runtime layer-2 / layer-3 fallback fills the
|
|
19
|
+
// remaining fields).
|
|
20
|
+
//
|
|
21
|
+
// (c) `data: { fov: 'bad' }` fires field-level TS2322 — AC-03
|
|
22
|
+
// Mapped-tuple primary inference must NOT degrade after the
|
|
23
|
+
// widening: the wrong-VALUE case continues to surface as
|
|
24
|
+
// TS2322 ("Type 'string' is not assignable to type 'number'")
|
|
25
|
+
// directly on the offending key — NOT as the "No overload matches"
|
|
26
|
+
// wall the legacy fallback overload would fall back to (C-4).
|
|
27
|
+
//
|
|
28
|
+
// AI users grep `// @ts-expect-error AC-` here to find the regression
|
|
29
|
+
// barrier that protects the mapped-tuple primary-inference behaviour
|
|
30
|
+
// across TS upgrades (R-1 / plan-strategy §2.7 P3).
|
|
31
|
+
|
|
32
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
33
|
+
import { defineComponent, type SchemaOf, type ShapeOf } from '../component';
|
|
34
|
+
import { type ComponentData, World } from '../world';
|
|
35
|
+
|
|
36
|
+
// Shared schema fixture — mirrors the runtime Camera token (9 f32) without
|
|
37
|
+
// pulling @forgeax/engine-runtime in.
|
|
38
|
+
const CameraLike = defineComponent('CameraLike', {
|
|
39
|
+
fov: { type: 'f32' },
|
|
40
|
+
aspect: { type: 'f32' },
|
|
41
|
+
near: { type: 'f32' },
|
|
42
|
+
far: { type: 'f32' },
|
|
43
|
+
projection: { type: 'f32' },
|
|
44
|
+
left: { type: 'f32' },
|
|
45
|
+
right: { type: 'f32' },
|
|
46
|
+
bottom: { type: 'f32' },
|
|
47
|
+
top: { type: 'f32' },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
type CameraLikeSchema = SchemaOf<typeof CameraLike>;
|
|
51
|
+
|
|
52
|
+
describe('ComponentData<S>.data — Partial<ShapeOf<S>> (AC-01, t6.a)', () => {
|
|
53
|
+
it('ComponentData<S>["data"] equals Partial<ShapeOf<S>>', () => {
|
|
54
|
+
type DataField = ComponentData<CameraLikeSchema>['data'];
|
|
55
|
+
expectTypeOf<DataField>().toEqualTypeOf<Partial<ShapeOf<CameraLikeSchema>>>();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('ComponentData<S>["data"] is NOT the un-Partial ShapeOf<S>', () => {
|
|
59
|
+
type DataField = ComponentData<CameraLikeSchema>['data'];
|
|
60
|
+
// The wider Partial<ShapeOf<S>> is assignable to itself but the
|
|
61
|
+
// narrower ShapeOf<S> would need every key. After M2 widening,
|
|
62
|
+
// a Partial value is assignable to DataField; before M2 the same
|
|
63
|
+
// Partial is rejected by ShapeOf<S>'s required-keys contract.
|
|
64
|
+
const partial: Partial<ShapeOf<CameraLikeSchema>> = { fov: 1 };
|
|
65
|
+
const field: DataField = partial;
|
|
66
|
+
void field;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('ComponentData<S>["data"] is NOT widened to any', () => {
|
|
70
|
+
type DataField = ComponentData<CameraLikeSchema>['data'];
|
|
71
|
+
expectTypeOf<DataField>().not.toBeAny();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('world.spawn — partial data accepted (AC-02, t6.b)', () => {
|
|
76
|
+
it('hello-window-shape 4-field call compiles after M2 widening', () => {
|
|
77
|
+
const w = new World();
|
|
78
|
+
// The hello-window:93 baseline call: drop the 5 ortho fields on a
|
|
79
|
+
// perspective spawn. Before M2 this fires ts(2739) ("missing 5
|
|
80
|
+
// properties"); after M2 it compiles and runtime layer-2 / layer-3
|
|
81
|
+
// fallback fills the missing fields.
|
|
82
|
+
const r = w.spawn({
|
|
83
|
+
component: CameraLike,
|
|
84
|
+
data: { fov: 1, aspect: 1, near: 1, far: 100 },
|
|
85
|
+
});
|
|
86
|
+
void r;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('full 9-field call still compiles (no regression)', () => {
|
|
90
|
+
const w = new World();
|
|
91
|
+
const r = w.spawn({
|
|
92
|
+
component: CameraLike,
|
|
93
|
+
data: {
|
|
94
|
+
fov: 1,
|
|
95
|
+
aspect: 1,
|
|
96
|
+
near: 1,
|
|
97
|
+
far: 100,
|
|
98
|
+
projection: 0,
|
|
99
|
+
left: -1,
|
|
100
|
+
right: 1,
|
|
101
|
+
bottom: -1,
|
|
102
|
+
top: 1,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
void r;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('completely empty data: {} compiles (every field defaulted)', () => {
|
|
109
|
+
const w = new World();
|
|
110
|
+
const r = w.spawn({
|
|
111
|
+
component: CameraLike,
|
|
112
|
+
data: {},
|
|
113
|
+
});
|
|
114
|
+
void r;
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('world.spawn — wrong-value fires field-level TS2322 (AC-03, t6.c)', () => {
|
|
119
|
+
it('data: { fov: "bad" } reports field-level TS2322 (mapped-tuple primary intact)', () => {
|
|
120
|
+
const w = new World();
|
|
121
|
+
// AC-03: mapped-tuple primary inference surfaces wrong-VALUE on the
|
|
122
|
+
// OFFENDING KEY (field-level TS2322), not as the overload wall.
|
|
123
|
+
// The directive sits directly above the offending key so vitest's
|
|
124
|
+
// ts-expect-error tracker sees the suppression land on the right
|
|
125
|
+
// expression.
|
|
126
|
+
w.spawn({
|
|
127
|
+
component: CameraLike,
|
|
128
|
+
data: {
|
|
129
|
+
// @ts-expect-error AC-03 fov is f32 (number), not string
|
|
130
|
+
fov: 'bad',
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('data: { aspect: false } reports field-level TS2322 (bool->f32 narrowing)', () => {
|
|
136
|
+
const w = new World();
|
|
137
|
+
w.spawn({
|
|
138
|
+
component: CameraLike,
|
|
139
|
+
data: {
|
|
140
|
+
// @ts-expect-error AC-03 aspect is f32 (number), not boolean
|
|
141
|
+
aspect: false,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('data: { unknownKey: 1 } reports excess-property error, not "No overload matches"', () => {
|
|
147
|
+
const w = new World();
|
|
148
|
+
w.spawn({
|
|
149
|
+
component: CameraLike,
|
|
150
|
+
data: {
|
|
151
|
+
// @ts-expect-error AC-03 unknownKey is not part of CameraLike schema
|
|
152
|
+
unknownKey: 1,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|