@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,418 @@
|
|
|
1
|
+
// @forgeax/engine-ecs - SharedRefStore.
|
|
2
|
+
//
|
|
3
|
+
// Reference-counted store for AssetRegistry-style shared handles. Schema vocab
|
|
4
|
+
// `shared<T>` fields derive `Handle<T, 'shared'>` whose lifecycle is tracked
|
|
5
|
+
// by retain/release on the holder side - allocators (typically the asset
|
|
6
|
+
// registry) call `alloc` once and let consumers retain/release as the asset
|
|
7
|
+
// flows through ECS components and external systems. When rc transitions
|
|
8
|
+
// 1 -> 0 the store publishes release evidence. External owners dispose their
|
|
9
|
+
// payloads from their own lifecycle effects; ECS does not invoke callbacks.
|
|
10
|
+
//
|
|
11
|
+
// Companion to UniqueRefStore (1-holder-direct-release semantics). The two
|
|
12
|
+
// stores share storage shape and code patterns; only the lifecycle differs:
|
|
13
|
+
//
|
|
14
|
+
// UniqueRefStore: alloc -> store; release -> drop. No retain.
|
|
15
|
+
// SharedRefStore: alloc -> rc=1; retain -> rc++; release -> rc--; rc=0 -> drop.
|
|
16
|
+
//
|
|
17
|
+
// §contract - shared handles are refcounted AND carry a generation
|
|
18
|
+
// The handle u32 packs `(generation << 24) | slot` via the shared codec in
|
|
19
|
+
// `@forgeax/engine-types` (same SSOT codec as UniqueRefStore and ECS
|
|
20
|
+
// EntityHandle). `alloc` welds the slot's current generation; resolve /
|
|
21
|
+
// retain / release compare the handle generation against
|
|
22
|
+
// `_generations[slot]` BEFORE any refcount/payload work, so a handle whose
|
|
23
|
+
// slot was released and re-allocated returns `SharedRefStaleError`
|
|
24
|
+
// ('shared-ref-stale') instead of silently resolving the next payload.
|
|
25
|
+
// Generation advances on the rc=0 drop and retires the slot when gen would
|
|
26
|
+
// exceed MAX_GEN (gen 255 is still usable; retire at would-be 256, no wrap).
|
|
27
|
+
// AI users keep the rule "don't cache a handle
|
|
28
|
+
// past release" - the AssetRegistry mediates most shared handle lifecycles,
|
|
29
|
+
// but a stale handle now fails structurally rather than silently.
|
|
30
|
+
//
|
|
31
|
+
// §tier boundary (feat-20260614 M6 D-15)
|
|
32
|
+
// This store manages ONLY user-tier slots (`slot >= BUILTIN_BASE`). Builtin
|
|
33
|
+
// asset payloads in the builtin range are process-static and live in their
|
|
34
|
+
// authoring package; they
|
|
35
|
+
// are never reference-counted. `nextSlot` starts at `BUILTIN_BASE` so minted
|
|
36
|
+
// handles never collide with builtin slots, and alloc/retain/release/resolve
|
|
37
|
+
// fail-fast with `BuiltinSlotNotOwnedError` when handed a builtin slot.
|
|
38
|
+
//
|
|
39
|
+
// Storage shape:
|
|
40
|
+
// - `payloads: Map<number, unknown>` - key = handle u32 = slot index.
|
|
41
|
+
// - `refcounts: Map<number, number>` - key = handle u32; rc >= 1 while live;
|
|
42
|
+
// removed (not set to 0) on final release
|
|
43
|
+
// so resolve / retain can detect the
|
|
44
|
+
// released state via `payloads.has(raw)`.
|
|
45
|
+
// - `freeSlots: number[]` - LIFO stack of recyclable slot indices.
|
|
46
|
+
// - `nextSlot` - bump counter for the never-recycled tail
|
|
47
|
+
// (starts at BUILTIN_BASE; user tier only).
|
|
48
|
+
//
|
|
49
|
+
// 24 bits = 16_777_215 simultaneous live shared handles - same ceiling as
|
|
50
|
+
// UniqueRefStore + Entity, so the three managed-handle resources fail-fast
|
|
51
|
+
// at the same bound.
|
|
52
|
+
//
|
|
53
|
+
// Release path (D-1 codes):
|
|
54
|
+
// - resolve(h): err(SharedRefReleasedError) if payload absent.
|
|
55
|
+
// - markChanged(h): publish an in-place payload mutation to subscribers.
|
|
56
|
+
// - retain(h): err(SharedRefReleasedError) if payload absent.
|
|
57
|
+
// - release(h): err(SharedRefDoubleReleaseError, rc=0) on rc=0 input.
|
|
58
|
+
// - any(builtin slot): err(BuiltinSlotNotOwnedError) (D-15).
|
|
59
|
+
//
|
|
60
|
+
// Identity invariant (mirrors UniqueRefStore): resolve(h) returns the SAME
|
|
61
|
+
// payload object on every call until the final release. Archetype migration
|
|
62
|
+
// preserves this trivially - the column carries the u32 handle, never the
|
|
63
|
+
// payload reference.
|
|
64
|
+
|
|
65
|
+
import type { Handle } from '@forgeax/engine-types';
|
|
66
|
+
import {
|
|
67
|
+
BUILTIN_BASE,
|
|
68
|
+
err,
|
|
69
|
+
handleGeneration,
|
|
70
|
+
handleSlot,
|
|
71
|
+
isRetiredSlot,
|
|
72
|
+
MAX_SLOT,
|
|
73
|
+
ok,
|
|
74
|
+
pack,
|
|
75
|
+
type Result,
|
|
76
|
+
toShared,
|
|
77
|
+
unwrapHandle,
|
|
78
|
+
} from '@forgeax/engine-types';
|
|
79
|
+
import {
|
|
80
|
+
BuiltinSlotNotOwnedError,
|
|
81
|
+
SharedRefDoubleReleaseError,
|
|
82
|
+
SharedRefPayloadInvalidError,
|
|
83
|
+
SharedRefReleasedError,
|
|
84
|
+
SharedRefStaleError,
|
|
85
|
+
} from './errors';
|
|
86
|
+
|
|
87
|
+
const SHARED_REF_MUTATION_JOURNAL_CAPACITY = 4096;
|
|
88
|
+
|
|
89
|
+
export interface SharedRefMutation {
|
|
90
|
+
readonly epoch: number;
|
|
91
|
+
readonly handle: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SharedRefReleaseEvidence {
|
|
95
|
+
readonly payload: unknown;
|
|
96
|
+
readonly refcount: 0;
|
|
97
|
+
readonly generation: number;
|
|
98
|
+
readonly evidence: 'released';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type SharedRefMutationRead =
|
|
102
|
+
| {
|
|
103
|
+
readonly status: 'ok';
|
|
104
|
+
readonly cursor: number;
|
|
105
|
+
readonly records: readonly SharedRefMutation[];
|
|
106
|
+
}
|
|
107
|
+
| {
|
|
108
|
+
readonly status: 'overflow';
|
|
109
|
+
readonly cursor: number;
|
|
110
|
+
readonly oldestAvailable: number;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// MAX_SLOT is now imported from @forgeax/engine-types (codec SSOT, D-1).
|
|
114
|
+
// The local constant is removed to avoid drift (AC-15).
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Reference-counted store for ECS-aware `Handle<T, 'shared'>` lifecycles.
|
|
118
|
+
*
|
|
119
|
+
* The producer (typically AssetRegistry) calls `alloc` once and owns the
|
|
120
|
+
* "alloc-grant" rc=1; consumers (ECS schema fields, external systems) call
|
|
121
|
+
* `retain` on each new holder and `release` when the holder drops. When rc
|
|
122
|
+
* transitions 1 -> 0 the store publishes one structured release-evidence
|
|
123
|
+
* record; it does not invoke a user callback or own payload disposal.
|
|
124
|
+
*
|
|
125
|
+
* D-15: manages ONLY user-tier slots (`slot >= BUILTIN_BASE`). Builtin slots
|
|
126
|
+
* (`< BUILTIN_BASE`) fail-fast with `BuiltinSlotNotOwnedError`.
|
|
127
|
+
*
|
|
128
|
+
* Public API:
|
|
129
|
+
* - alloc(target, payload) -> Handle<T, 'shared'> (rc=1)
|
|
130
|
+
* - intern(target, payload) -> stable producer handle per target + object identity
|
|
131
|
+
* - resolve(handle) -> Result<T, SharedRefReleasedError | SharedRefStaleError | BuiltinSlotNotOwnedError>
|
|
132
|
+
* - markChanged(handle) -> Result<void, SharedRefReleasedError | SharedRefStaleError | BuiltinSlotNotOwnedError>
|
|
133
|
+
* - getMutationEpoch() -> monotonic payload-mutation cursor
|
|
134
|
+
* - retain(handle) -> Result<void, SharedRefReleasedError | SharedRefStaleError | BuiltinSlotNotOwnedError>
|
|
135
|
+
* - release(handle) -> Result<release evidence | undefined, ...>
|
|
136
|
+
* - refcount(handle) -> number (0 == released; debug + tests)
|
|
137
|
+
* - _liveCount() -> live slot count (debug + inspector)
|
|
138
|
+
*/
|
|
139
|
+
export class SharedRefStore {
|
|
140
|
+
private readonly payloads = new Map<number, unknown>();
|
|
141
|
+
private readonly refcounts = new Map<number, number>();
|
|
142
|
+
private readonly freeSlots: number[] = [];
|
|
143
|
+
private readonly internedByTarget = new Map<string, WeakMap<object, number>>();
|
|
144
|
+
private readonly internedKeys = new Map<
|
|
145
|
+
number,
|
|
146
|
+
{ readonly target: string; readonly payload: object }
|
|
147
|
+
>();
|
|
148
|
+
private nextSlot = BUILTIN_BASE;
|
|
149
|
+
private mutationEpoch = 0;
|
|
150
|
+
private readonly mutationJournal: SharedRefMutation[] = [];
|
|
151
|
+
private readonly releaseJournal: SharedRefReleaseEvidence[] = [];
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Generation table indexed by slot (D-6). Each entry tracks the current
|
|
155
|
+
* generation for the slot — written to during alloc (welded into the
|
|
156
|
+
* returned handle via pack) and incremented on release (M4).
|
|
157
|
+
*
|
|
158
|
+
* @internal
|
|
159
|
+
*/
|
|
160
|
+
// biome-ignore lint/style/useNamingConvention: internal field — @internal JSDoc suppresses lint:internal gate
|
|
161
|
+
private readonly _generations: number[] = [];
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Allocate a fresh shared handle for `payload`, branded against `target`.
|
|
165
|
+
* Refcount starts at 1 (the alloc-grant). Nullish payloads are rejected
|
|
166
|
+
* before a slot or free-list is touched.
|
|
167
|
+
*
|
|
168
|
+
* The returned handle carries a generation tag welded via codec.pack
|
|
169
|
+
* (D-8, OOS-2): first allocation gen=0 (AC-06), reused slot gen = the
|
|
170
|
+
* current generation from _generations[slot]. The toShared brand cast
|
|
171
|
+
* happens internally — external callers no longer construct Handle<...>
|
|
172
|
+
* directly.
|
|
173
|
+
*
|
|
174
|
+
* Minted slots are user-tier (`>= BUILTIN_BASE`); builtin slots are never
|
|
175
|
+
* produced here (D-15).
|
|
176
|
+
*/
|
|
177
|
+
alloc<Target extends string, T = unknown>(target: Target, payload: T): Handle<Target, 'shared'> {
|
|
178
|
+
void target; // target is a phantom - tag flows only at the type level via Handle<Target,_>.
|
|
179
|
+
if (payload === null || payload === undefined) {
|
|
180
|
+
throw new SharedRefPayloadInvalidError(target, payload === null ? 'null' : 'undefined');
|
|
181
|
+
}
|
|
182
|
+
const slot = this.freeSlots.pop() ?? this.nextSlot++;
|
|
183
|
+
if (slot > MAX_SLOT) {
|
|
184
|
+
throw new RangeError(
|
|
185
|
+
`SharedRefStore: slot index ${slot} exceeds 24-bit max (${MAX_SLOT}). ` +
|
|
186
|
+
'Reduce simultaneous shared handles or investigate handle leaks.',
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const gen = this._generations[slot] ?? 0;
|
|
190
|
+
const raw = pack(slot, gen);
|
|
191
|
+
this.payloads.set(raw, payload);
|
|
192
|
+
this.refcounts.set(raw, 1);
|
|
193
|
+
return toShared(raw);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Return the idempotent producer handle for one object payload in this
|
|
198
|
+
* store. Identity includes `target`, so the same object branded for two
|
|
199
|
+
* asset kinds does not alias.
|
|
200
|
+
*
|
|
201
|
+
* A cache hit deliberately does not retain: this is one producer grant,
|
|
202
|
+
* discovered repeatedly by an asset catalogue. Actual holders retain and
|
|
203
|
+
* release through the ECS write barrier. Callers needing independent
|
|
204
|
+
* grants or a per-handle deleter must use {@link alloc}.
|
|
205
|
+
*/
|
|
206
|
+
intern<Target extends string, T extends object>(
|
|
207
|
+
target: Target,
|
|
208
|
+
payload: T,
|
|
209
|
+
): Handle<Target, 'shared'> {
|
|
210
|
+
let byPayload = this.internedByTarget.get(target);
|
|
211
|
+
if (byPayload === undefined) {
|
|
212
|
+
byPayload = new WeakMap<object, number>();
|
|
213
|
+
this.internedByTarget.set(target, byPayload);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const existingRaw = byPayload.get(payload);
|
|
217
|
+
if (existingRaw !== undefined && this.payloads.has(existingRaw)) {
|
|
218
|
+
return toShared(existingRaw);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const handle = this.alloc(target, payload);
|
|
222
|
+
const raw = unwrapHandle(handle);
|
|
223
|
+
byPayload.set(payload, raw);
|
|
224
|
+
this.internedKeys.set(raw, { target, payload });
|
|
225
|
+
return handle;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Look up `payload` by handle. Returns `err(shared-ref-released)` when
|
|
230
|
+
* the handle's slot has no live payload (rc reached 0, no re-alloc has
|
|
231
|
+
* filled the slot); `err(builtin-slot-not-owned)` for a builtin slot.
|
|
232
|
+
*
|
|
233
|
+
* §contract - mirrors UniqueRefStore: a stale handle whose slot has been
|
|
234
|
+
* released and re-allocated returns `err(shared-ref-stale)` - the handle's
|
|
235
|
+
* welded generation no longer matches `_generations[slot]`. The gen check
|
|
236
|
+
* runs before payload lookup, so stale-by-reuse is caught deterministically.
|
|
237
|
+
*/
|
|
238
|
+
resolve<Target extends string, T = unknown>(
|
|
239
|
+
handle: Handle<Target, 'shared'>,
|
|
240
|
+
): Result<T, SharedRefReleasedError | SharedRefStaleError | BuiltinSlotNotOwnedError> {
|
|
241
|
+
const raw = unwrapHandle(handle);
|
|
242
|
+
if (raw < BUILTIN_BASE) return err(new BuiltinSlotNotOwnedError(raw));
|
|
243
|
+
// Gen comparison (M4): extract slot + handle gen, compare against
|
|
244
|
+
// store's current gen. Mismatch means slot was released and re-allocated —
|
|
245
|
+
// the caller's handle is stale. This check runs BEFORE payload lookup so
|
|
246
|
+
// stale-by-reuse is always caught (AC-01).
|
|
247
|
+
const slot = handleSlot(handle);
|
|
248
|
+
const handleGen = handleGeneration(handle);
|
|
249
|
+
const storeGen = this._generations[slot] ?? 0;
|
|
250
|
+
if (handleGen !== storeGen) {
|
|
251
|
+
return err(new SharedRefStaleError(slot, handleGen, storeGen));
|
|
252
|
+
}
|
|
253
|
+
const payload = this.payloads.get(raw);
|
|
254
|
+
if (payload === undefined) {
|
|
255
|
+
return err(new SharedRefReleasedError(raw, '<unknown>'));
|
|
256
|
+
}
|
|
257
|
+
return ok(payload as T);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Publish that a live payload was mutated in place. Consumers that retain
|
|
262
|
+
* projections of shared payload data compare the monotonic epoch and
|
|
263
|
+
* explicitly refresh instead of rescanning every payload each frame.
|
|
264
|
+
*/
|
|
265
|
+
markChanged<Target extends string>(
|
|
266
|
+
handle: Handle<Target, 'shared'>,
|
|
267
|
+
): Result<void, SharedRefReleasedError | SharedRefStaleError | BuiltinSlotNotOwnedError> {
|
|
268
|
+
const resolved = this.resolve(handle);
|
|
269
|
+
if (!resolved.ok) return resolved;
|
|
270
|
+
if (this.mutationEpoch >= Number.MAX_SAFE_INTEGER) {
|
|
271
|
+
throw new RangeError('SharedRefStore mutation epoch exhausted');
|
|
272
|
+
}
|
|
273
|
+
this.mutationEpoch += 1;
|
|
274
|
+
this.mutationJournal.push({ epoch: this.mutationEpoch, handle: unwrapHandle(handle) });
|
|
275
|
+
if (this.mutationJournal.length > SHARED_REF_MUTATION_JOURNAL_CAPACITY) {
|
|
276
|
+
this.mutationJournal.shift();
|
|
277
|
+
}
|
|
278
|
+
return ok(undefined);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Current upper bound for explicitly published payload mutations. */
|
|
282
|
+
getMutationEpoch(): number {
|
|
283
|
+
return this.mutationEpoch;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Read exact changed handles after `cursor`; overflow requires consumer resync. */
|
|
287
|
+
readChangesSince(cursor: number): SharedRefMutationRead {
|
|
288
|
+
const oldestAvailable = this.mutationJournal[0]?.epoch ?? this.mutationEpoch + 1;
|
|
289
|
+
if (cursor < oldestAvailable - 1) {
|
|
290
|
+
return { status: 'overflow', cursor: this.mutationEpoch, oldestAvailable };
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
status: 'ok',
|
|
294
|
+
cursor: this.mutationEpoch,
|
|
295
|
+
records: this.mutationJournal.filter((record) => record.epoch > cursor),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Increment the refcount of a live shared handle. Returns
|
|
301
|
+
* `err(shared-ref-released)` when the handle is not live - retain MUST
|
|
302
|
+
* NOT resurrect a released slot (charter P3 explicit failure; would
|
|
303
|
+
* defeat the rc=0 -> drop invariant); `err(builtin-slot-not-owned)` for a
|
|
304
|
+
* builtin slot.
|
|
305
|
+
*/
|
|
306
|
+
retain<Target extends string>(
|
|
307
|
+
handle: Handle<Target, 'shared'>,
|
|
308
|
+
): Result<void, SharedRefReleasedError | SharedRefStaleError | BuiltinSlotNotOwnedError> {
|
|
309
|
+
const raw = unwrapHandle(handle);
|
|
310
|
+
if (raw < BUILTIN_BASE) return err(new BuiltinSlotNotOwnedError(raw));
|
|
311
|
+
// Gen comparison runs before rc read (q12: all three operations compare gen first).
|
|
312
|
+
const slot = handleSlot(handle);
|
|
313
|
+
const handleGen = handleGeneration(handle);
|
|
314
|
+
const storeGen = this._generations[slot] ?? 0;
|
|
315
|
+
if (handleGen !== storeGen) {
|
|
316
|
+
return err(new SharedRefStaleError(slot, handleGen, storeGen));
|
|
317
|
+
}
|
|
318
|
+
const rc = this.refcounts.get(raw);
|
|
319
|
+
if (rc === undefined) {
|
|
320
|
+
return err(new SharedRefReleasedError(raw, '<unknown>'));
|
|
321
|
+
}
|
|
322
|
+
this.refcounts.set(raw, rc + 1);
|
|
323
|
+
return ok(undefined);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Decrement the refcount. When rc transitions 1 -> 0, the slot is dropped
|
|
328
|
+
* and one structured release-evidence record is published. The evidence
|
|
329
|
+
* captures the payload, zero refcount, next generation and `released` marker
|
|
330
|
+
* after the store has removed the live slot.
|
|
331
|
+
*
|
|
332
|
+
* Returns `err(shared-ref-double-release, rc=0)` when the handle has
|
|
333
|
+
* already reached rc=0 (or was never live); `err(builtin-slot-not-owned)`
|
|
334
|
+
* for a builtin slot. AI users branch on `.code` and route the
|
|
335
|
+
* second-release log to Layer 3 ErrorHandler without aborting the despawn
|
|
336
|
+
* chain.
|
|
337
|
+
*/
|
|
338
|
+
release<Target extends string>(
|
|
339
|
+
handle: Handle<Target, 'shared'>,
|
|
340
|
+
): Result<
|
|
341
|
+
SharedRefReleaseEvidence | undefined,
|
|
342
|
+
SharedRefDoubleReleaseError | SharedRefStaleError | BuiltinSlotNotOwnedError
|
|
343
|
+
> {
|
|
344
|
+
const raw = unwrapHandle(handle);
|
|
345
|
+
if (raw < BUILTIN_BASE) return err(new BuiltinSlotNotOwnedError(raw));
|
|
346
|
+
// Gen comparison runs FIRST — before rc read, before any mutation (q12).
|
|
347
|
+
const slot = handleSlot(handle);
|
|
348
|
+
const handleGen = handleGeneration(handle);
|
|
349
|
+
const storeGen = this._generations[slot] ?? 0;
|
|
350
|
+
if (handleGen !== storeGen) {
|
|
351
|
+
// AC-03: stale release MUST NOT touch rc / payload / freeSlots.
|
|
352
|
+
return err(new SharedRefStaleError(slot, handleGen, storeGen));
|
|
353
|
+
}
|
|
354
|
+
const rc = this.refcounts.get(raw);
|
|
355
|
+
if (rc === undefined) {
|
|
356
|
+
return err(new SharedRefDoubleReleaseError(raw, '<unknown>', 0));
|
|
357
|
+
}
|
|
358
|
+
if (rc > 1) {
|
|
359
|
+
this.refcounts.set(raw, rc - 1);
|
|
360
|
+
return ok(undefined);
|
|
361
|
+
}
|
|
362
|
+
// rc === 1 -> drop. Capture the payload for release evidence before
|
|
363
|
+
// clearing the live slot; nullish payloads never enter the store.
|
|
364
|
+
const payload = this.payloads.get(raw);
|
|
365
|
+
const internedKey = this.internedKeys.get(raw);
|
|
366
|
+
if (internedKey !== undefined) {
|
|
367
|
+
const byPayload = this.internedByTarget.get(internedKey.target);
|
|
368
|
+
if (byPayload?.get(internedKey.payload) === raw) {
|
|
369
|
+
byPayload.delete(internedKey.payload);
|
|
370
|
+
}
|
|
371
|
+
this.internedKeys.delete(raw);
|
|
372
|
+
}
|
|
373
|
+
this.refcounts.delete(raw);
|
|
374
|
+
this.payloads.delete(raw);
|
|
375
|
+
// Gen increment + retire (AC-07): bump gen; once it would exceed MAX_GEN
|
|
376
|
+
// (gen 255 is still usable; the bump to 256 triggers retire) the slot is
|
|
377
|
+
// permanently retired — NOT pushed to freeSlots. This prevents handle
|
|
378
|
+
// aliasing. Shares the isRetiredSlot SSOT predicate with EntityHandle.
|
|
379
|
+
const generation = storeGen + 1;
|
|
380
|
+
this._generations[slot] = generation;
|
|
381
|
+
if (!isRetiredSlot(generation)) {
|
|
382
|
+
this.freeSlots.push(slot);
|
|
383
|
+
}
|
|
384
|
+
// else: slot retired (gen exceeded MAX_GEN) - never returns to freeSlots.
|
|
385
|
+
const evidence = Object.freeze({
|
|
386
|
+
payload,
|
|
387
|
+
refcount: 0 as const,
|
|
388
|
+
generation,
|
|
389
|
+
evidence: 'released' as const,
|
|
390
|
+
});
|
|
391
|
+
this.releaseJournal.push(evidence);
|
|
392
|
+
if (this.releaseJournal.length > SHARED_REF_MUTATION_JOURNAL_CAPACITY) {
|
|
393
|
+
this.releaseJournal.shift();
|
|
394
|
+
}
|
|
395
|
+
return ok(evidence);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Read the bounded release evidence owned by this store. */
|
|
399
|
+
readReleaseEvidence(): readonly SharedRefReleaseEvidence[] {
|
|
400
|
+
return this.releaseJournal;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Return the current refcount for `handle`. Returns 0 for a released
|
|
405
|
+
* (or never-allocated) slot. Primarily a debug + tests entry point;
|
|
406
|
+
* production code rarely reads rc directly (the rc=0 -> drop invariant
|
|
407
|
+
* is the surface AI users consume via release / the per-handle deleter).
|
|
408
|
+
*/
|
|
409
|
+
refcount<Target extends string>(handle: Handle<Target, 'shared'>): number {
|
|
410
|
+
const raw = unwrapHandle(handle);
|
|
411
|
+
return this.refcounts.get(raw) ?? 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** @internal Diagnostic count of live slots. Exposed for tests + inspector. */
|
|
415
|
+
_liveCount(): number {
|
|
416
|
+
return this.payloads.size;
|
|
417
|
+
}
|
|
418
|
+
}
|
package/src/shared.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
KernelDispatchFailure,
|
|
3
|
+
KernelDispatchResult,
|
|
4
|
+
KernelDispatchSpan,
|
|
5
|
+
SharedFieldView,
|
|
6
|
+
SharedKernelDefinition,
|
|
7
|
+
SharedKernelDispatch,
|
|
8
|
+
SharedKernelEligibilityReason,
|
|
9
|
+
SharedKernelExecutor,
|
|
10
|
+
SharedKernelHandle,
|
|
11
|
+
SharedSpanBinding,
|
|
12
|
+
WorldExecutionHealth,
|
|
13
|
+
} from './execution/shared-kernel';
|
|
14
|
+
export {
|
|
15
|
+
bindSharedSpan,
|
|
16
|
+
defineSharedKernel,
|
|
17
|
+
isKernelDispatchFailure,
|
|
18
|
+
isSharedSpan,
|
|
19
|
+
SHARED_KERNEL_ELIGIBILITY_REASONS,
|
|
20
|
+
SHARED_KERNEL_EXECUTOR_RESOURCE_KEY,
|
|
21
|
+
sharedKernelEligibility,
|
|
22
|
+
splitSharedSpan,
|
|
23
|
+
} from './execution/shared-kernel';
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// @forgeax/engine-ecs — ArchetypeGraph: manages archetype creation and edge caching.
|
|
2
|
+
//
|
|
3
|
+
// O(1) archetype migration lookup via cached edges (D-02 / Finding 2).
|
|
4
|
+
// Global `generation` counter incremented on each new archetype creation
|
|
5
|
+
// (ArchetypeGeneration for query cache incremental update).
|
|
6
|
+
|
|
7
|
+
import type { Component, ComponentId } from '../component';
|
|
8
|
+
import * as componentOwner from '../component';
|
|
9
|
+
import { type Archetype, type ArchetypeId, archetypeKey, createArchetype } from './archetype';
|
|
10
|
+
import { createSparseTagSet, type SparseTagSet } from './change-detection';
|
|
11
|
+
import { canonicalComponents, createTable, type Table, type TableId, tableKey } from './table';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The ArchetypeGraph owns all archetypes and their edge caches.
|
|
15
|
+
*
|
|
16
|
+
* `archetypes` is the SSOT: a dense array indexed by `ArchetypeId` (the id is
|
|
17
|
+
* allocated as the next array slot, so `archetypes[id].id === id`).
|
|
18
|
+
* `dedupByKey` is a derived index `key → ArchetypeId` used only at archetype
|
|
19
|
+
* creation to dedupe; consumers always read through `archetypes[id]`
|
|
20
|
+
* (architecture-principles.md §1 SSOT, §2 Derive — replaces the historical
|
|
21
|
+
* `byKey: Map<string, Archetype>` / `byId: Archetype[]` pair which carried two
|
|
22
|
+
* full Archetype refs per row).
|
|
23
|
+
*/
|
|
24
|
+
export interface ArchetypeGraph {
|
|
25
|
+
/** SSOT: dense array indexed by `ArchetypeId` (id == array slot). */
|
|
26
|
+
archetypes: Archetype[];
|
|
27
|
+
/** Derived dedup index: canonical-key string → ArchetypeId. */
|
|
28
|
+
dedupByKey: Map<string, ArchetypeId>;
|
|
29
|
+
/** Global generation counter. Incremented on each new archetype. */
|
|
30
|
+
generation: number;
|
|
31
|
+
tables: Table[];
|
|
32
|
+
tableDedupByKey: Map<string, TableId>;
|
|
33
|
+
tableGeneration: number;
|
|
34
|
+
sparseTags: Map<ComponentId, SparseTagSet>;
|
|
35
|
+
readonly shared: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a fresh ArchetypeGraph.
|
|
40
|
+
*/
|
|
41
|
+
export function createArchetypeGraph(shared = false): ArchetypeGraph {
|
|
42
|
+
return {
|
|
43
|
+
archetypes: [],
|
|
44
|
+
dedupByKey: new Map(),
|
|
45
|
+
generation: 0,
|
|
46
|
+
tables: [],
|
|
47
|
+
tableDedupByKey: new Map(),
|
|
48
|
+
tableGeneration: 0,
|
|
49
|
+
sparseTags: new Map(),
|
|
50
|
+
shared,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getTable(graph: ArchetypeGraph, id: TableId): Table {
|
|
55
|
+
const table = graph.tables[id];
|
|
56
|
+
if (table === undefined) throw new Error(`Table ${id} does not exist.`);
|
|
57
|
+
return table;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getOrCreateTable(
|
|
61
|
+
graph: ArchetypeGraph,
|
|
62
|
+
components: ReadonlyArray<Component>,
|
|
63
|
+
): Table {
|
|
64
|
+
const tableComponents = canonicalComponents(
|
|
65
|
+
components.filter((component) => component.storage === 'table'),
|
|
66
|
+
);
|
|
67
|
+
const key = tableKey(tableComponents.map((component) => componentOwner.componentId(component)));
|
|
68
|
+
const existingId = graph.tableDedupByKey.get(key);
|
|
69
|
+
if (existingId !== undefined) return getTable(graph, existingId);
|
|
70
|
+
const table = createTable(tableComponents, graph.tables.length, graph.shared);
|
|
71
|
+
graph.tables.push(table);
|
|
72
|
+
graph.tableDedupByKey.set(key, table.id);
|
|
73
|
+
graph.tableGeneration += 1;
|
|
74
|
+
return table;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getOrCreateSparseTagSet(graph: ArchetypeGraph, component: Component): SparseTagSet {
|
|
78
|
+
const current = graph.sparseTags.get(componentOwner.componentId(component));
|
|
79
|
+
if (current !== undefined) return current;
|
|
80
|
+
const set = createSparseTagSet(component);
|
|
81
|
+
graph.sparseTags.set(componentOwner.componentId(component), set);
|
|
82
|
+
return set;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get or create an archetype for the given component set.
|
|
87
|
+
* ComponentIds are sorted to produce a canonical key.
|
|
88
|
+
*/
|
|
89
|
+
export function getOrCreateArchetype(
|
|
90
|
+
graph: ArchetypeGraph,
|
|
91
|
+
componentIds: ReadonlyArray<ComponentId>,
|
|
92
|
+
components: ReadonlyArray<Component>,
|
|
93
|
+
): Archetype {
|
|
94
|
+
const key = archetypeKey(componentIds);
|
|
95
|
+
const existingId = graph.dedupByKey.get(key);
|
|
96
|
+
if (existingId !== undefined) {
|
|
97
|
+
// biome-ignore lint/style/noNonNullAssertion: dedupByKey only ever holds ids that were pushed into archetypes[].
|
|
98
|
+
return graph.archetypes[existingId]!;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const fullComponents = canonicalComponents(components);
|
|
102
|
+
const table = getOrCreateTable(graph, fullComponents);
|
|
103
|
+
const archId = graph.archetypes.length;
|
|
104
|
+
const arch = createArchetype(fullComponents, archId, table.id);
|
|
105
|
+
graph.archetypes.push(arch);
|
|
106
|
+
graph.dedupByKey.set(key, archId);
|
|
107
|
+
graph.generation += 1;
|
|
108
|
+
return arch;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get the target archetype after adding `componentId` to `src`.
|
|
113
|
+
* Caches the edge for O(1) subsequent lookups.
|
|
114
|
+
*/
|
|
115
|
+
export function getAddEdge(
|
|
116
|
+
graph: ArchetypeGraph,
|
|
117
|
+
src: Archetype,
|
|
118
|
+
componentId: ComponentId,
|
|
119
|
+
component: Component,
|
|
120
|
+
): Archetype {
|
|
121
|
+
const cached = src.addEdges.get(componentId);
|
|
122
|
+
if (cached !== undefined) {
|
|
123
|
+
const target = graph.archetypes[cached];
|
|
124
|
+
if (target) {
|
|
125
|
+
return target;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Compute target archetype: src components + new component.
|
|
130
|
+
const newIds = [...src.components.map((c) => componentOwner.componentId(c)), componentId];
|
|
131
|
+
const newComponents = [...src.components, component];
|
|
132
|
+
const target = getOrCreateArchetype(graph, newIds, newComponents);
|
|
133
|
+
src.addEdges.set(componentId, target.id);
|
|
134
|
+
return target;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get the target archetype after removing `componentId` from `src`.
|
|
139
|
+
* Caches the edge for O(1) subsequent lookups.
|
|
140
|
+
*
|
|
141
|
+
* Component list is derived from `src.components.filter(c => componentOwner.componentId(c) !== componentId)`
|
|
142
|
+
* (no separate componentRegistry — feat-20260611 AC-09).
|
|
143
|
+
*/
|
|
144
|
+
export function getRemoveEdge(
|
|
145
|
+
graph: ArchetypeGraph,
|
|
146
|
+
src: Archetype,
|
|
147
|
+
componentId: ComponentId,
|
|
148
|
+
): Archetype {
|
|
149
|
+
const cached = src.removeEdges.get(componentId);
|
|
150
|
+
if (cached !== undefined) {
|
|
151
|
+
const target = graph.archetypes[cached];
|
|
152
|
+
if (target) {
|
|
153
|
+
return target;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Compute target archetype: src components minus removed component.
|
|
158
|
+
// Derive componentIds from src.components (single-field Archetype).
|
|
159
|
+
const newIds = src.components
|
|
160
|
+
.map((c) => componentOwner.componentId(c))
|
|
161
|
+
.filter((id) => id !== componentId);
|
|
162
|
+
const newComponents = src.components.filter((c) => componentOwner.componentId(c) !== componentId);
|
|
163
|
+
const target = getOrCreateArchetype(graph, newIds, newComponents);
|
|
164
|
+
src.removeEdges.set(componentId, target.id);
|
|
165
|
+
return target;
|
|
166
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Component, ComponentId } from '../component';
|
|
2
|
+
import { componentId } from '../component';
|
|
3
|
+
import { foldEssentials } from '../entity';
|
|
4
|
+
import type { TableId } from './table';
|
|
5
|
+
|
|
6
|
+
const INITIAL_CAPACITY = 64;
|
|
7
|
+
|
|
8
|
+
export type ArchetypeId = number;
|
|
9
|
+
|
|
10
|
+
export interface Archetype {
|
|
11
|
+
readonly id: ArchetypeId;
|
|
12
|
+
readonly key: string;
|
|
13
|
+
readonly components: ReadonlyArray<Component>;
|
|
14
|
+
readonly tableId: TableId;
|
|
15
|
+
rows: Uint32Array;
|
|
16
|
+
size: number;
|
|
17
|
+
capacity: number;
|
|
18
|
+
addEdges: Map<ComponentId, ArchetypeId>;
|
|
19
|
+
removeEdges: Map<ComponentId, ArchetypeId>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function archetypeKey(componentIds: ReadonlyArray<ComponentId>): string {
|
|
23
|
+
return [...foldEssentials(componentIds)].sort((a, b) => a - b).join('+');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createArchetype(
|
|
27
|
+
components: ReadonlyArray<Component>,
|
|
28
|
+
id: ArchetypeId,
|
|
29
|
+
tableId: TableId,
|
|
30
|
+
): Archetype {
|
|
31
|
+
const byId = new Map<ComponentId, Component>();
|
|
32
|
+
for (const component of components) byId.set(componentId(component), component);
|
|
33
|
+
const sorted = [...byId.values()].sort((a, b) => componentId(a) - componentId(b));
|
|
34
|
+
return {
|
|
35
|
+
id,
|
|
36
|
+
key: archetypeKey(sorted.map((component) => componentId(component))),
|
|
37
|
+
components: sorted,
|
|
38
|
+
tableId,
|
|
39
|
+
rows: new Uint32Array(INITIAL_CAPACITY),
|
|
40
|
+
size: 0,
|
|
41
|
+
capacity: INITIAL_CAPACITY,
|
|
42
|
+
addEdges: new Map(),
|
|
43
|
+
removeEdges: new Map(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function appendArchetypeRow(archetype: Archetype, tableRow: number): number {
|
|
48
|
+
if (archetype.size === archetype.capacity) growArchetype(archetype, archetype.capacity * 2);
|
|
49
|
+
const row = archetype.size;
|
|
50
|
+
archetype.rows[row] = tableRow;
|
|
51
|
+
archetype.size = row + 1;
|
|
52
|
+
return row;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function removeArchetypeRow(
|
|
56
|
+
archetype: Archetype,
|
|
57
|
+
row: number,
|
|
58
|
+
): { movedTableRow: number; newRow: number } | null {
|
|
59
|
+
const lastRow = archetype.size - 1;
|
|
60
|
+
if (row === lastRow) {
|
|
61
|
+
archetype.size = lastRow;
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const movedTableRow = archetype.rows[lastRow] ?? 0;
|
|
65
|
+
archetype.rows[row] = movedTableRow;
|
|
66
|
+
archetype.size = lastRow;
|
|
67
|
+
return { movedTableRow, newRow: row };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function growArchetype(archetype: Archetype, targetCapacity: number): void {
|
|
71
|
+
let capacity = archetype.capacity;
|
|
72
|
+
while (capacity < targetCapacity) capacity *= 2;
|
|
73
|
+
if (capacity === archetype.capacity) return;
|
|
74
|
+
const rows = new Uint32Array(capacity);
|
|
75
|
+
rows.set(archetype.rows);
|
|
76
|
+
archetype.rows = rows;
|
|
77
|
+
archetype.capacity = capacity;
|
|
78
|
+
}
|