@daneren2005/shared-memory-ecs 1.2.0 → 1.2.2

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
@@ -20,7 +20,8 @@ fog of war, sub-classed entities, required components, etc).
20
20
  needs the `type` plus the entity's serialization. `BaseWorld#loadEntity` always goes through the factory.
21
21
  - **`BaseWorld<C>`** – builds one `MemoryComponent` per registered component (attached to that component's
22
22
  definition as `world.registry[name].memoryComponent`) and runs systems. It is generic over your component
23
- map `C`, so `world.registry`, `entity.components`, `setComponent`, etc. are fully typed.
23
+ map `C`, so `world.registry`, `entity.components`, `setComponent`, etc. are fully typed. Its entities live
24
+ in `world.entities`, a `Map` keyed by `eid` — see [Iterating entities](#iterating-entities).
24
25
  - **`BaseEntity<C>`** – an `eid`, an optional `id`, and a bag of memory-backed components. It has no
25
26
  direct property accessors and only loads/saves component data. Every entity has an `entity` component
26
27
  whose `type` (a plain, worker-invisible string) records the factory template it was built from.
@@ -109,10 +110,68 @@ console.log(goblin.save()); // { type: 'goblin', health: 10 } - no templated max
109
110
  world.loadEntity(goblin.save());
110
111
  ```
111
112
 
113
+ ## Iterating entities
114
+
115
+ `world.entities` is a `Map` keyed by `eid`, not an array, and so is `entities` on `EntitySystem` and
116
+ `ComponentSystem`:
117
+
118
+ ```ts
119
+ world.entities.forEach(entity => { ... }); // in the order they were added
120
+ world.entities.get(eid); // same as world.getEntityByEid(eid)
121
+ world.entities.size;
122
+ for(const entity of world.entities.values()) { ... }
123
+
124
+ // For the array methods a Map does not have:
125
+ Array.from(world.entities.values()).filter(entity => !!entity.components.health);
126
+ ```
127
+
128
+ An entity leaves from anywhere in the middle of that collection — every death is one — and finding it in an
129
+ array meant a scan of the whole world, then a shift of everything after it, repeated for each system that
130
+ held it as well. A few thousand entities with a few dozen deaths a run made that the most expensive thing the
131
+ main thread did on a busy frame; a `Map` deletes in constant time. It also replaced the separate
132
+ `entitiesByEid` lookup, so there is only ever one collection to keep straight.
133
+
134
+ Iteration order is still insertion order, so `load` / `save` round-trip in the order they always did.
135
+
136
+ ## Reading components on a hot path
137
+
138
+ `entity.components.health!.health` is the way to read a component, and for almost everything it is the right
139
+ one: it is typed, it is readable, and one read costs nothing worth measuring.
140
+
141
+ It is not free, though, and the cost shows up in exactly one situation — reading the same component off
142
+ *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.
147
+
148
+ Where that matters, hold the block instead. It is the same memory the accessors read, so nothing changes about
149
+ what you get, and it is what the update functions already work on:
150
+
151
+ ```ts
152
+ import { TRANSFORM_X_INDEX } from '@daneren2005/shared-memory-physics';
153
+
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;
157
+
158
+ // Then per frame, per entity:
159
+ block[HEALTH_INDEX];
160
+ ```
161
+
162
+ Two things come with that. The block is only valid while the component is: resolve it again if the component
163
+ can be removed and re-added, or hang it off something that dies with the entity. And it is indexed rather than
164
+ named, so the offsets have to be exported alongside the definition — which is why every component in the
165
+ physics library exports its `*_INDEX` constants.
166
+
167
+ Reach for this when a profile says to, not by default. A menu, a save, a system that touches a dozen entities:
168
+ use the accessors.
169
+
112
170
  ## ComponentSystem workers
113
171
 
114
172
  `ComponentSystem` needs a `getWorker()` that returns a real `Worker`, and an `updateFunction`. Your
115
- worker entry file calls `createComponentWorker(self, updateFunction)`. When Web Workers or
173
+ worker entry file calls `createComponentWorker(self, updateFunction)`, importing it from the
174
+ [`/worker` subpath](#importing-in-workers) so the worker bundle stays small. When Web Workers or
116
175
  `SharedArrayBuffer` are unavailable it transparently falls back to running the same update function on
117
176
  the main thread. Attach any extra per-run data (the equivalent of the old faction/fog-of-war fields)
118
177
  by overriding `addDataToWorld(world)`. Declare its shape with the `W` type parameter (an interface
@@ -131,6 +190,29 @@ class DamageSystem extends ComponentSystem<Components, { health: Int32Array }, D
131
190
  }
132
191
  ```
133
192
 
193
+ ### Importing in workers
194
+
195
+ Each worker entry file is bundled on its own, and a single-entry bundle cannot tree-shake this package's
196
+ barrel: importing `createComponentWorker` from `@daneren2005/shared-memory-ecs` drags the whole library -
197
+ `BaseWorld`, every system, their `@daneren2005/shared-memory-objects` dependencies - into the worker, even
198
+ though a worker never runs any of it (easily ~20kb of dead code per worker). Import worker-side helpers from
199
+ the `@daneren2005/shared-memory-ecs/worker` subpath instead. It exposes only what runs in a worker -
200
+ `createComponentWorker`, `createEntityWorker`, `killEntityWorker`, `DEAD_INDEX` (plus the worker-relevant
201
+ types) - so the bundle stays tiny:
202
+
203
+ ```ts
204
+ // damage.worker.ts - the worker entry file
205
+ import { createComponentWorker } from '@daneren2005/shared-memory-ecs/worker';
206
+ import { damageUpdate } from './damage-update';
207
+
208
+ createComponentWorker(self, damageUpdate);
209
+ ```
210
+
211
+ The same applies to any module the worker file pulls in: an update function that calls `createEntityWorker`
212
+ or `killEntityWorker` should import them from `/worker` too. Type-only imports (`EntityUpdateFunction`,
213
+ `ComponentSystemWorld`, ...) can come from either path since types are erased, and main-thread code
214
+ (`ComponentSystem`, `BaseWorld`, `EntityFactory`, ...) keeps importing from the package root.
215
+
134
216
  ### Reporting back to the main thread
135
217
 
136
218
  An update function runs on shared memory, so anything it writes is already visible on the main thread. What
@@ -156,6 +238,41 @@ no class instances. Nothing about the name or the args is checked against your c
156
238
  event is the system's own concept rather than a component, so export both alongside the update function that
157
239
  emits them.
158
240
 
241
+ ### Reporting something that happens to everything, every run
242
+
243
+ Every callback above costs an event object per entity: an allocation in the worker, a structured clone of it,
244
+ an eid lookup on the main thread, and an emit on that entity. That is fine for a death or a hit, which happen
245
+ to a handful of entities a run. It is not fine for a move, which happens to nearly all of them, every run -
246
+ at ten thousand entities that is ten thousand objects and ten thousand emits a frame, and it can easily cost
247
+ more than the simulation it is reporting.
248
+
249
+ `emitSystemEvent` is the version of that with everything avoidable taken out. Name the event and give it an
250
+ entity id; the whole run arrives on the **system** as one call with one array of ids:
251
+
252
+ ```ts
253
+ // in the update function - just the id, once per entity that moved
254
+ callbacks.emitSystemEvent('position-updated', entityId);
255
+
256
+ // on the main thread - one call for the run, however many entities are in it
257
+ system.on('position-updated', (entityIds: Array<number>) => {
258
+ for(const eid of entityIds) {
259
+ const sprite = sprites.get(eid);
260
+ // The worker wrote the position into shared memory, so it is already here to read.
261
+ if(sprite) {
262
+ const transform = world.getEntityByEid(eid)?.components.transform;
263
+ ...
264
+ }
265
+ }
266
+ });
267
+ ```
268
+
269
+ Nothing travels with the id on purpose. The blocks the update just wrote are shared memory, so the main
270
+ thread already holds the values - sending them along would only pay to copy what is already there. Reach for
271
+ `emitEntityEvent` when the thing you want to report is *not* in a component block, and for this when it is.
272
+
273
+ System events are dispatched before the per-entity events of the same run, so an entity a run both moved and
274
+ killed is still in the world when its move is reported.
275
+
159
276
  ## Measuring performance
160
277
 
161
278
  `PerformanceTiming` watches a world and reports what running it costs. Hand it the world and it hooks itself
@@ -1,4 +1,4 @@
1
- import type { TypedArrayConstructor } from '@daneren2005/shared-memory-objects';
1
+ import type { TypedArrayConstructor } from '@daneren2005/shared-memory-objects/interfaces/typed-array-constructor';
2
2
  import type MemoryComponent from './memory-component';
3
3
  import type { ComponentTypedArray } from './memory-component';
4
4
  import type BaseEntity from './entity';
@@ -0,0 +1,126 @@
1
+ //#region src/systems/workers/apply-query-delta.ts
2
+ function e(e, t) {
3
+ if (t.removed.length) {
4
+ let n = new Set(t.removed);
5
+ e = e.filter((e) => !n.has(e.entityId));
6
+ }
7
+ if (t.added.length) {
8
+ let n = /* @__PURE__ */ new Map();
9
+ e.forEach((e, t) => n.set(e.entityId, t));
10
+ for (let r of t.added) {
11
+ let t = n.get(r.entityId);
12
+ t === void 0 ? (n.set(r.entityId, e.length), e.push(r)) : e[t] = r;
13
+ }
14
+ }
15
+ return e;
16
+ }
17
+ //#endregion
18
+ //#region src/entity-component.ts
19
+ var t = 0, n = 1, r = {
20
+ type: Uint32Array,
21
+ size: 2,
22
+ loadProperties: ["type", "isStatic"],
23
+ load(e, t, n) {
24
+ let r = t.create([+!!n.dead, +!!n.isStatic]), i = t.getBlock(r);
25
+ return {
26
+ index: r,
27
+ type: n.type,
28
+ get dead() {
29
+ return i[0] === 1;
30
+ },
31
+ set dead(e) {
32
+ i[0] = +!!e;
33
+ },
34
+ get isStatic() {
35
+ return i[1] === 1;
36
+ },
37
+ set isStatic(e) {
38
+ i[1] = +!!e;
39
+ }
40
+ };
41
+ },
42
+ save(e) {
43
+ let t = {};
44
+ return e.type && (t.type = e.type), e.dead && (t.dead = !0), t;
45
+ }
46
+ };
47
+ //#endregion
48
+ //#region src/actions/kill-entity-worker.ts
49
+ function i(e, t, n) {
50
+ let r = t.entity;
51
+ r && (r[0] = 1), n.entityDied(e);
52
+ }
53
+ //#endregion
54
+ //#region src/actions/create-entity-worker.ts
55
+ function a(e, t) {
56
+ t.createEntity(e);
57
+ }
58
+ //#endregion
59
+ //#region src/systems/workers/create-component-worker.ts
60
+ function o(t, n) {
61
+ let r = [], i = {};
62
+ t.onmessage = function(a) {
63
+ let o = a.data;
64
+ if (o.type === "init") s(t, { type: "loaded" });
65
+ else if (o.type === "run") {
66
+ let a = performance.now(), c = [], l = {}, u = [];
67
+ r = e(r, o.entities);
68
+ let d = {};
69
+ Object.entries(o.queries).forEach(([t, n]) => {
70
+ let r = e(i[t] ?? [], n);
71
+ i[t] = r, d[t] = r;
72
+ });
73
+ let f = {
74
+ entityComponentChanged(e, t, n, r) {
75
+ c.push({
76
+ entityId: e,
77
+ event: "component-property-updated",
78
+ args: [
79
+ t,
80
+ n,
81
+ r
82
+ ]
83
+ });
84
+ },
85
+ emitEntityEvent(e, t, ...n) {
86
+ c.push({
87
+ entityId: e,
88
+ event: t,
89
+ args: n
90
+ });
91
+ },
92
+ emitSystemEvent(e, t) {
93
+ (l[e] ?? (l[e] = [])).push(t);
94
+ },
95
+ entityDied(e) {
96
+ c.push({
97
+ entityId: e,
98
+ event: "death",
99
+ args: []
100
+ });
101
+ },
102
+ createEntity(e) {
103
+ u.push(e);
104
+ }
105
+ };
106
+ n.preRun && n.preRun(o.world, r, d, f), r.forEach((e) => {
107
+ n(o.world, e.entityId, e.components, d, f);
108
+ }), n.entityRemoved && o.entities.removed.forEach((e) => {
109
+ n.entityRemoved(o.world, e, f);
110
+ }), s(t, {
111
+ type: "run-complete",
112
+ runTime: performance.now() - a,
113
+ events: c,
114
+ systemEvents: l,
115
+ created: u
116
+ });
117
+ }
118
+ };
119
+ }
120
+ function s(e, t) {
121
+ e.postMessage(t);
122
+ }
123
+ //#endregion
124
+ export { n as a, t as i, a as n, r as o, i as r, e as s, o as t };
125
+
126
+ //# sourceMappingURL=create-component-worker-Bd1BTUkq.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-component-worker-Bd1BTUkq.js","names":[],"sources":["../src/systems/workers/apply-query-delta.ts","../src/entity-component.ts","../src/actions/kill-entity-worker.ts","../src/actions/create-entity-worker.ts","../src/systems/workers/create-component-worker.ts"],"sourcesContent":["import type { EntityUpdateComponents, QueryDelta, UpdateEntityConfigObject } from '../component-system';\n\n// A worker keeps one persistent list per query and applies the delta each run carries: drop the entities that\n// left, then upsert the ones that joined (or whose component blocks changed). Because the main thread only\n// sends changes, a steady-state run - where membership is unchanged - carries empty arrays and this returns the\n// existing list untouched with no allocation, which is the whole point of the delta protocol: we stop\n// re-transmitting (and re-cloning) every entity id every single frame.\n//\n// The list is returned (rather than mutated in place) because removals rebuild it via filter; callers must store\n// the returned reference back as their persistent list.\nexport function applyQueryDelta<T extends EntityUpdateComponents>(\n\tlist: Array<UpdateEntityConfigObject<T>>,\n\tdelta: QueryDelta<T>,\n): Array<UpdateEntityConfigObject<T>> {\n\tif(delta.removed.length) {\n\t\tconst removedSet = new Set(delta.removed);\n\t\tlist = list.filter(entry => !removedSet.has(entry.entityId));\n\t}\n\n\tif(delta.added.length) {\n\t\t// Map existing members so a re-added entity (its component set changed) replaces its entry in place\n\t\t// instead of being duplicated; genuinely new entities are appended.\n\t\tconst indexByEid = new Map<number, number>();\n\t\tlist.forEach((entry, index) => indexByEid.set(entry.entityId, index));\n\n\t\tfor(let entity of delta.added) {\n\t\t\tconst existing = indexByEid.get(entity.entityId);\n\t\t\tif(existing !== undefined) {\n\t\t\t\tlist[existing] = entity;\n\t\t\t} else {\n\t\t\t\tindexByEid.set(entity.entityId, list.length);\n\t\t\t\tlist.push(entity);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn list;\n}\n","import type { ComponentDefinition } from './component-definition';\n\n// The one component every entity is required to have. `dead` and `isStatic` are boolean flags stored in\n// the shared-memory block so a worker that pulls the `entity` component into its query can read them too.\n// `type` is a plain string that names the EntityFactory template this entity was built from; it lives off\n// to the side (not in shared memory), so - unlike `dead`/`isStatic` - it is NOT visible to workers.\n// Games that need a stable string id (or any other per-entity data) add their own component for it.\nexport interface EntityComponent {\n\tindex: number\n\ttype: string\n\tdead: boolean\n\tisStatic: boolean\n}\n\n// The defining config for the entity component. `type` is required - every entity is created from a factory\n// template, named by its type - while `isStatic` is an optional flag supplied up front.\nexport interface EntityComponentConfig {\n\ttype: string\n\tisStatic?: boolean\n}\n\n// Runtime-derived state plus `type`: `dead` is only meaningful once the entity is alive, and `type` is kept\n// so a save can be re-expanded through the factory. `isStatic` is intentionally absent - it comes back from\n// the type's template config, not the save.\nexport interface EntityComponentSerialization {\n\ttype?: string\n\tdead?: boolean\n}\n\nexport const DEAD_INDEX = 0;\nexport const STATIC_INDEX = 1;\n\nexport const entityDefinition: ComponentDefinition<EntityComponent, Uint32Array, EntityComponentConfig, EntityComponentSerialization> = {\n\ttype: Uint32Array,\n\tsize: 2,\n\t// The entity component is always loaded by BaseEntity's constructor, so this never actually gates\n\t// loading; it documents the defining config props and keeps it consistent with game components.\n\tloadProperties: ['type', 'isStatic'],\n\tload(entity, memory, config) {\n\t\tconst index = memory.create([config.dead ? 1 : 0, config.isStatic ? 1 : 0]);\n\t\tconst block = memory.getBlock(index);\n\n\t\treturn {\n\t\t\tindex,\n\t\t\t// Plain (non-memory) property, so it is main-thread only and not readable from workers.\n\t\t\ttype: config.type,\n\t\t\tget dead() {\n\t\t\t\treturn block[DEAD_INDEX] === 1;\n\t\t\t},\n\t\t\tset dead(value: boolean) {\n\t\t\t\tblock[DEAD_INDEX] = value ? 1 : 0;\n\t\t\t},\n\t\t\tget isStatic() {\n\t\t\t\treturn block[STATIC_INDEX] === 1;\n\t\t\t},\n\t\t\tset isStatic(value: boolean) {\n\t\t\t\tblock[STATIC_INDEX] = value ? 1 : 0;\n\t\t\t},\n\t\t};\n\t},\n\tsave(component) {\n\t\t// `type` + `dead` are serialized; `isStatic` (and the rest of the template) come back from the type's\n\t\t// config on reload, so they are deliberately left out.\n\t\tconst config: EntityComponentSerialization = {};\n\t\tif(component.type) {\n\t\t\tconfig.type = component.type;\n\t\t}\n\t\tif(component.dead) {\n\t\t\tconfig.dead = true;\n\t\t}\n\n\t\treturn config;\n\t},\n};\n","import type { ComponentTypedArray } from '../memory-component';\nimport type { ComponentSystemCallbacks, EntityUpdateComponents } from '../systems/component-system';\nimport { DEAD_INDEX } from '../entity-component';\n\n// The worker-thread counterpart of killEntity. Called from inside a component system's update function,\n// it flags the entity dead directly in its shared-memory block and reports the death back to the main\n// thread through the callbacks so the world can run the same cleanup killEntity would. The entity\n// component must be part of the system's query for its block to be available here.\nexport default function killEntityWorker(entityId: number, components: EntityUpdateComponents, callbacks: ComponentSystemCallbacks): void {\n\tconst block = (components as { entity?: ComponentTypedArray }).entity;\n\tif(block) {\n\t\tblock[DEAD_INDEX] = 1;\n\t}\n\n\tcallbacks.entityDied(entityId);\n}\n","import type { ComponentSystemCallbacks, CreateEntityConfig } from '../systems/component-system';\n\n// The worker-thread counterpart of loadEntity. Called from inside a component system's update function, it\n// can't build the entity itself (eid generation, shared-memory allocation, and factory expansion all live on\n// the main thread), so it buffers the flat config through the callbacks. The main thread creates the entity\n// via world.loadEntity once the run completes, meaning it first exists on the following frame.\nexport default function createEntityWorker(config: CreateEntityConfig, callbacks: ComponentSystemCallbacks): void {\n\tcallbacks.createEntity(config);\n}\n","import type ComponentWorkerMessage from './component-worker-message';\nimport type { EntityEvent, SystemEvents } from './component-worker-message';\nimport type { ComponentMap } from '../../component-definition';\nimport type { ComponentSystemCallbacks, ComponentSystemWorld, EntityQueryComponents, EntityUpdateComponents, EntityUpdateFunction, QueryDelta, UpdateEntityConfigObject } from '../component-system';\nimport { applyQueryDelta } from './apply-query-delta';\n\n// The slice of the Web Worker global scope createComponentWorker actually touches. Worker files pass the\n// real `self` (e.g. `createComponentWorker(self, fn)`); passing it explicitly also lets test runners that\n// inject `self` as a module local rather than a true global - like @vitest/web-worker - drive the worker.\nexport interface ComponentWorkerScope {\n\tonmessage: ((e: MessageEvent) => void) | null\n\tpostMessage(message: ComponentWorkerMessage): void\n}\n\n// Entry point a game's worker file calls with its update function. It wires up `scope.onmessage`,\n// caches component blocks by entity id (so subsequent runs only need the id), and posts results back.\nexport default function createComponentWorker<\n\tC extends ComponentMap,\n\tT extends EntityUpdateComponents<C>,\n\tW extends ComponentSystemWorld = ComponentSystemWorld,\n>(scope: ComponentWorkerScope, updateFunction: EntityUpdateFunction<C, T, W>) {\n\t// The worker's persistent iteration lists. The main thread now sends only membership changes, so these are\n\t// carried across runs and mutated by the deltas each run brings (see applyQueryDelta).\n\tlet entities: Array<UpdateEntityConfigObject<T>> = [];\n\tconst queryEntities: { [key: string]: Array<UpdateEntityConfigObject<T>> } = {};\n\n\tscope.onmessage = function(e) {\n\t\tconst message = e.data as ComponentWorkerMessage<W>;\n\n\t\tif(message.type === 'init') {\n\t\t\tpostMessageTyped(scope, {\n\t\t\t\ttype: 'loaded',\n\t\t\t});\n\t\t} else if(message.type === 'run') {\n\t\t\tconst start = performance.now();\n\t\t\tlet entityEvents: Array<EntityEvent> = [];\n\t\t\tlet systemEvents: SystemEvents = {};\n\t\t\tlet createdEntities: Array<Record<string, unknown>> = [];\n\n\t\t\tentities = applyQueryDelta(entities, message.entities as QueryDelta<T>);\n\n\t\t\tlet queries: EntityQueryComponents<C> = {};\n\t\t\tObject.entries(message.queries).forEach(([queryKey, delta]) => {\n\t\t\t\tconst list = applyQueryDelta(queryEntities[queryKey] ?? [], delta as QueryDelta<T>);\n\t\t\t\tqueryEntities[queryKey] = list;\n\t\t\t\tqueries[queryKey] = list;\n\t\t\t});\n\n\t\t\tlet callbacks: ComponentSystemCallbacks<C> = {\n\t\t\t\tentityComponentChanged<K extends keyof C, P extends keyof C[K]>(entityId: number, componentName: K, prop: P, value: C[K][P]) {\n\t\t\t\t\tentityEvents.push({\n\t\t\t\t\t\tentityId,\n\t\t\t\t\t\tevent: 'component-property-updated',\n\t\t\t\t\t\targs: [componentName, prop, value],\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\temitEntityEvent(entityId: number, event: string, ...args: Array<unknown>) {\n\t\t\t\t\tentityEvents.push({\n\t\t\t\t\t\tentityId,\n\t\t\t\t\t\tevent,\n\t\t\t\t\t\targs,\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\temitSystemEvent(event: string, entityId: number) {\n\t\t\t\t\t// The list per event name is only made the first time that event comes up in a run, so a system\n\t\t\t\t\t// that has an event it rarely reports does not pay an empty array for it every run.\n\t\t\t\t\t(systemEvents[event] ?? (systemEvents[event] = [])).push(entityId);\n\t\t\t\t},\n\t\t\t\tentityDied(entityId: number) {\n\t\t\t\t\tentityEvents.push({\n\t\t\t\t\t\tentityId,\n\t\t\t\t\t\tevent: 'death',\n\t\t\t\t\t\targs: [],\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\tcreateEntity(config: Record<string, unknown>) {\n\t\t\t\t\tcreatedEntities.push(config);\n\t\t\t\t},\n\t\t\t};\n\t\t\tif(updateFunction.preRun) {\n\t\t\t\tupdateFunction.preRun(message.world, entities, queries, callbacks);\n\t\t\t}\n\n\t\t\tentities.forEach(entity => {\n\t\t\t\tupdateFunction(message.world, entity.entityId, entity.components, queries, callbacks);\n\t\t\t});\n\n\t\t\t// applyQueryDelta already dropped the removed entities from the persistent lists above; the hook just\n\t\t\t// lets the update function react to entities that left the system's main query.\n\t\t\tif(updateFunction.entityRemoved) {\n\t\t\t\tmessage.entities.removed.forEach(entityId => {\n\t\t\t\t\tupdateFunction.entityRemoved!(message.world, entityId, callbacks);\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst runTime = performance.now() - start;\n\n\t\t\tpostMessageTyped(scope, {\n\t\t\t\ttype: 'run-complete',\n\t\t\t\trunTime,\n\t\t\t\tevents: entityEvents,\n\t\t\t\tsystemEvents,\n\t\t\t\tcreated: createdEntities,\n\t\t\t});\n\t\t}\n\t};\n}\n\nfunction postMessageTyped(scope: ComponentWorkerScope, message: ComponentWorkerMessage) {\n\tscope.postMessage(message);\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -24,4 +24,4 @@ export { default as WebWorker } from './systems/workers/web-worker';
24
24
  export { default as ComponentWebWorker } from './systems/workers/component-web-worker';
25
25
  export { default as createComponentWorker } from './systems/workers/create-component-worker';
26
26
  export type { default as ComponentWorkerMessage } from './systems/workers/component-worker-message';
27
- export type { EntityEvent } from './systems/workers/component-worker-message';
27
+ export type { EntityEvent, SystemEvents } from './systems/workers/component-worker-message';