@daneren2005/shared-memory-ecs 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,10 +10,14 @@ fog of war, sub-classed entities, required components, etc).
10
10
  - **Component** – a plain object with an `index` (its block inside a shared-memory pool) plus getters/
11
11
  setters over that memory. Games decide what components exist.
12
12
  - **`ComponentDefinition`** – describes a component: its typed array `type`, block `size`, the config keys
13
- that trigger loading (`loadProperties`), a `load(entity, memory, config)` and an optional `save(component)`.
14
- A component's data splits into `Config` (defining props supplied up front, e.g. `maxHealth`) and
15
- `Serialization` (runtime-derived state, e.g. current `health`); `load` sees `Config & Serialization` while
16
- `save` returns only the `Serialization` slice.
13
+ that trigger loading (`loadProperties`), and the two halves that build it `toBlock(config)` (maps the
14
+ config to the raw block values) and `attach(entity, memory, index)` (builds the accessor over that block).
15
+ Loading a component is `attach(entity, memory, memory.create(toBlock(config)))`; splitting it this way lets a
16
+ worker build the block off-thread (it calls only `toBlock`) and the main thread wrap it (`attach`) when it
17
+ adopts the entity. Plus an optional `save(component)` and `free(component)`. A component's data splits into
18
+ `Config` (defining props supplied up front, e.g. `maxHealth`) and `Serialization` (runtime-derived state,
19
+ e.g. current `health`); `toBlock` sees `Config & Serialization` while `save` returns only the `Serialization`
20
+ slice. `free` runs when the component is torn down (see [Freeing extra resources](#freeing-extra-resources)).
17
21
  - **`ComponentRegistry<C>`** – the map of all component definitions for a game.
18
22
  - **`EntityFactory<C>`** – maps an entity `type` name to a base (template) config. Loading an entity layers
19
23
  the caller's config over its type's template, so shared static data lives in one place and a save only
@@ -32,6 +36,7 @@ fog of war, sub-classed entities, required components, etc).
32
36
  ## Defining components
33
37
 
34
38
  ```ts
39
+ import { Component } from '@daneren2005/shared-memory-ecs';
35
40
  import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';
36
41
 
37
42
  interface HealthComponent {
@@ -43,7 +48,16 @@ interface HealthComponent {
43
48
  const HEALTH_INDEX = 0;
44
49
  const HEALTH_MAX_INDEX = 1;
45
50
 
46
- // `maxHealth` is the defining Config prop; `health` is optional, runtime-derived Serialization. `load`
51
+ // The accessor is a subclass of `Component`, so its get/set live on one shared prototype: reading a component off
52
+ // thousands of entities stays monomorphic and inline, and each instance is a single allocation rather than a
53
+ // closure per accessor. Index `this.block` (the typed-array view, set for you) by the exported *_INDEX constants.
54
+ class Health extends Component<Int32Array> implements HealthComponent {
55
+ get health() { return this.block[HEALTH_INDEX]; }
56
+ set health(value: number) { this.block[HEALTH_INDEX] = value; }
57
+ get maxHealth() { return this.block[HEALTH_MAX_INDEX]; }
58
+ }
59
+
60
+ // `maxHealth` is the defining Config prop; `health` is optional, runtime-derived Serialization. `toBlock`
47
61
  // and `save` both see the combined `{ maxHealth: number, health?: number }`.
48
62
  const healthDefinition: ComponentDefinition<HealthComponent, Int32Array, { maxHealth: number }, { health?: number }> = {
49
63
  type: Int32Array,
@@ -51,16 +65,15 @@ const healthDefinition: ComponentDefinition<HealthComponent, Int32Array, { maxHe
51
65
  // Configs are flat and shared: this component loads whenever `maxHealth` is present, then reads what it
52
66
  // needs off the same config object. Only Config props may appear here.
53
67
  loadProperties: ['maxHealth'],
54
- load(entity, memoryComponent, config) {
55
- const index = memoryComponent.create([config.health ?? config.maxHealth, config.maxHealth]);
56
- const memory = memoryComponent.getBlock(index);
57
-
58
- return {
59
- index,
60
- get health() { return memory[HEALTH_INDEX]; },
61
- set health(value: number) { memory[HEALTH_INDEX] = value; },
62
- get maxHealth() { return memory[HEALTH_MAX_INDEX]; }
63
- };
68
+ // The block values, purely from config — so a worker can build the block off-thread. No entity/world access.
69
+ toBlock(config) {
70
+ return [config.health ?? config.maxHealth, config.maxHealth];
71
+ },
72
+ // The accessor over an already-allocated block. Runs on the main thread both when loading and when adopting a
73
+ // worker-built entity; a component's own extra allocations (a SharedList, a resource block) go here — a
74
+ // subclass that owns extra memory takes more constructor args (the entity, another pool) and stores them.
75
+ attach(entity, memoryComponent, index) {
76
+ return new Health(memoryComponent.getBlock(index), index);
64
77
  },
65
78
  save(component) {
66
79
  // save returns only Serialization; maxHealth is Config and comes back from the template on reload.
@@ -73,7 +86,7 @@ const registry = {
73
86
  health: healthDefinition
74
87
  // ...other component definitions
75
88
  };
76
- type Components = { [K in keyof typeof registry]: ReturnType<(typeof registry)[K]['load']> };
89
+ type Components = { [K in keyof typeof registry]: ReturnType<(typeof registry)[K]['attach']> };
77
90
  ```
78
91
 
79
92
  ## Using the world
@@ -110,6 +123,12 @@ console.log(goblin.save()); // { type: 'goblin', health: 10 } - no templated max
110
123
  world.loadEntity(goblin.save());
111
124
  ```
112
125
 
126
+ An entity's `type` lives in shared memory (worker-visible), not as a plain field. Each distinct type string is
127
+ interned once as an immutable `ConstantString` in the heap — 15 `"Space Ship"` entities share one allocation —
128
+ and the `entity` block stores only a pointer to it at `TYPE_INDEX`. Reading `entity.components.entity.type`
129
+ resolves that pointer back to the string through the world's cache; a worker can do the same (see
130
+ [Reading an entity's type in a worker](#reading-an-entitys-type-in-a-worker)).
131
+
113
132
  ## Iterating entities
114
133
 
115
134
  `world.entities` is a `Map` keyed by `eid`, not an array, and so is `entities` on `EntitySystem` and
@@ -140,10 +159,10 @@ one: it is typed, it is readable, and one read costs nothing worth measuring.
140
159
 
141
160
  It is not free, though, and the cost shows up in exactly one situation — reading the same component off
142
161
  *thousands* of entities, *every frame*. The accessor walks several objects to get there and ends in a getter
143
- closure over the shared block, and because every entity has its own closure those call sites go megamorphic
144
- once enough entities are alive, so none of it inlines. Measured over ~10,000 entities, reading four values per
145
- entity cost **~950ns through the accessors against ~140ns straight off the block** the difference between
146
- 10ms a frame and 1.5ms.
162
+ call over the shared block. `Component`-subclass accessors are prototype getters (all instances of a type share
163
+ one hidden class, so those reads stay monomorphic and inline unlike the old per-entity closures, which went
164
+ megamorphic), but a getter call plus the property walk still loses to a raw indexed read in the very tightest
165
+ per-frame loops.
147
166
 
148
167
  Where that matters, hold the block instead. It is the same memory the accessors read, so nothing changes about
149
168
  what you get, and it is what the update functions already work on:
@@ -151,15 +170,15 @@ what you get, and it is what the update functions already work on:
151
170
  ```ts
152
171
  import { TRANSFORM_X_INDEX } from '@daneren2005/shared-memory-physics';
153
172
 
154
- // Resolve once, when whatever is doing the reading is set up.
155
- const health = entity.components.health!;
156
- const block = world.registry.health.memoryComponent.getBlock(health.index) as Int32Array;
173
+ // Resolve once, when whatever is doing the reading is set up. The component caches its view on `.block` when it
174
+ // is attached, so read that rather than paying for another getBlock (each call allocates a fresh subarray).
175
+ const block = entity.components.health!.block as Int32Array;
157
176
 
158
177
  // Then per frame, per entity:
159
178
  block[HEALTH_INDEX];
160
179
  ```
161
180
 
162
- Two things come with that. The block is only valid while the component is: resolve it again if the component
181
+ Two things come with that. The block is only valid while the component is: re-read `.block` if the component
163
182
  can be removed and re-added, or hang it off something that dies with the entity. And it is indexed rather than
164
183
  named, so the offsets have to be exported alongside the definition — which is why every component in the
165
184
  physics library exports its `*_INDEX` constants.
@@ -258,8 +277,8 @@ barrel: importing `createComponentWorker` from `@daneren2005/shared-memory-ecs`
258
277
  `BaseWorld`, every system, their `@daneren2005/shared-memory-objects` dependencies - into the worker, even
259
278
  though a worker never runs any of it (easily ~20kb of dead code per worker). Import worker-side helpers from
260
279
  the `@daneren2005/shared-memory-ecs/worker` subpath instead. It exposes only what runs in a worker -
261
- `createComponentWorker`, `createEntityWorker`, `killEntityWorker`, `DEAD_INDEX` (plus the worker-relevant
262
- types) - so the bundle stays tiny:
280
+ `createComponentWorker`, `createEntityWorker`, `killEntityWorker`, `DEAD_INDEX`, `TYPE_INDEX` (plus the
281
+ worker-relevant types) - so the bundle stays tiny:
263
282
 
264
283
  ```ts
265
284
  // damage.worker.ts - the worker entry file
@@ -274,12 +293,46 @@ or `killEntityWorker` should import them from `/worker` too. Type-only imports (
274
293
  `ComponentSystemWorld`, ...) can come from either path since types are erased, and main-thread code
275
294
  (`ComponentSystem`, `BaseWorld`, `EntityFactory`, ...) keeps importing from the package root.
276
295
 
296
+ A worker that creates entities (see [Creating entities from a worker](#creating-entities-from-a-worker)) passes
297
+ your component registry as the third argument - `createComponentWorker(self, shipUpdate, registry)` - so it has
298
+ each component's `toBlock`. Only do this in workers that actually create entities; it pulls the registry (and
299
+ whatever it imports) into that worker's bundle.
300
+
301
+ ### Reading an entity's type in a worker
302
+
303
+ The `entity` component carries the entity's `type` in shared memory as a pointer, so a worker can resolve it
304
+ back to a string. Pull `entity` into the system's query (its block only reaches the worker if it is `required`
305
+ or `optional`), read the pointer at `TYPE_INDEX`, and hand it to `world.getString` — the framework injects that
306
+ resolver on `world` before every run:
307
+
308
+ ```ts
309
+ import { TYPE_INDEX } from '@daneren2005/shared-memory-ecs/worker';
310
+ import type { EntityUpdateFunction } from '@daneren2005/shared-memory-ecs/worker';
311
+
312
+ const update: EntityUpdateFunction<Components, { entity: Uint32Array }> = (world, entityId, components) => {
313
+ const type = world.getString(components.entity[TYPE_INDEX]); // e.g. "Space Ship"
314
+ // ...branch on type, etc.
315
+ };
316
+
317
+ class TypedSystem extends ComponentSystem<Components, { entity: Uint32Array }> {
318
+ constructor(world: BaseWorld<Components>) {
319
+ super(world, { name: 'TypedSystem', required: ['entity'], updateFunction: update, getWorker: () => new Worker(/* ... */) });
320
+ }
321
+ }
322
+ ```
323
+
324
+ `world.getString(pointer)` is a Map lookup before it ever rebuilds the string from memory, and returns `''` for
325
+ the empty type or a pointer whose buffer has not synced to the worker yet. This works identically on the
326
+ main-thread fallback. It is not limited to type — any pointer to a `ConstantString` (via `world.constantStrings`
327
+ on the main thread) resolves the same way.
328
+
277
329
  ### Reporting back to the main thread
278
330
 
279
331
  An update function runs on shared memory, so anything it writes is already visible on the main thread. What
280
332
  it cannot do from there is touch the world, so the things that have to happen back on it go through
281
333
  `callbacks`: `entityComponentChanged` (emitted on the entity as `component-property-updated`), `entityDied`
282
- (as `death`), and `createEntity`. All of them are collected during the run and applied once it completes.
334
+ (as `death`), and `createEntity` (see [Creating entities from a worker](#creating-entities-from-a-worker)).
335
+ All of them are collected during the run and applied once it completes.
283
336
 
284
337
  `emitEntityEvent` is the escape hatch for an event of your own: name it whatever you like and give it
285
338
  whatever args suit it, and it is emitted on the entity under that name. It exists so a system does not have
@@ -334,6 +387,112 @@ thread already holds the values - sending them along would only pay to copy what
334
387
  System events are dispatched before the per-entity events of the same run, so an entity a run both moved and
335
388
  killed is still in the world when its move is reported.
336
389
 
390
+ ### Creating entities from a worker
391
+
392
+ An update function can spawn a whole entity, off-thread, from a factory config - the same
393
+ `{ type, ...overrides }` you would pass to `world.loadEntity`. Because component pools live in shared memory,
394
+ the worker merges the type's factory template, allocates each component's block and writes it, then reports
395
+ what it made; the main thread only wraps the result:
396
+
397
+ ```ts
398
+ // in the update function - spawn a ship, overriding two fields of its template
399
+ createEntityWorker(world, { type: 'ship', x: 100, y: 150 }, callbacks);
400
+ ```
401
+
402
+ `createEntityWorker` layers your overrides over the `ship` template, mints a unique id from a shared atomic
403
+ counter (so it never collides with one the main thread or another worker hands out), and for each component
404
+ the merged config triggers, pushes a block into its pool and writes the values - all off-thread. It reports
405
+ back an id-plus-block-indexes descriptor; when the run completes the main thread *adopts* it, building the
406
+ always-present `entity` component there (interning the `type` is a main-thread job) and wrapping each block the
407
+ worker wrote - no block is copied or re-allocated. Like every other worker report-back, the new entity first
408
+ exists on the following frame, so the system picks it up next run.
409
+
410
+ Two things make this work, both opt-in so only the workers that create entities pay for them:
411
+
412
+ - Register the system with `createsEntities: true`. That ships the factory templates to its worker on load.
413
+ - In that system's worker entry, pass your component registry to `createComponentWorker`, so the worker has
414
+ each component's block builder:
415
+
416
+ ```ts
417
+ createComponentWorker(self, shipUpdate, registry);
418
+ ```
419
+
420
+ Every component is already defined as two halves for exactly this — `toBlock(config)` (the block values) and
421
+ `attach(entity, memory, index)` (the accessor), see [Defining components](#defining-components). The worker
422
+ runs only `toBlock` (off-thread, no entity/world), and the main thread runs `attach` when it adopts the entity.
423
+ So creating a component in a worker needs nothing extra as long as its `toBlock` is genuinely pure over config —
424
+ no `entity`/`world` access:
425
+
426
+ ```ts
427
+ const shipHealth: ComponentDefinition<HealthComponent, Int32Array, HealthConfig> = {
428
+ // ...
429
+ toBlock(config) {
430
+ return [config.health ?? config.maxHealth, config.maxHealth];
431
+ },
432
+ attach(entity, memory, index) {
433
+ const block = memory.getBlock(index);
434
+ // ...build the accessor over `block`
435
+ },
436
+ };
437
+ ```
438
+
439
+ This runs identically on the main-thread fallback. Two current limits: a `loadInFinishLoading` component (one
440
+ that reads other entities) is skipped, since the worker has no world to read; and an adopted entity is always
441
+ the base `BaseEntity` - a factory per-type subclass is not applied to it.
442
+
443
+ ### Freeing component memory safely
444
+
445
+ Component blocks live in a shared pool, so a freed block gets handed straight back out to the next entity that
446
+ needs one. That is a problem across workers: if one system kills an entity, a second reuses its freed block for
447
+ a brand-new entity, and a third is still mid-run over what it thinks is the old entity, the third system writes
448
+ into the new entity's memory.
449
+
450
+ So the library never frees a block the instant it is orphaned. When an entity dies or you call
451
+ `removeComponent`, the block is *deferred* — the component is gone from the entity immediately, but the memory is
452
+ held until every system that could be mid-run over it has finished a run. Only then is the block returned to the
453
+ pool for reuse. This is automatic; there is nothing to call. The one visible effect is that
454
+ `memoryComponent.length` can briefly sit one higher than the number of live components, until the next update
455
+ lets the holding systems finish. If a system stays stuck (never completes a run) for more than ten seconds of
456
+ unscaled time, the world logs a warning naming that system and frees the blocks anyway rather than leak them —
457
+ a stuck system there is a bug worth chasing down.
458
+
459
+ ### Freeing extra resources
460
+
461
+ Everything above frees a component's own block. A component that allocates something *else* in `attach` —
462
+ another heap structure (a `SharedList`, a `SharedString`) or child entities it owns — needs to release that
463
+ too, and the block-level deferred free won't do it. Give the definition an optional `free(component)`:
464
+
465
+ ```ts
466
+ const cargoDefinition: ComponentDefinition<Cargo, Uint32Array, CargoConfig> = {
467
+ type: Uint32Array,
468
+ size: 3,
469
+ loadProperties: ['cargoSpace'],
470
+ toBlock(config) {
471
+ /* the block's own values */
472
+ },
473
+ attach(entity, memory, index) {
474
+ /* allocate a SharedList in the heap, stash its pointer in the block, return accessors */
475
+ },
476
+ free(component) {
477
+ component.items.free(); // release the SharedList + the item entities it holds
478
+ },
479
+ };
480
+ ```
481
+
482
+ `free` runs exactly once per component teardown: on `removeComponent`, and on every path that removes an
483
+ entity — `removeEntity`, a `death` (from `killEntity`), a `load` that drops the old batch, and `clear`.
484
+ It is deferred to the same safe point as the component's own block (see above), not fired the instant the
485
+ entity dies — so a system still mid-run over that memory can't see your resource released early. Reload calls
486
+ it without any `death` event, so it is the reliable place to avoid leaks when a world is reused.
487
+
488
+ ### Reusing a world: `load` and `clear`
489
+
490
+ A world is meant to be reused rather than rebuilt. `world.load(config)` swaps in a fresh scenario — it removes
491
+ the old entities, clears every system, and loads the new batch (deferred frees from the old contents drain over
492
+ the following updates, as above). `await world.clear()` instead tears the world all the way back down to an
493
+ empty, reusable state: it waits for every system's in-flight worker run to finish so nothing is still reading
494
+ the memory, frees the held blocks immediately, and resolves once the world is ready to load into again.
495
+
337
496
  ## Measuring performance
338
497
 
339
498
  `PerformanceTiming` watches a world and reports what running it costs. Hand it the world and it hooks itself
@@ -0,0 +1,13 @@
1
+ import type { WorkerAllocator, WorkerCreateEntityConfig, WorkerCreatedEntity } from '../systems/component-system';
2
+ export interface WorkerCreatableComponent {
3
+ loadProperties: Array<string>;
4
+ loadInFinishLoading?: boolean;
5
+ toBlock(config: Record<string, unknown>): Array<number>;
6
+ }
7
+ export type WorkerCreateRegistry = {
8
+ [name: string]: WorkerCreatableComponent;
9
+ };
10
+ export type FactoryConfigs = {
11
+ [type: string]: Record<string, unknown>;
12
+ };
13
+ export declare function buildWorkerEntity(config: WorkerCreateEntityConfig, factoryConfigs: FactoryConfigs, registry: WorkerCreateRegistry, allocator: WorkerAllocator): WorkerCreatedEntity;
@@ -1,2 +1,2 @@
1
- import type { ComponentSystemCallbacks, CreateEntityConfig } from '../systems/component-system';
2
- export default function createEntityWorker(config: CreateEntityConfig, callbacks: ComponentSystemCallbacks): void;
1
+ import type { ComponentSystemCallbacks, ComponentSystemWorld, WorkerCreateEntityConfig } from '../systems/component-system';
2
+ export default function createEntityWorker(world: ComponentSystemWorld, config: WorkerCreateEntityConfig, callbacks: ComponentSystemCallbacks): void;
@@ -5,6 +5,7 @@ import type BaseEntity from './entity';
5
5
  import type { EntityComponent, EntityComponentConfig, EntityComponentSerialization } from './entity-component';
6
6
  export interface BaseComponent {
7
7
  index: number;
8
+ block?: ComponentTypedArray;
8
9
  }
9
10
  export type ComponentMap = Record<string, BaseComponent>;
10
11
  export interface ComponentDefinition<Component extends BaseComponent, T extends ComponentTypedArray = ComponentTypedArray, Config = any, Serialization = object> {
@@ -12,8 +13,10 @@ export interface ComponentDefinition<Component extends BaseComponent, T extends
12
13
  size: number;
13
14
  loadProperties: Array<keyof Config & string>;
14
15
  loadInFinishLoading?: boolean;
15
- load(entity: BaseEntity, memory: MemoryComponent<T>, config: Config & Serialization): Component;
16
+ toBlock(config: Config & Serialization, entity?: BaseEntity): Array<number>;
17
+ attach(entity: BaseEntity, memory: MemoryComponent<T>, index: number): Component;
16
18
  save?(component: Component): Serialization;
19
+ free?(component: Component): void;
17
20
  }
18
21
  export type RegisteredComponentDefinition<Component extends BaseComponent, T extends ComponentTypedArray = ComponentTypedArray, Config = any, Serialization = object> = ComponentDefinition<Component, T, Config, Serialization> & {
19
22
  memoryComponent: MemoryComponent<T>;
@@ -28,9 +31,9 @@ export type ComponentDefinitionMap = Record<string, ComponentDefinition<BaseComp
28
31
  type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
29
32
  type DefinitionConfig<D> = D extends ComponentDefinition<BaseComponent, ComponentTypedArray, infer Config, infer Serialization> ? Config & Serialization : never;
30
33
  export type ComponentsOf<R extends ComponentDefinitionMap> = {
31
- [K in keyof R]: ReturnType<R[K]['load']>;
34
+ [K in keyof R]: ReturnType<R[K]['attach']> & BaseComponent;
32
35
  } & {
33
- entity: EntityComponent;
36
+ entity: EntityComponent & BaseComponent;
34
37
  };
35
38
  export type EntityConfigOf<R extends ComponentDefinitionMap> = Partial<UnionToIntersection<{
36
39
  [K in keyof R]: DefinitionConfig<R[K]>;
@@ -0,0 +1,7 @@
1
+ import type { ComponentTypedArray } from './memory-component';
2
+ import type { BaseComponent } from './component-definition';
3
+ export default abstract class Component<T extends ComponentTypedArray = ComponentTypedArray> implements BaseComponent {
4
+ readonly index: number;
5
+ readonly block: T;
6
+ constructor(block: T, index: number);
7
+ }
@@ -0,0 +1,11 @@
1
+ import ConstantString from '@daneren2005/shared-memory-objects/constant-string';
2
+ import type MemoryHeap from '@daneren2005/shared-memory-objects/memory-heap';
3
+ export default class ConstantStringCache {
4
+ private heap;
5
+ private byValue;
6
+ private byPointer;
7
+ constructor(heap: MemoryHeap);
8
+ getOrCreate(value: string): ConstantString;
9
+ getString(pointer: number): string | undefined;
10
+ clear(): void;
11
+ }
@@ -0,0 +1,263 @@
1
+ import e from "@daneren2005/shared-memory-objects/memory-heap";
2
+ import { getPointer as t } from "@daneren2005/shared-memory-objects/utils/pointer";
3
+ import n from "@daneren2005/shared-memory-objects/shared-pool";
4
+ import r from "@daneren2005/shared-memory-objects/constant-string";
5
+ //#region src/memory-component.ts
6
+ var i = class e {
7
+ heap;
8
+ pool;
9
+ constructor(e, t, r, i) {
10
+ this.heap = e, this.pool = i ? new n(e, i) : new n(e, {
11
+ type: t,
12
+ dataLength: r
13
+ });
14
+ }
15
+ static fromSharedMemory(t, n) {
16
+ return new e(t, Uint32Array, 1, n);
17
+ }
18
+ getSharedMemory() {
19
+ return this.pool.getSharedMemory();
20
+ }
21
+ get length() {
22
+ return this.pool.length;
23
+ }
24
+ get rawLength() {
25
+ return this.pool.bufferLength;
26
+ }
27
+ create(e) {
28
+ return this.pool.push(e);
29
+ }
30
+ getBlock(e) {
31
+ return this.pool.at(e);
32
+ }
33
+ get(e, t) {
34
+ return this.pool.get(e, t);
35
+ }
36
+ set(e, t, n) {
37
+ let r = this.pool.at(e);
38
+ r[t] = n;
39
+ }
40
+ delete(e) {
41
+ this.pool.deleteIndex(e);
42
+ }
43
+ clear() {
44
+ this.pool.clear();
45
+ }
46
+ }, a = class {
47
+ heap;
48
+ byValue = /* @__PURE__ */ new Map();
49
+ byPointer = /* @__PURE__ */ new Map();
50
+ constructor(e) {
51
+ this.heap = e;
52
+ }
53
+ getOrCreate(e) {
54
+ let t = this.byValue.get(e);
55
+ if (t) return t;
56
+ let n = new r(this.heap, e);
57
+ return this.byValue.set(e, n), this.byPointer.set(n.pointer, e), n;
58
+ }
59
+ getString(e) {
60
+ if (e === 0) return "";
61
+ let n = this.byPointer.get(e);
62
+ if (n !== void 0) return n;
63
+ let i = t(e);
64
+ if (this.heap.buffers[i.bufferPosition] === void 0) return;
65
+ let a = new r(this.heap, i).value;
66
+ return this.byPointer.set(e, a), a;
67
+ }
68
+ clear() {
69
+ this.byValue.forEach((e) => e.free()), this.byValue.clear(), this.byPointer.clear();
70
+ }
71
+ };
72
+ //#endregion
73
+ //#region src/systems/workers/apply-query-delta.ts
74
+ function o(e, t) {
75
+ if (t.removed.length) {
76
+ let n = new Set(t.removed);
77
+ e = e.filter((e) => !n.has(e.entityId));
78
+ }
79
+ if (t.added.length) {
80
+ let n = /* @__PURE__ */ new Map();
81
+ e.forEach((e, t) => n.set(e.entityId, t));
82
+ for (let r of t.added) {
83
+ let t = n.get(r.entityId);
84
+ t === void 0 ? (n.set(r.entityId, e.length), e.push(r)) : e[t] = r;
85
+ }
86
+ }
87
+ return e;
88
+ }
89
+ //#endregion
90
+ //#region src/actions/build-worker-entity.ts
91
+ function s(e, t, n, r) {
92
+ let i = {
93
+ ...t[e.type] ?? {},
94
+ ...e
95
+ }, a = r.allocateEid(), o = {};
96
+ for (let e of Object.keys(n)) {
97
+ if (e === "entity") continue;
98
+ let t = n[e];
99
+ t.loadInFinishLoading || t.loadProperties.some((e) => e in i) && (o[e] = r.allocateComponentBlock(e, t.toBlock(i)));
100
+ }
101
+ return {
102
+ eid: a,
103
+ type: e.type,
104
+ isStatic: i.isStatic,
105
+ components: o
106
+ };
107
+ }
108
+ //#endregion
109
+ //#region src/component.ts
110
+ var c = class {
111
+ index;
112
+ block;
113
+ constructor(e, t) {
114
+ this.block = e, this.index = t;
115
+ }
116
+ }, l = 0, u = 1, d = 2, f = class extends c {
117
+ cache;
118
+ constructor(e, t, n) {
119
+ super(e, t), this.cache = n;
120
+ }
121
+ get type() {
122
+ return this.cache.getString(this.block[2]) ?? "";
123
+ }
124
+ set type(e) {
125
+ this.block[2] = e ? this.cache.getOrCreate(e).pointer : 0;
126
+ }
127
+ get dead() {
128
+ return this.block[0] === 1;
129
+ }
130
+ set dead(e) {
131
+ this.block[0] = +!!e;
132
+ }
133
+ get isStatic() {
134
+ return this.block[1] === 1;
135
+ }
136
+ set isStatic(e) {
137
+ this.block[1] = +!!e;
138
+ }
139
+ }, p = {
140
+ type: Uint32Array,
141
+ size: 3,
142
+ loadProperties: ["type", "isStatic"],
143
+ toBlock(e, t) {
144
+ let n = t.world.constantStrings, r = e.type ? n.getOrCreate(e.type).pointer : 0;
145
+ return [
146
+ +!!e.dead,
147
+ +!!e.isStatic,
148
+ r
149
+ ];
150
+ },
151
+ attach(e, t, n) {
152
+ return new f(t.getBlock(n), n, e.world.constantStrings);
153
+ },
154
+ save(e) {
155
+ let t = {};
156
+ return e.type && (t.type = e.type), e.dead && (t.dead = !0), t;
157
+ }
158
+ };
159
+ //#endregion
160
+ //#region src/actions/kill-entity-worker.ts
161
+ function m(e, t, n) {
162
+ let r = t.entity;
163
+ r && (r[0] = 1), n.entityDied(e);
164
+ }
165
+ //#endregion
166
+ //#region src/actions/create-entity-worker.ts
167
+ function h(e, t, n) {
168
+ if (!e.buildEntityDescriptor) throw Error("createEntityWorker requires the system to be registered with createsEntities: true and the component registry passed to createComponentWorker");
169
+ n.createEntity(e.buildEntityDescriptor(t));
170
+ }
171
+ //#endregion
172
+ //#region src/systems/workers/create-component-worker.ts
173
+ function g(t, n, r) {
174
+ let c = [], l = {}, u, d, f, p = {}, m, h, g = (e) => f?.getString(e) ?? "";
175
+ t.onmessage = function(v) {
176
+ let y = v.data;
177
+ if (y.type === "init") _(t, { type: "init-complete" });
178
+ else if (y.type === "load") {
179
+ if (y.heap && (d = new e(y.heap), f = new a(d), d.addOnGrowBufferHandlers((e) => _(t, {
180
+ type: "grow-buffer-from-worker",
181
+ buffer: e
182
+ }))), d && y.sharedMemory) {
183
+ p = {};
184
+ for (let e of Object.keys(y.sharedMemory.components)) p[e] = i.fromSharedMemory(d, y.sharedMemory.components[e]);
185
+ m = d.getSharedAlloc(y.sharedMemory.eidCounter);
186
+ }
187
+ h = y.factoryConfigs, u = n.init?.(y.data) ?? void 0, _(t, { type: "loaded" });
188
+ } else if (y.type === "grow-buffer") d && d.buffers[y.buffer.bufferPosition] === void 0 && d.addSharedBuffer(y.buffer);
189
+ else if (y.type === "reset") {
190
+ c = [];
191
+ for (let e of Object.keys(l)) delete l[e];
192
+ u = void 0;
193
+ } else if (y.type === "run") {
194
+ u && Object.assign(y.world, u), y.world.getString = g;
195
+ let e = {
196
+ allocateEid: () => m ? Atomics.add(m.data, 0, 1) + 1 : 0,
197
+ allocateComponentBlock: (e, t) => p[e].create(t)
198
+ };
199
+ y.world.allocate = e, h && r && (y.world.buildEntityDescriptor = (t) => s(t, h, r, e));
200
+ let i = performance.now(), a = [], d = {}, f = [];
201
+ c = o(c, y.entities);
202
+ let v = {};
203
+ Object.entries(y.queries).forEach(([e, t]) => {
204
+ let n = o(l[e] ?? [], t);
205
+ l[e] = n, v[e] = n;
206
+ });
207
+ let b = {
208
+ entityComponentChanged(e, t, n, r) {
209
+ a.push({
210
+ entityId: e,
211
+ event: "component-property-updated",
212
+ args: [
213
+ t,
214
+ n,
215
+ r
216
+ ]
217
+ });
218
+ },
219
+ emitEntityEvent(e, t, ...n) {
220
+ a.push({
221
+ entityId: e,
222
+ event: t,
223
+ args: n
224
+ });
225
+ },
226
+ emitSystemEvent(e, t) {
227
+ (d[e] ?? (d[e] = [])).push(t);
228
+ },
229
+ entityDied(e) {
230
+ a.push({
231
+ entityId: e,
232
+ event: "death",
233
+ args: []
234
+ });
235
+ },
236
+ createEntity(e) {
237
+ f.push(e);
238
+ }
239
+ };
240
+ n.preRun && n.preRun(y.world, c, v, b), c.forEach((e) => {
241
+ n(y.world, e.entityId, e.components, v, b);
242
+ }), n.entityRemoved && y.entities.removed.forEach((e) => {
243
+ n.entityRemoved(y.world, e, b);
244
+ });
245
+ let x = performance.now() - i;
246
+ _(t, {
247
+ type: "run-complete",
248
+ generation: y.generation,
249
+ runTime: x,
250
+ events: a,
251
+ systemEvents: d,
252
+ created: f
253
+ });
254
+ }
255
+ };
256
+ }
257
+ function _(e, t) {
258
+ e.postMessage(t);
259
+ }
260
+ //#endregion
261
+ export { u as a, c, a as d, i as f, l as i, s as l, h as n, d as o, m as r, p as s, g as t, o as u };
262
+
263
+ //# sourceMappingURL=create-component-worker-CaB32Dgn.js.map