@ecsia/core 0.1.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/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/bitmask/bitmask.d.ts +21 -0
- package/dist/bitmask/bitmask.d.ts.map +1 -0
- package/dist/bitmask/bitmask.js +103 -0
- package/dist/bitmask/bitmask.js.map +1 -0
- package/dist/bitmask/index.d.ts +3 -0
- package/dist/bitmask/index.d.ts.map +1 -0
- package/dist/bitmask/index.js +2 -0
- package/dist/bitmask/index.js.map +1 -0
- package/dist/component/accessor.d.ts +40 -0
- package/dist/component/accessor.d.ts.map +1 -0
- package/dist/component/accessor.js +220 -0
- package/dist/component/accessor.js.map +1 -0
- package/dist/component/column-set.d.ts +20 -0
- package/dist/component/column-set.d.ts.map +1 -0
- package/dist/component/column-set.js +60 -0
- package/dist/component/column-set.js.map +1 -0
- package/dist/component/define.d.ts +23 -0
- package/dist/component/define.d.ts.map +1 -0
- package/dist/component/define.js +155 -0
- package/dist/component/define.js.map +1 -0
- package/dist/component/descriptors.d.ts +3 -0
- package/dist/component/descriptors.d.ts.map +1 -0
- package/dist/component/descriptors.js +147 -0
- package/dist/component/descriptors.js.map +1 -0
- package/dist/component/index.d.ts +10 -0
- package/dist/component/index.d.ts.map +1 -0
- package/dist/component/index.js +6 -0
- package/dist/component/index.js.map +1 -0
- package/dist/component/sidecar.d.ts +58 -0
- package/dist/component/sidecar.d.ts.map +1 -0
- package/dist/component/sidecar.js +136 -0
- package/dist/component/sidecar.js.map +1 -0
- package/dist/config.d.ts +55 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +70 -0
- package/dist/config.js.map +1 -0
- package/dist/entity/codec.d.ts +45 -0
- package/dist/entity/codec.d.ts.map +1 -0
- package/dist/entity/codec.js +53 -0
- package/dist/entity/codec.js.map +1 -0
- package/dist/entity/index-allocator.d.ts +46 -0
- package/dist/entity/index-allocator.d.ts.map +1 -0
- package/dist/entity/index-allocator.js +121 -0
- package/dist/entity/index-allocator.js.map +1 -0
- package/dist/entity/index.d.ts +13 -0
- package/dist/entity/index.d.ts.map +1 -0
- package/dist/entity/index.js +7 -0
- package/dist/entity/index.js.map +1 -0
- package/dist/entity/record.d.ts +28 -0
- package/dist/entity/record.d.ts.map +1 -0
- package/dist/entity/record.js +42 -0
- package/dist/entity/record.js.map +1 -0
- package/dist/entity/ref.d.ts +70 -0
- package/dist/entity/ref.d.ts.map +1 -0
- package/dist/entity/ref.js +104 -0
- package/dist/entity/ref.js.map +1 -0
- package/dist/entity/reservation.d.ts +12 -0
- package/dist/entity/reservation.d.ts.map +1 -0
- package/dist/entity/reservation.js +28 -0
- package/dist/entity/reservation.js.map +1 -0
- package/dist/entity/store.d.ts +60 -0
- package/dist/entity/store.d.ts.map +1 -0
- package/dist/entity/store.js +193 -0
- package/dist/entity/store.js.map +1 -0
- package/dist/env.d.ts +2 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +12 -0
- package/dist/env.js.map +1 -0
- package/dist/ids.d.ts +9 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +8 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect-surface.d.ts +27 -0
- package/dist/inspect-surface.d.ts.map +1 -0
- package/dist/inspect-surface.js +14 -0
- package/dist/inspect-surface.js.map +1 -0
- package/dist/internal.d.ts +19 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +19 -0
- package/dist/internal.js.map +1 -0
- package/dist/memory/allocU32.d.ts +25 -0
- package/dist/memory/allocU32.d.ts.map +1 -0
- package/dist/memory/allocU32.js +95 -0
- package/dist/memory/allocU32.js.map +1 -0
- package/dist/memory/buffers.d.ts +94 -0
- package/dist/memory/buffers.d.ts.map +1 -0
- package/dist/memory/buffers.js +308 -0
- package/dist/memory/buffers.js.map +1 -0
- package/dist/memory/index.d.ts +7 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +4 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/layout.d.ts +37 -0
- package/dist/memory/layout.d.ts.map +1 -0
- package/dist/memory/layout.js +116 -0
- package/dist/memory/layout.js.map +1 -0
- package/dist/query/compile.d.ts +73 -0
- package/dist/query/compile.d.ts.map +1 -0
- package/dist/query/compile.js +158 -0
- package/dist/query/compile.js.map +1 -0
- package/dist/query/engine.d.ts +48 -0
- package/dist/query/engine.d.ts.map +1 -0
- package/dist/query/engine.js +230 -0
- package/dist/query/engine.js.map +1 -0
- package/dist/query/index.d.ts +8 -0
- package/dist/query/index.d.ts.map +1 -0
- package/dist/query/index.js +10 -0
- package/dist/query/index.js.map +1 -0
- package/dist/query/live-query.d.ts +122 -0
- package/dist/query/live-query.d.ts.map +1 -0
- package/dist/query/live-query.js +543 -0
- package/dist/query/live-query.js.map +1 -0
- package/dist/query/sparse-set.d.ts +18 -0
- package/dist/query/sparse-set.d.ts.map +1 -0
- package/dist/query/sparse-set.js +126 -0
- package/dist/query/sparse-set.js.map +1 -0
- package/dist/reactivity/change-version.d.ts +19 -0
- package/dist/reactivity/change-version.d.ts.map +1 -0
- package/dist/reactivity/change-version.js +76 -0
- package/dist/reactivity/change-version.js.map +1 -0
- package/dist/reactivity/index.d.ts +12 -0
- package/dist/reactivity/index.d.ts.map +1 -0
- package/dist/reactivity/index.js +12 -0
- package/dist/reactivity/index.js.map +1 -0
- package/dist/reactivity/log.d.ts +83 -0
- package/dist/reactivity/log.d.ts.map +1 -0
- package/dist/reactivity/log.js +260 -0
- package/dist/reactivity/log.js.map +1 -0
- package/dist/reactivity/observer-commands.d.ts +40 -0
- package/dist/reactivity/observer-commands.d.ts.map +1 -0
- package/dist/reactivity/observer-commands.js +111 -0
- package/dist/reactivity/observer-commands.js.map +1 -0
- package/dist/reactivity/observers.d.ts +50 -0
- package/dist/reactivity/observers.d.ts.map +1 -0
- package/dist/reactivity/observers.js +127 -0
- package/dist/reactivity/observers.js.map +1 -0
- package/dist/reactivity/reactivity.d.ts +141 -0
- package/dist/reactivity/reactivity.d.ts.map +1 -0
- package/dist/reactivity/reactivity.js +479 -0
- package/dist/reactivity/reactivity.js.map +1 -0
- package/dist/reactivity/structural-journal.d.ts +30 -0
- package/dist/reactivity/structural-journal.d.ts.map +1 -0
- package/dist/reactivity/structural-journal.js +77 -0
- package/dist/reactivity/structural-journal.js.map +1 -0
- package/dist/registry.d.ts +26 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +58 -0
- package/dist/registry.js.map +1 -0
- package/dist/serialize-surface.d.ts +170 -0
- package/dist/serialize-surface.d.ts.map +1 -0
- package/dist/serialize-surface.js +6 -0
- package/dist/serialize-surface.js.map +1 -0
- package/dist/storage/archetype.d.ts +38 -0
- package/dist/storage/archetype.d.ts.map +1 -0
- package/dist/storage/archetype.js +47 -0
- package/dist/storage/archetype.js.map +1 -0
- package/dist/storage/cold-store.d.ts +41 -0
- package/dist/storage/cold-store.d.ts.map +1 -0
- package/dist/storage/cold-store.js +100 -0
- package/dist/storage/cold-store.js.map +1 -0
- package/dist/storage/index.d.ts +10 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/signature.d.ts +27 -0
- package/dist/storage/signature.d.ts.map +1 -0
- package/dist/storage/signature.js +115 -0
- package/dist/storage/signature.js.map +1 -0
- package/dist/storage/storage.d.ts +72 -0
- package/dist/storage/storage.d.ts.map +1 -0
- package/dist/storage/storage.js +192 -0
- package/dist/storage/storage.js.map +1 -0
- package/dist/storage/store.d.ts +88 -0
- package/dist/storage/store.d.ts.map +1 -0
- package/dist/storage/store.js +473 -0
- package/dist/storage/store.js.map +1 -0
- package/dist/util/stable-index.d.ts +29 -0
- package/dist/util/stable-index.d.ts.map +1 -0
- package/dist/util/stable-index.js +51 -0
- package/dist/util/stable-index.js.map +1 -0
- package/dist/world.d.ts +262 -0
- package/dist/world.d.ts.map +1 -0
- package/dist/world.js +831 -0
- package/dist/world.js.map +1 -0
- package/package.json +52 -0
package/dist/world.js
ADDED
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
// The World keystone: option resolution, the phase/tick contracts, and the module-wiring seam.
|
|
2
|
+
// The seven owning modules attach in the fixed order
|
|
3
|
+
// registry → buffers → storage → reactivity → queries → scheduler → serialization.
|
|
4
|
+
import { resolveOptions } from './config.js';
|
|
5
|
+
import { EntityStore, handleIndex, makeHandleLayout, reserveEntityBlock, returnReservedIds, } from './entity/index.js';
|
|
6
|
+
import { Buffers, probeCapabilities } from './memory/index.js';
|
|
7
|
+
import { ComponentRegistry } from './registry.js';
|
|
8
|
+
import { SidecarStore, sidecarKey } from './component/index.js';
|
|
9
|
+
import { Bitmask } from './bitmask/index.js';
|
|
10
|
+
import { Storage } from './storage/index.js';
|
|
11
|
+
import { QueryEngine } from './query/index.js';
|
|
12
|
+
import { ShapeKind } from './reactivity/index.js';
|
|
13
|
+
import { Reactivity, ObserverCommandBuffer, onAdd, onRemove, onChange } from './reactivity/index.js';
|
|
14
|
+
import { IS_DEV } from './env.js';
|
|
15
|
+
import { isSharedBacking } from './memory/buffers.js';
|
|
16
|
+
/**
|
|
17
|
+
* The only world constructor. Resolves and validates options fail-fast, then
|
|
18
|
+
* (at later milestones) probes capabilities, allocates bounded buffers, and wires the owning
|
|
19
|
+
* modules. Returns a frozen World facade.
|
|
20
|
+
*/
|
|
21
|
+
export function createWorld(options = {}) {
|
|
22
|
+
const resolved = resolveOptions(options);
|
|
23
|
+
// --- Module wiring seam ---
|
|
24
|
+
// registry → buffers → storage → reactivity → queries → scheduler → serialization.
|
|
25
|
+
// The entity layer lands first; later layers fill in around it.
|
|
26
|
+
const state = { phase: 'serial', tick: 0 };
|
|
27
|
+
const handleLayout = makeHandleLayout(resolved.generationBits);
|
|
28
|
+
const entities = new EntityStore({
|
|
29
|
+
layout: handleLayout,
|
|
30
|
+
maxEntities: resolved.maxEntities,
|
|
31
|
+
shared: resolved.threaded,
|
|
32
|
+
});
|
|
33
|
+
// --- buffers: one capability probe, one SAB-vs-AB decision ---
|
|
34
|
+
const workerMode = resolved.threaded
|
|
35
|
+
? resolved.scheduler.workers === 'no-sab'
|
|
36
|
+
? 'no-sab'
|
|
37
|
+
: 'auto'
|
|
38
|
+
: 'single';
|
|
39
|
+
const capabilities = probeCapabilities(workerMode, (message) => {
|
|
40
|
+
if (typeof console !== 'undefined')
|
|
41
|
+
console.warn(`[ecsia] ${message}`);
|
|
42
|
+
});
|
|
43
|
+
const buffers = new Buffers({ capabilities, maxEntities: resolved.maxEntities });
|
|
44
|
+
// The accessor seam: a setter calls world.trackWrite. handleIndex strips the generation so the LOW
|
|
45
|
+
// handle bits index the write log. Routes to the reactivity module once built
|
|
46
|
+
// (late-bound so the acyclic construction order registry → buffers → storage → reactivity holds).
|
|
47
|
+
let reactivity = null;
|
|
48
|
+
const trackWrite = (index, componentId, fieldIndex) => {
|
|
49
|
+
reactivity?.trackWrite(index, componentId, fieldIndex);
|
|
50
|
+
};
|
|
51
|
+
// The shared "any write consumer exists" cell the accessor setters read to fast-out the trackWrite
|
|
52
|
+
// chain. Reactivity owns recomputing `.active` on every flavor/observer/changeVersion
|
|
53
|
+
// (de)registration; until reactivity is wired it stays false (no consumers can exist yet).
|
|
54
|
+
const tracking = { active: false };
|
|
55
|
+
// The rich-field sidecar. Created BEFORE accessorWorld so the accessor's rich
|
|
56
|
+
// getters/setters can delegate through the seam. Main-thread-only; never shared with workers.
|
|
57
|
+
const sidecar = new SidecarStore();
|
|
58
|
+
// During an observer drain the rich getter must read the DYING entity's value (RF-REMOVE-READ); the
|
|
59
|
+
// sidecar disambiguates via its pending-clear table, so route reads through readForObserver while a
|
|
60
|
+
// pending window is open and through the generation-guarded read otherwise.
|
|
61
|
+
const accessorWorld = {
|
|
62
|
+
trackWrite,
|
|
63
|
+
tracking,
|
|
64
|
+
handleIndex: (handle) => handleIndex(handle, handleLayout),
|
|
65
|
+
sidecarRead: (key, index, gen) => sidecar.hasPending() ? sidecar.readForObserver(key, index, gen) : sidecar.read(key, index, gen),
|
|
66
|
+
sidecarWrite: (key, index, gen, value) => sidecar.write(key, index, gen, value),
|
|
67
|
+
generationOf: (index) => entities.decodeHandle(entities.handleOfIndex(index)).generation,
|
|
68
|
+
};
|
|
69
|
+
// --- registry: mint dense user ids, wire accessor factories ---
|
|
70
|
+
const registry = new ComponentRegistry();
|
|
71
|
+
registry.register(resolved.components);
|
|
72
|
+
// Declare a sidecar column per rich field of every registered component. This
|
|
73
|
+
// is the single place the sidecar learns about a rich column; ensureColumn is idempotent.
|
|
74
|
+
for (const def of resolved.components) {
|
|
75
|
+
const id = registry.idOf(def);
|
|
76
|
+
if (id === undefined)
|
|
77
|
+
continue;
|
|
78
|
+
let fieldIndex = 0;
|
|
79
|
+
for (const f of def.fields) {
|
|
80
|
+
if (f.rich !== undefined)
|
|
81
|
+
sidecar.ensureColumn(sidecarKey(id, fieldIndex), f.rich, f.default);
|
|
82
|
+
fieldIndex += 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// --- bitmask + storage: the per-entity membership index and the
|
|
86
|
+
// archetype tables. The bitmask stride = ceil(nextComponentId/32); both derive from the SAME
|
|
87
|
+
// registered-component count so sigWords and bitmask layouts align. Structural mutation is
|
|
88
|
+
// serial; the bitmask asserts world.phase === 'serial'.
|
|
89
|
+
const bitmask = new Bitmask(buffers, registry.nextComponentId, resolved.maxEntities, () => state.phase);
|
|
90
|
+
const stride = bitmask.stride;
|
|
91
|
+
const records = entities.records;
|
|
92
|
+
const handleIndexOf = (handle) => handleIndex(handle, handleLayout);
|
|
93
|
+
// Relations attach late: the pair-resolver feeds the query compiler, the preDespawn hook runs
|
|
94
|
+
// before storage tears the row down, and the apply-pair fns back __apply for the scheduler. All are
|
|
95
|
+
// mutable so the relations runtime injects them post-construction without a core→relations import.
|
|
96
|
+
let pairResolver = null;
|
|
97
|
+
let preDespawnHook = null;
|
|
98
|
+
let applyAddPair = null;
|
|
99
|
+
let applyRemovePair = null;
|
|
100
|
+
let serializationProvider = null;
|
|
101
|
+
// The query engine is created AFTER storage (it subscribes to storage.onArchetypeCreated), but
|
|
102
|
+
// storage's single-entity maintenance hooks must call into the engine. Late-bind through a mutable
|
|
103
|
+
// reference so the wiring stays acyclic (storage → engine, set once below).
|
|
104
|
+
let engine = null;
|
|
105
|
+
const storage = new Storage({
|
|
106
|
+
buffers,
|
|
107
|
+
accessorWorld,
|
|
108
|
+
bitmask,
|
|
109
|
+
record: records,
|
|
110
|
+
registry,
|
|
111
|
+
maxHotArchetypes: resolved.maxHotArchetypes,
|
|
112
|
+
stride,
|
|
113
|
+
maxEntities: resolved.maxEntities,
|
|
114
|
+
enqueueRemoveLog: (index, c) => reactivity?.enqueueRemoveLog(index, c),
|
|
115
|
+
hasRemoveObserver: (c) => reactivity?.hasRemoveObserver(c) ?? false,
|
|
116
|
+
trackShape: (index, c, kind) => reactivity?.trackShape(index, c, kind),
|
|
117
|
+
maintainEntity: (index, c) => engine?.maintainEntity(index, c),
|
|
118
|
+
onEntitySpawned: (index) => engine?.onEntitySpawned(index, storage.archetypes.emptyArchetype),
|
|
119
|
+
dropEntity: (index) => engine?.dropEntity(index),
|
|
120
|
+
tick: () => state.tick,
|
|
121
|
+
handleIndex: handleIndexOf,
|
|
122
|
+
});
|
|
123
|
+
// --- queries: the canonical-hash dedup cache + per-archetype matching, kept
|
|
124
|
+
// current by the archetypeCreated hook. fixedBitCount = stride*32 (ids below it pack into the
|
|
125
|
+
// signature words; larger pair ids are residual — ).
|
|
126
|
+
engine = new QueryEngine({
|
|
127
|
+
buffers,
|
|
128
|
+
bitmask,
|
|
129
|
+
maxEntities: resolved.maxEntities,
|
|
130
|
+
byId: storage.archetypes.byId,
|
|
131
|
+
onArchetypeCreated: (fn) => storage.onArchetypeCreated(fn),
|
|
132
|
+
compileContext: {
|
|
133
|
+
idOf: (def) => {
|
|
134
|
+
const id = registry.idOf(def);
|
|
135
|
+
if (id === undefined)
|
|
136
|
+
throw new Error(`component '${def.name}' is not registered with this world — register it in createWorld({ components: [...] })`);
|
|
137
|
+
return id;
|
|
138
|
+
},
|
|
139
|
+
fixedBitCount: stride * 32,
|
|
140
|
+
resolvePair: (relationId, target) => {
|
|
141
|
+
if (pairResolver === null)
|
|
142
|
+
return { componentId: 0, unsatisfiable: true };
|
|
143
|
+
return pairResolver(relationId, target);
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
resolveLocation: (index) => entities.locationOfIndex(index),
|
|
147
|
+
handleOf: (index) => entities.handleOfIndex(index),
|
|
148
|
+
indexOfHandle: handleIndexOf,
|
|
149
|
+
coldResidentsOf: (archetypeId) => storage.coldResidentsOf(archetypeId),
|
|
150
|
+
coldColumnSet: (componentId) => storage.coldColumnSet(componentId),
|
|
151
|
+
coldRowOf: (index, componentId) => storage.coldRowOf(index, componentId),
|
|
152
|
+
signatureOf: (index) => {
|
|
153
|
+
const archId = records.archetypeIdOf(index);
|
|
154
|
+
return storage.archetypes.byId[archId].signature;
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
// Rich-field despawn clear. Walk the dying entity's signature for rich
|
|
158
|
+
// fields and decide whether to DEFER the clear: when any held component has a remove-observer, the
|
|
159
|
+
// deferred row reclaim keeps numeric values readable in onRemove, so the sidecar must stash the
|
|
160
|
+
// dying rich values for the same observer window (RF-REMOVE-READ). The decision mirrors storage's
|
|
161
|
+
// `defer` gate exactly so rich and numeric parity holds.
|
|
162
|
+
const sidecarOnDespawn = (handle) => {
|
|
163
|
+
const index = handleIndexOf(handle);
|
|
164
|
+
const archId = records.archetypeIdOf(index);
|
|
165
|
+
const arch = storage.archetypes.byId[archId];
|
|
166
|
+
if (arch === undefined)
|
|
167
|
+
return;
|
|
168
|
+
const richKeys = [];
|
|
169
|
+
let defer = false;
|
|
170
|
+
for (let i = 0; i < arch.signature.length; i++) {
|
|
171
|
+
const c = arch.signature[i];
|
|
172
|
+
const def = registry.defOf(c);
|
|
173
|
+
if (def !== undefined && def.hasRichFields) {
|
|
174
|
+
let fieldIndex = 0;
|
|
175
|
+
for (const f of def.fields) {
|
|
176
|
+
if (f.rich !== undefined)
|
|
177
|
+
richKeys.push(sidecarKey(c, fieldIndex));
|
|
178
|
+
fieldIndex += 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (reactivity?.hasRemoveObserver(c) === true)
|
|
182
|
+
defer = true;
|
|
183
|
+
}
|
|
184
|
+
if (richKeys.length > 0)
|
|
185
|
+
sidecar.onDespawn(index, richKeys, defer);
|
|
186
|
+
};
|
|
187
|
+
entities.setAccessorResolver(storage);
|
|
188
|
+
entities.setLifecycle({
|
|
189
|
+
onSpawn: (handle) => storage.onSpawn(handle),
|
|
190
|
+
onDespawn: (handle) => {
|
|
191
|
+
// PreDespawn (cascade + pair teardown) runs WHILE `dying` is still
|
|
192
|
+
// alive/resolvable, BEFORE storage.onDespawn shuffle-pops the row and the entity layer frees it.
|
|
193
|
+
preDespawnHook?.(handle);
|
|
194
|
+
// The rich clear runs BEFORE storage.onDespawn so the dying entity's signature is still resolvable
|
|
195
|
+
// (storage.onDespawn shuffle-pops the row; the entity index record is unchanged either way).
|
|
196
|
+
sidecarOnDespawn(handle);
|
|
197
|
+
storage.onDespawn(handle);
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
// --- reactivity: the write/shape rings, changeVersion stamps, deferred
|
|
201
|
+
// observers, and the query-flavor hooks. Wired AFTER storage + queries so trackWrite/trackShape and
|
|
202
|
+
// observer dispatch can resolve locations and re-test entities.
|
|
203
|
+
reactivity = new Reactivity({
|
|
204
|
+
buffers,
|
|
205
|
+
maxEntities: resolved.maxEntities,
|
|
206
|
+
indexBits: handleLayout.indexBits,
|
|
207
|
+
logEntryWords: resolved.reactivity.logEntryWords,
|
|
208
|
+
maxWritesPerFrame: resolved.reactivity.maxWritesPerFrame,
|
|
209
|
+
maxShapeChangesPerFrame: resolved.reactivity.maxShapeChangesPerFrame,
|
|
210
|
+
shrinkRings: resolved.reactivity.shrinkRings,
|
|
211
|
+
dev: IS_DEV,
|
|
212
|
+
resolveLocation: (index) => entities.locationOfIndex(index),
|
|
213
|
+
tick: () => state.tick,
|
|
214
|
+
advanceTick: () => {
|
|
215
|
+
state.tick = (state.tick + 1) >>> 0;
|
|
216
|
+
},
|
|
217
|
+
idOf: (def) => {
|
|
218
|
+
const id = registry.idOf(def);
|
|
219
|
+
if (id === undefined)
|
|
220
|
+
throw new Error(`component '${def.name}' is not registered with this world — register it in createWorld({ components: [...] })`);
|
|
221
|
+
return id;
|
|
222
|
+
},
|
|
223
|
+
holdsAll: (index, componentIds) => {
|
|
224
|
+
for (const c of componentIds)
|
|
225
|
+
if (!bitmask.bitmaskHas(index, c))
|
|
226
|
+
return false;
|
|
227
|
+
return true;
|
|
228
|
+
},
|
|
229
|
+
refOf: (index) => entities.entity(entities.handleOfIndex(index), { lenient: true }),
|
|
230
|
+
resolveHandle: (index) => entities.handleOfIndex(index),
|
|
231
|
+
tracking,
|
|
232
|
+
});
|
|
233
|
+
// The shape-log drain re-runs the same idempotent single-entity maintenance the synchronous
|
|
234
|
+
// hook performs; the conservative overflow path needs a "current matches" source for change
|
|
235
|
+
// observers.
|
|
236
|
+
reactivity.setMaintainHook((index, c) => engine?.maintainEntity(index, c));
|
|
237
|
+
reactivity.setCurrentMembersSource(() => collectCurrentMembers(engine));
|
|
238
|
+
engine.setReactivity({
|
|
239
|
+
attachChangedFlavor: (q, ids) => reactivity.attachChangedFlavor(q, ids),
|
|
240
|
+
drainChanged: (q) => reactivity.drainChanged(q),
|
|
241
|
+
});
|
|
242
|
+
// --- deferred-observer command buffer ---
|
|
243
|
+
// While observerDrain runs, the world's structural verbs (spawn/spawnWith/add/remove/despawn and
|
|
244
|
+
// the relations apply-pair seam) STAGE here instead of direct-applying, so an observer handler that
|
|
245
|
+
// mutates structure cannot corrupt the wave the drain is replaying. The staged ops apply at
|
|
246
|
+
// the start of the NEXT drain — i.e. the next serial flush. applyAddPair/applyRemovePair are the
|
|
247
|
+
// SAME relation seams the scheduler's command-apply path drives (filled by __installRelations).
|
|
248
|
+
const observerCommands = new ObserverCommandBuffer();
|
|
249
|
+
const observerApply = {
|
|
250
|
+
placeReserved(handle, defs) {
|
|
251
|
+
// The handle was reserved-alive when the observer called spawn; place it into the EMPTY archetype
|
|
252
|
+
// (emitting Create), then migrate it to the requested signature in one move (spawnWith semantics).
|
|
253
|
+
storage.onSpawn(handle);
|
|
254
|
+
if (defs.length > 0)
|
|
255
|
+
storage.addMany(handle, defs);
|
|
256
|
+
},
|
|
257
|
+
add: (handle, def) => storage.add(handle, def),
|
|
258
|
+
remove: (handle, def) => storage.remove(handle, def),
|
|
259
|
+
despawn: (handle) => entities.despawn(handle),
|
|
260
|
+
isAlive: (handle) => entities.isAlive(handle),
|
|
261
|
+
writePayload: (handle, def, values) => {
|
|
262
|
+
const view = entities.entity(handle).write(def);
|
|
263
|
+
for (const k of Object.keys(values))
|
|
264
|
+
view[k] = values[k];
|
|
265
|
+
},
|
|
266
|
+
addPair: (subject, relationId, target, payload) => applyAddPair?.(subject, relationId, target, payload),
|
|
267
|
+
removePair: (subject, relationId, target) => applyRemovePair?.(subject, relationId, target),
|
|
268
|
+
};
|
|
269
|
+
// --- serialization surface -------------------------
|
|
270
|
+
// Reads archetype columns/signatures + the registry + the relation provider; drives deserialize-side
|
|
271
|
+
// spawn/migrate. The user-component metadata is the registered defs in dense-id order. Synthetic ids
|
|
272
|
+
// (pair/presence/overflow) are intentionally excluded from `components()` — they are reconstructed by
|
|
273
|
+
// re-minting on the receiver, never shipped as schema.
|
|
274
|
+
const userComponents = resolved.components;
|
|
275
|
+
const componentIdByName = new Map();
|
|
276
|
+
for (const def of userComponents) {
|
|
277
|
+
const id = registry.idOf(def);
|
|
278
|
+
if (id !== undefined)
|
|
279
|
+
componentIdByName.set(def.name, id);
|
|
280
|
+
}
|
|
281
|
+
// Rich-field enumeration for the serialization sidecar section. Built once
|
|
282
|
+
// from the registered defs in (componentId, fieldIndex) order; the snapshot/delta writers join this
|
|
283
|
+
// with each entity's signature — they MUST NOT read `archetypeView().components`, which strips rich
|
|
284
|
+
// fields (and drops rich-only components entirely).
|
|
285
|
+
const richFieldList = [];
|
|
286
|
+
for (const def of userComponents) {
|
|
287
|
+
const id = registry.idOf(def);
|
|
288
|
+
if (id === undefined)
|
|
289
|
+
continue;
|
|
290
|
+
let fieldIndex = 0;
|
|
291
|
+
for (const f of def.fields) {
|
|
292
|
+
if (f.rich !== undefined)
|
|
293
|
+
richFieldList.push({ componentId: id, fieldIndex, name: f.name, kind: f.rich });
|
|
294
|
+
fieldIndex += 1;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const computeSchemaHash = () => {
|
|
298
|
+
// FNV-1a over the canonical (componentName, fieldName, token)* + relation names. The receiver
|
|
299
|
+
// recomputes it from ITS defineComponent set and must match — a fail-fast guard against stale code.
|
|
300
|
+
let h = 0x811c9dc5;
|
|
301
|
+
const fnv = (s) => {
|
|
302
|
+
for (let i = 0; i < s.length; i++) {
|
|
303
|
+
h ^= s.charCodeAt(i);
|
|
304
|
+
h = Math.imul(h, 0x01000193);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
for (const def of userComponents) {
|
|
308
|
+
fnv(def.name);
|
|
309
|
+
for (const f of def.fields) {
|
|
310
|
+
fnv(f.name);
|
|
311
|
+
fnv(typeof f.token === 'string' ? f.token : JSON.stringify(f.token));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const prov = serializationProvider;
|
|
315
|
+
if (prov !== null)
|
|
316
|
+
for (const r of prov.relations())
|
|
317
|
+
fnv(r.name);
|
|
318
|
+
return h >>> 0;
|
|
319
|
+
};
|
|
320
|
+
const archetypeView = (arch) => {
|
|
321
|
+
const components = [];
|
|
322
|
+
// Iterate the signature in canonical (sorted) order so column order is deterministic.
|
|
323
|
+
for (let i = 0; i < arch.signature.length; i++) {
|
|
324
|
+
const c = arch.signature[i];
|
|
325
|
+
const set = arch.columnSets.get(c);
|
|
326
|
+
if (set === undefined)
|
|
327
|
+
continue; // tag / pair / no column
|
|
328
|
+
const def = registry.defOf(c);
|
|
329
|
+
if (def === undefined)
|
|
330
|
+
continue;
|
|
331
|
+
const fields = [];
|
|
332
|
+
for (const f of def.fields)
|
|
333
|
+
if (f.ctor !== null)
|
|
334
|
+
fields.push(f);
|
|
335
|
+
components.push({ componentId: c, columns: set.columns, fields });
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
id: arch.id,
|
|
339
|
+
signature: arch.signature,
|
|
340
|
+
count: arch.count,
|
|
341
|
+
rows: arch.rows,
|
|
342
|
+
components,
|
|
343
|
+
};
|
|
344
|
+
};
|
|
345
|
+
const serialize = {
|
|
346
|
+
schemaHash: computeSchemaHash,
|
|
347
|
+
components() {
|
|
348
|
+
const out = [];
|
|
349
|
+
for (const def of userComponents) {
|
|
350
|
+
const id = registry.idOf(def);
|
|
351
|
+
if (id === undefined)
|
|
352
|
+
continue;
|
|
353
|
+
const rt = def;
|
|
354
|
+
out.push({ name: def.name, id, fieldCount: def.fields.length, storage: rt.options.storage });
|
|
355
|
+
}
|
|
356
|
+
return out;
|
|
357
|
+
},
|
|
358
|
+
fieldsOf(id) {
|
|
359
|
+
return registry.defOf(id)?.fields;
|
|
360
|
+
},
|
|
361
|
+
componentIdByName(name) {
|
|
362
|
+
return componentIdByName.get(name);
|
|
363
|
+
},
|
|
364
|
+
numComponentTypes() {
|
|
365
|
+
return registry.nextComponentId;
|
|
366
|
+
},
|
|
367
|
+
archetypes() {
|
|
368
|
+
const out = [];
|
|
369
|
+
for (const arch of storage.archetypes.byId) {
|
|
370
|
+
if (arch.cold || arch.count === 0)
|
|
371
|
+
continue;
|
|
372
|
+
out.push(archetypeView(arch));
|
|
373
|
+
}
|
|
374
|
+
// id-ascending (byId is already in id order, but tag/cold gaps are filtered above).
|
|
375
|
+
out.sort((a, b) => a.id - b.id);
|
|
376
|
+
return out;
|
|
377
|
+
},
|
|
378
|
+
relations() {
|
|
379
|
+
return serializationProvider ?? undefined;
|
|
380
|
+
},
|
|
381
|
+
richFields() {
|
|
382
|
+
return richFieldList;
|
|
383
|
+
},
|
|
384
|
+
richValueOf(handle, componentId, fieldIndex) {
|
|
385
|
+
if (!entities.isAlive(handle))
|
|
386
|
+
return undefined;
|
|
387
|
+
const index = handleIndexOf(handle);
|
|
388
|
+
const key = sidecarKey(componentId, fieldIndex);
|
|
389
|
+
if (!sidecar.hasColumn(key))
|
|
390
|
+
return undefined;
|
|
391
|
+
const gen = entities.decodeHandle(entities.handleOfIndex(index)).generation;
|
|
392
|
+
return sidecar.read(key, index, gen);
|
|
393
|
+
},
|
|
394
|
+
richIsPresent(handle, componentId, fieldIndex) {
|
|
395
|
+
if (!entities.isAlive(handle))
|
|
396
|
+
return false;
|
|
397
|
+
const index = handleIndexOf(handle);
|
|
398
|
+
const key = sidecarKey(componentId, fieldIndex);
|
|
399
|
+
if (!sidecar.hasColumn(key))
|
|
400
|
+
return false;
|
|
401
|
+
const gen = entities.decodeHandle(entities.handleOfIndex(index)).generation;
|
|
402
|
+
return sidecar.isPresent(key, index, gen);
|
|
403
|
+
},
|
|
404
|
+
setRichValue(handle, componentId, fieldIndex, value) {
|
|
405
|
+
if (!entities.isAlive(handle))
|
|
406
|
+
return;
|
|
407
|
+
const index = handleIndexOf(handle);
|
|
408
|
+
const key = sidecarKey(componentId, fieldIndex);
|
|
409
|
+
if (!sidecar.hasColumn(key))
|
|
410
|
+
return;
|
|
411
|
+
const gen = entities.decodeHandle(entities.handleOfIndex(index)).generation;
|
|
412
|
+
sidecar.write(key, index, gen, value);
|
|
413
|
+
// Mark the loaded value changed identically to a live write so a delta from THIS receiver re-emits
|
|
414
|
+
// it (parity with the column path, which writes through tracked accessor setters). Whole-entity
|
|
415
|
+
// changeVersion stamp; fieldIndex forwarded but discarded downstream as for numeric fields.
|
|
416
|
+
trackWrite(index, componentId, fieldIndex);
|
|
417
|
+
},
|
|
418
|
+
enableStructuralJournal() {
|
|
419
|
+
;
|
|
420
|
+
reactivity.enableStructuralJournal();
|
|
421
|
+
},
|
|
422
|
+
drainStructuralSince(since) {
|
|
423
|
+
return reactivity.drainStructuralSince(since);
|
|
424
|
+
},
|
|
425
|
+
relationIdOfPair(pairId) {
|
|
426
|
+
return serializationProvider?.relationIdOfPair(pairId);
|
|
427
|
+
},
|
|
428
|
+
spawn() {
|
|
429
|
+
return entities.spawn();
|
|
430
|
+
},
|
|
431
|
+
spawnInto(handle, componentIds) {
|
|
432
|
+
if (componentIds.length === 0)
|
|
433
|
+
return;
|
|
434
|
+
const defs = [];
|
|
435
|
+
for (const c of componentIds) {
|
|
436
|
+
const def = registry.defOf(c);
|
|
437
|
+
if (def !== undefined)
|
|
438
|
+
defs.push(def);
|
|
439
|
+
}
|
|
440
|
+
storage.addMany(handle, defs);
|
|
441
|
+
},
|
|
442
|
+
removeComponents(handle, componentIds) {
|
|
443
|
+
if (componentIds.length === 0 || !entities.isAlive(handle))
|
|
444
|
+
return;
|
|
445
|
+
const defs = [];
|
|
446
|
+
for (const c of componentIds) {
|
|
447
|
+
const def = registry.defOf(c);
|
|
448
|
+
if (def !== undefined)
|
|
449
|
+
defs.push(def);
|
|
450
|
+
}
|
|
451
|
+
if (defs.length > 0)
|
|
452
|
+
storage.removeMany(handle, defs);
|
|
453
|
+
},
|
|
454
|
+
despawn(handle) {
|
|
455
|
+
if (entities.isAlive(handle))
|
|
456
|
+
entities.despawn(handle);
|
|
457
|
+
},
|
|
458
|
+
columnsOf(handle, componentId) {
|
|
459
|
+
if (!entities.isAlive(handle))
|
|
460
|
+
return null;
|
|
461
|
+
const index = handleIndexOf(handle);
|
|
462
|
+
const archId = records.archetypeIdOf(index);
|
|
463
|
+
const arch = storage.archetypes.byId[archId];
|
|
464
|
+
if (arch === undefined || arch.cold)
|
|
465
|
+
return null;
|
|
466
|
+
const set = arch.columnSets.get(componentId);
|
|
467
|
+
if (set === undefined)
|
|
468
|
+
return null;
|
|
469
|
+
const def = registry.defOf(componentId);
|
|
470
|
+
if (def === undefined)
|
|
471
|
+
return null;
|
|
472
|
+
const fields = [];
|
|
473
|
+
for (const f of def.fields)
|
|
474
|
+
if (f.ctor !== null)
|
|
475
|
+
fields.push(f);
|
|
476
|
+
return { columns: set.columns, fields, row: records.rowOf(index) };
|
|
477
|
+
},
|
|
478
|
+
clearAll() {
|
|
479
|
+
// Collect alive handles from every hot archetype first (despawn shuffle-pops rows mid-iteration).
|
|
480
|
+
const handles = [];
|
|
481
|
+
for (const arch of storage.archetypes.byId) {
|
|
482
|
+
if (arch.cold)
|
|
483
|
+
continue;
|
|
484
|
+
for (let r = 0; r < arch.count; r++)
|
|
485
|
+
handles.push(arch.rows[r]);
|
|
486
|
+
}
|
|
487
|
+
for (const h of handles)
|
|
488
|
+
if (entities.isAlive(h))
|
|
489
|
+
entities.despawn(h);
|
|
490
|
+
},
|
|
491
|
+
aliveCount() {
|
|
492
|
+
return entities.handleStats().aliveCount;
|
|
493
|
+
},
|
|
494
|
+
indexBits: handleLayout.indexBits,
|
|
495
|
+
handleIndex: (handle) => handleIndexOf(handle),
|
|
496
|
+
capabilities: () => capabilities,
|
|
497
|
+
};
|
|
498
|
+
// --- introspection surface (@ecsia/devtools) -------------------------
|
|
499
|
+
// The FULL archetype census (cold + empty, which __serialize.archetypes() filters out) and the live
|
|
500
|
+
// query enumeration (the QueryEngine's `liveQueries` getter is core-private). Pure reads.
|
|
501
|
+
const inspect = {
|
|
502
|
+
archetypes() {
|
|
503
|
+
const out = [];
|
|
504
|
+
for (const arch of storage.archetypes.byId) {
|
|
505
|
+
out.push({
|
|
506
|
+
// Copy: arch.signature is the LIVE typed-array backing the archetype; the seam is read-only
|
|
507
|
+
// and must never hand out a buffer a consumer could mutate to corrupt storage.
|
|
508
|
+
id: arch.id,
|
|
509
|
+
signature: Array.from(arch.signature),
|
|
510
|
+
count: arch.count,
|
|
511
|
+
cold: arch.cold,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
out.sort((a, b) => a.id - b.id);
|
|
515
|
+
return out;
|
|
516
|
+
},
|
|
517
|
+
queries() {
|
|
518
|
+
const out = [];
|
|
519
|
+
for (const lq of engine.liveQueries) {
|
|
520
|
+
out.push({ terms: lq.terms, matchedArchetypes: lq.matchingArchetypes.length, size: lq.count });
|
|
521
|
+
}
|
|
522
|
+
return out;
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
const world = {
|
|
526
|
+
get options() {
|
|
527
|
+
return resolved;
|
|
528
|
+
},
|
|
529
|
+
get phase() {
|
|
530
|
+
return state.phase;
|
|
531
|
+
},
|
|
532
|
+
__setPhase(phase) {
|
|
533
|
+
state.phase = phase;
|
|
534
|
+
},
|
|
535
|
+
__spawnReserved(handle) {
|
|
536
|
+
storage.onSpawn(handle);
|
|
537
|
+
},
|
|
538
|
+
__apply: {
|
|
539
|
+
defOf(id) {
|
|
540
|
+
return registry.defOf(id);
|
|
541
|
+
},
|
|
542
|
+
addMany(handle, defs) {
|
|
543
|
+
storage.addMany(handle, defs);
|
|
544
|
+
},
|
|
545
|
+
removeMany(handle, defs) {
|
|
546
|
+
storage.removeMany(handle, defs);
|
|
547
|
+
},
|
|
548
|
+
writePayload(handle, def, values) {
|
|
549
|
+
const view = entities.entity(handle).write(def);
|
|
550
|
+
for (const k of Object.keys(values))
|
|
551
|
+
view[k] = values[k];
|
|
552
|
+
},
|
|
553
|
+
addPair(subject, relationId, target, payload) {
|
|
554
|
+
applyAddPair?.(subject, relationId, target, payload);
|
|
555
|
+
},
|
|
556
|
+
removePair(subject, relationId, target) {
|
|
557
|
+
applyRemovePair?.(subject, relationId, target);
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
__installRelations() {
|
|
561
|
+
return {
|
|
562
|
+
allocSyntheticId: () => registry.allocSyntheticId(),
|
|
563
|
+
registerSynthetic: (def, id) => registry.registerSynthetic(def, id),
|
|
564
|
+
defOf: (id) => registry.defOf(id),
|
|
565
|
+
migrateAddingMany: (handle, defs) => storage.addMany(handle, defs),
|
|
566
|
+
migrateRemovingMany: (handle, defs) => storage.removeMany(handle, defs),
|
|
567
|
+
bitmaskHas: (index, id) => bitmask.bitmaskHas(index, id),
|
|
568
|
+
columnSetFor: (handle, def) => {
|
|
569
|
+
if (!entities.isAlive(handle))
|
|
570
|
+
return null;
|
|
571
|
+
const index = handleIndexOf(handle);
|
|
572
|
+
const archId = records.archetypeIdOf(index);
|
|
573
|
+
const arch = storage.archetypes.byId[archId];
|
|
574
|
+
if (arch === undefined || arch.cold)
|
|
575
|
+
return null;
|
|
576
|
+
const set = arch.columnSets.get(registry.idOf(def));
|
|
577
|
+
if (set === undefined)
|
|
578
|
+
return null;
|
|
579
|
+
return { set, row: records.rowOf(index) };
|
|
580
|
+
},
|
|
581
|
+
isAlive: (handle) => entities.isAlive(handle),
|
|
582
|
+
handleIndex: (handle) => handleIndexOf(handle),
|
|
583
|
+
handleOfIndex: (index) => entities.handleOfIndex(index),
|
|
584
|
+
despawn: (handle) => entities.despawn(handle),
|
|
585
|
+
deferRelationOp: (op, subject, relation, target, payload) => {
|
|
586
|
+
if (!observerCommands.deferring)
|
|
587
|
+
return false;
|
|
588
|
+
const relationId = relation.id;
|
|
589
|
+
if (op === 'add')
|
|
590
|
+
observerCommands.stageAddPair(subject, relation, relationId, target, payload);
|
|
591
|
+
else
|
|
592
|
+
observerCommands.stageRemovePair(subject, relation, relationId, target);
|
|
593
|
+
return true;
|
|
594
|
+
},
|
|
595
|
+
trackWrite: (index, componentId) => trackWrite(index, componentId),
|
|
596
|
+
trackShapePair: (index, pairId, targetIndex, add) => {
|
|
597
|
+
;
|
|
598
|
+
reactivity.trackShapePair(index, pairId, targetIndex, add ? ShapeKind.AddPair : ShapeKind.RemovePair);
|
|
599
|
+
},
|
|
600
|
+
trackShapeSetPayload: (index, pairId, targetIndex) => {
|
|
601
|
+
;
|
|
602
|
+
reactivity.trackShapeSetPayload(index, pairId, targetIndex);
|
|
603
|
+
},
|
|
604
|
+
buffers,
|
|
605
|
+
accessorWorld,
|
|
606
|
+
setPreDespawn: (hook) => {
|
|
607
|
+
preDespawnHook = hook;
|
|
608
|
+
},
|
|
609
|
+
setPairResolver: (resolve) => {
|
|
610
|
+
pairResolver = resolve;
|
|
611
|
+
},
|
|
612
|
+
setApplyPair: (addPair, removePair) => {
|
|
613
|
+
applyAddPair = addPair;
|
|
614
|
+
applyRemovePair = removePair;
|
|
615
|
+
},
|
|
616
|
+
setSerializationProvider: (provider) => {
|
|
617
|
+
serializationProvider = provider;
|
|
618
|
+
},
|
|
619
|
+
maxEntities: resolved.maxEntities,
|
|
620
|
+
indexBits: handleLayout.indexBits,
|
|
621
|
+
};
|
|
622
|
+
},
|
|
623
|
+
__exportShared() {
|
|
624
|
+
const base = buffers.exportSharedHandles();
|
|
625
|
+
// The entity-record regions are owned by EntityStore (allocU32), not the Buffers registry, so
|
|
626
|
+
// merge them into the manifest here — a worker needs them to resolve (archetypeId, row).
|
|
627
|
+
const rec = entities.sharedRecordRegions();
|
|
628
|
+
const extra = [];
|
|
629
|
+
if (isSharedBacking(rec.archetypeId)) {
|
|
630
|
+
extra.push({ key: 'entity.archetypeId', backing: rec.archetypeId, element: 'u32' });
|
|
631
|
+
}
|
|
632
|
+
if (isSharedBacking(rec.archetypeRow)) {
|
|
633
|
+
extra.push({ key: 'entity.archetypeRow', backing: rec.archetypeRow, element: 'u32' });
|
|
634
|
+
}
|
|
635
|
+
return { columns: base.columns, regions: [...base.regions, ...extra] };
|
|
636
|
+
},
|
|
637
|
+
__columnGrowth() {
|
|
638
|
+
return buffers.columnGrowth();
|
|
639
|
+
},
|
|
640
|
+
__serialize: serialize,
|
|
641
|
+
__inspect: inspect,
|
|
642
|
+
__mergeWorkerWrites(pairs, count) {
|
|
643
|
+
;
|
|
644
|
+
reactivity.mergeWorkerWrites(pairs, count);
|
|
645
|
+
},
|
|
646
|
+
get tick() {
|
|
647
|
+
return state.tick;
|
|
648
|
+
},
|
|
649
|
+
currentTick() {
|
|
650
|
+
return state.tick;
|
|
651
|
+
},
|
|
652
|
+
spawn() {
|
|
653
|
+
if (observerCommands.deferring) {
|
|
654
|
+
// A spawn inside an observer reserves a live handle NOW (so the handler can configure it)
|
|
655
|
+
// but defers archetype placement to the next flush — the command-buffer reserved-spawn model.
|
|
656
|
+
const handle = reserveEntityBlock(entities.index, -1, 1).handles[0];
|
|
657
|
+
observerCommands.stageSpawnWith(handle, []);
|
|
658
|
+
return handle;
|
|
659
|
+
}
|
|
660
|
+
return entities.spawn();
|
|
661
|
+
},
|
|
662
|
+
spawnWith(...specs) {
|
|
663
|
+
// Split each arg into its def (for the single EMPTY→target migration) and any `[def, values]`
|
|
664
|
+
// initializer (Item 8). Values are written AFTER placement through the tracked accessor path so
|
|
665
|
+
// onChange/write-log fire exactly as a post-spawn write would.
|
|
666
|
+
const defs = [];
|
|
667
|
+
const values = [];
|
|
668
|
+
for (const spec of specs) {
|
|
669
|
+
if (Array.isArray(spec)) {
|
|
670
|
+
const [def, vals] = spec;
|
|
671
|
+
defs.push(def);
|
|
672
|
+
values.push([def, vals]);
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
defs.push(spec);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (observerCommands.deferring) {
|
|
679
|
+
const handle = reserveEntityBlock(entities.index, -1, 1).handles[0];
|
|
680
|
+
observerCommands.stageSpawnWith(handle, defs, values);
|
|
681
|
+
return handle;
|
|
682
|
+
}
|
|
683
|
+
const handle = entities.spawn();
|
|
684
|
+
storage.spawnWith(handle, defs);
|
|
685
|
+
for (const [def, vals] of values) {
|
|
686
|
+
const view = entities.entity(handle).write(def);
|
|
687
|
+
for (const k of Object.keys(vals))
|
|
688
|
+
view[k] = vals[k];
|
|
689
|
+
}
|
|
690
|
+
return handle;
|
|
691
|
+
},
|
|
692
|
+
add(handle, def) {
|
|
693
|
+
if (observerCommands.deferring) {
|
|
694
|
+
observerCommands.stageAdd(handle, def);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
storage.add(handle, def);
|
|
698
|
+
},
|
|
699
|
+
remove(handle, def) {
|
|
700
|
+
if (observerCommands.deferring) {
|
|
701
|
+
observerCommands.stageRemove(handle, def);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
storage.remove(handle, def);
|
|
705
|
+
},
|
|
706
|
+
warm(...defs) {
|
|
707
|
+
storage.warm(defs);
|
|
708
|
+
},
|
|
709
|
+
despawn(handle) {
|
|
710
|
+
if (observerCommands.deferring) {
|
|
711
|
+
observerCommands.stageDespawn(handle);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
entities.despawn(handle);
|
|
715
|
+
},
|
|
716
|
+
isAlive(handle) {
|
|
717
|
+
return entities.isAlive(handle);
|
|
718
|
+
},
|
|
719
|
+
has(handle, def) {
|
|
720
|
+
// Liveness first, WITHOUT the bitmask; a dead handle is never a member.
|
|
721
|
+
if (!entities.isAlive(handle))
|
|
722
|
+
return false;
|
|
723
|
+
return storage.has(handle, def);
|
|
724
|
+
},
|
|
725
|
+
entity(handle, opts) {
|
|
726
|
+
return entities.entity(handle, opts);
|
|
727
|
+
},
|
|
728
|
+
reserveEntityBlock(workerIndex, count) {
|
|
729
|
+
return reserveEntityBlock(entities.index, workerIndex, count);
|
|
730
|
+
},
|
|
731
|
+
returnReservedIds(reservation, consumedCount) {
|
|
732
|
+
returnReservedIds(entities.index, reservation, consumedCount);
|
|
733
|
+
},
|
|
734
|
+
handleLayout,
|
|
735
|
+
encodeHandle(index, generation) {
|
|
736
|
+
return entities.encodeHandle(index, generation);
|
|
737
|
+
},
|
|
738
|
+
decodeHandle(handle) {
|
|
739
|
+
return entities.decodeHandle(handle);
|
|
740
|
+
},
|
|
741
|
+
handleStats() {
|
|
742
|
+
return entities.handleStats();
|
|
743
|
+
},
|
|
744
|
+
trackWrite(index, componentId, fieldIndex) {
|
|
745
|
+
trackWrite(index, componentId, fieldIndex);
|
|
746
|
+
},
|
|
747
|
+
observe(term, handler) {
|
|
748
|
+
return reactivity.observe(term, handler);
|
|
749
|
+
},
|
|
750
|
+
changedSince(handle, since) {
|
|
751
|
+
;
|
|
752
|
+
reactivity.enableChangeVersion();
|
|
753
|
+
return reactivity.changedSince(handle, since);
|
|
754
|
+
},
|
|
755
|
+
changedRows(archetypeId, since) {
|
|
756
|
+
const arch = storage.archetypes.byId[archetypeId];
|
|
757
|
+
const count = arch === undefined ? 0 : arch.count;
|
|
758
|
+
const rows = arch?.rows;
|
|
759
|
+
return reactivity.changedRows(archetypeId, since, count, (row) => rows === undefined ? 0 : handleIndexOf(rows[row]));
|
|
760
|
+
},
|
|
761
|
+
advanceTick() {
|
|
762
|
+
state.tick = (state.tick + 1) >>> 0;
|
|
763
|
+
},
|
|
764
|
+
mergeCorrals() {
|
|
765
|
+
;
|
|
766
|
+
reactivity.mergeCorrals();
|
|
767
|
+
},
|
|
768
|
+
maintainStructural() {
|
|
769
|
+
;
|
|
770
|
+
reactivity.maintainStructural();
|
|
771
|
+
},
|
|
772
|
+
observerDrain() {
|
|
773
|
+
// re-entrancy guard: a drain must never re-enter itself (the flush below can trigger query
|
|
774
|
+
// maintenance / structural ops that must not recursively re-drain). If already draining, return.
|
|
775
|
+
if (!observerCommands.enterDrain())
|
|
776
|
+
return;
|
|
777
|
+
try {
|
|
778
|
+
// "applied at the NEXT serial flush": apply the ops staged by the PREVIOUS drain's
|
|
779
|
+
// observers before reading this drain's frozen log snapshot. For 'frame-end' cadence this is the
|
|
780
|
+
// next frame; for 'per-system' it is the next wave's slot. Applying here (not synchronously
|
|
781
|
+
// mid-handler) is what makes a spawned entity observable by onAdd NEXT drain, deterministically.
|
|
782
|
+
observerCommands.flush(observerApply);
|
|
783
|
+
// Structural ops the handlers issue during THIS drain stage to the buffer instead of mutating
|
|
784
|
+
// the world mid-drain (so no observer ever sees a partially-applied wave).
|
|
785
|
+
observerCommands.beginDeferring();
|
|
786
|
+
try {
|
|
787
|
+
;
|
|
788
|
+
reactivity.observerDrain();
|
|
789
|
+
}
|
|
790
|
+
finally {
|
|
791
|
+
observerCommands.endDeferring();
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
finally {
|
|
795
|
+
observerCommands.exitDrain();
|
|
796
|
+
// Flush the deferred rich-field clears now that onRemove handlers have run
|
|
797
|
+
// — the same post-observer point storage's deferred row reclaim ceases to be readable. After this
|
|
798
|
+
// the dying entity's rich values are gone (RF-REMOVE-READ window closes); a recycled index reads
|
|
799
|
+
// the default via the generation guard regardless.
|
|
800
|
+
sidecar.flushPending();
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
flushLogs() {
|
|
804
|
+
;
|
|
805
|
+
reactivity.flushLogs();
|
|
806
|
+
},
|
|
807
|
+
query: ((...terms) => {
|
|
808
|
+
// The LiveQuery structurally satisfies Query<T> (terms, each, [Symbol.iterator], flavors,
|
|
809
|
+
// count); the WorldQuery overload family supplies the element typing per the actual terms.
|
|
810
|
+
return engine.query(terms);
|
|
811
|
+
}),
|
|
812
|
+
frameReset() {
|
|
813
|
+
// Reactivity.frameReset advances the world tick + recycles the rings; then the query
|
|
814
|
+
// engine clears its per-frame added/removed delta lists.
|
|
815
|
+
;
|
|
816
|
+
reactivity.frameReset();
|
|
817
|
+
engine.frameReset();
|
|
818
|
+
},
|
|
819
|
+
};
|
|
820
|
+
return Object.freeze(world);
|
|
821
|
+
}
|
|
822
|
+
/** Collect the union of every live query's current matching indices. */
|
|
823
|
+
function collectCurrentMembers(engine) {
|
|
824
|
+
const seen = new Set();
|
|
825
|
+
for (const lq of engine.liveQueries) {
|
|
826
|
+
for (const index of lq.current)
|
|
827
|
+
seen.add(index);
|
|
828
|
+
}
|
|
829
|
+
return seen;
|
|
830
|
+
}
|
|
831
|
+
//# sourceMappingURL=world.js.map
|