@ecsia/scheduler 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.
Files changed (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -0
  3. package/dist/commands/apply.d.ts +49 -0
  4. package/dist/commands/apply.d.ts.map +1 -0
  5. package/dist/commands/apply.js +211 -0
  6. package/dist/commands/apply.js.map +1 -0
  7. package/dist/commands/buffer.d.ts +53 -0
  8. package/dist/commands/buffer.d.ts.map +1 -0
  9. package/dist/commands/buffer.js +66 -0
  10. package/dist/commands/buffer.js.map +1 -0
  11. package/dist/commands/encode.d.ts +30 -0
  12. package/dist/commands/encode.d.ts.map +1 -0
  13. package/dist/commands/encode.js +108 -0
  14. package/dist/commands/encode.js.map +1 -0
  15. package/dist/commands/fields.d.ts +22 -0
  16. package/dist/commands/fields.d.ts.map +1 -0
  17. package/dist/commands/fields.js +123 -0
  18. package/dist/commands/fields.js.map +1 -0
  19. package/dist/commands/index.d.ts +10 -0
  20. package/dist/commands/index.d.ts.map +1 -0
  21. package/dist/commands/index.js +6 -0
  22. package/dist/commands/index.js.map +1 -0
  23. package/dist/commands/op.d.ts +16 -0
  24. package/dist/commands/op.d.ts.map +1 -0
  25. package/dist/commands/op.js +43 -0
  26. package/dist/commands/op.js.map +1 -0
  27. package/dist/executor/guards.d.ts +8 -0
  28. package/dist/executor/guards.d.ts.map +1 -0
  29. package/dist/executor/guards.js +61 -0
  30. package/dist/executor/guards.js.map +1 -0
  31. package/dist/executor/index.d.ts +9 -0
  32. package/dist/executor/index.d.ts.map +1 -0
  33. package/dist/executor/index.js +6 -0
  34. package/dist/executor/index.js.map +1 -0
  35. package/dist/executor/run-wave.d.ts +19 -0
  36. package/dist/executor/run-wave.d.ts.map +1 -0
  37. package/dist/executor/run-wave.js +38 -0
  38. package/dist/executor/run-wave.js.map +1 -0
  39. package/dist/executor/scheduler.d.ts +41 -0
  40. package/dist/executor/scheduler.d.ts.map +1 -0
  41. package/dist/executor/scheduler.js +71 -0
  42. package/dist/executor/scheduler.js.map +1 -0
  43. package/dist/executor/seams.d.ts +38 -0
  44. package/dist/executor/seams.d.ts.map +1 -0
  45. package/dist/executor/seams.js +16 -0
  46. package/dist/executor/seams.js.map +1 -0
  47. package/dist/executor/update-threaded.d.ts +17 -0
  48. package/dist/executor/update-threaded.d.ts.map +1 -0
  49. package/dist/executor/update-threaded.js +67 -0
  50. package/dist/executor/update-threaded.js.map +1 -0
  51. package/dist/executor/update.d.ts +4 -0
  52. package/dist/executor/update.d.ts.map +1 -0
  53. package/dist/executor/update.js +23 -0
  54. package/dist/executor/update.js.map +1 -0
  55. package/dist/graph/dag.d.ts +16 -0
  56. package/dist/graph/dag.d.ts.map +1 -0
  57. package/dist/graph/dag.js +118 -0
  58. package/dist/graph/dag.js.map +1 -0
  59. package/dist/graph/edges.d.ts +20 -0
  60. package/dist/graph/edges.d.ts.map +1 -0
  61. package/dist/graph/edges.js +181 -0
  62. package/dist/graph/edges.js.map +1 -0
  63. package/dist/graph/index.d.ts +8 -0
  64. package/dist/graph/index.d.ts.map +1 -0
  65. package/dist/graph/index.js +5 -0
  66. package/dist/graph/index.js.map +1 -0
  67. package/dist/graph/waves.d.ts +30 -0
  68. package/dist/graph/waves.d.ts.map +1 -0
  69. package/dist/graph/waves.js +133 -0
  70. package/dist/graph/waves.js.map +1 -0
  71. package/dist/graph/weights.d.ts +7 -0
  72. package/dist/graph/weights.d.ts.map +1 -0
  73. package/dist/graph/weights.js +10 -0
  74. package/dist/graph/weights.js.map +1 -0
  75. package/dist/index.d.ts +9 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +21 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/internal.d.ts +13 -0
  80. package/dist/internal.d.ts.map +1 -0
  81. package/dist/internal.js +12 -0
  82. package/dist/internal.js.map +1 -0
  83. package/dist/planner/access.d.ts +18 -0
  84. package/dist/planner/access.d.ts.map +1 -0
  85. package/dist/planner/access.js +92 -0
  86. package/dist/planner/access.js.map +1 -0
  87. package/dist/planner/define-system.d.ts +14 -0
  88. package/dist/planner/define-system.d.ts.map +1 -0
  89. package/dist/planner/define-system.js +30 -0
  90. package/dist/planner/define-system.js.map +1 -0
  91. package/dist/planner/index.d.ts +6 -0
  92. package/dist/planner/index.d.ts.map +1 -0
  93. package/dist/planner/index.js +4 -0
  94. package/dist/planner/index.js.map +1 -0
  95. package/dist/planner/types.d.ts +74 -0
  96. package/dist/planner/types.d.ts.map +1 -0
  97. package/dist/planner/types.js +6 -0
  98. package/dist/planner/types.js.map +1 -0
  99. package/dist/workers/atomics-shim.d.ts +8 -0
  100. package/dist/workers/atomics-shim.d.ts.map +1 -0
  101. package/dist/workers/atomics-shim.js +14 -0
  102. package/dist/workers/atomics-shim.js.map +1 -0
  103. package/dist/workers/index.d.ts +13 -0
  104. package/dist/workers/index.d.ts.map +1 -0
  105. package/dist/workers/index.js +10 -0
  106. package/dist/workers/index.js.map +1 -0
  107. package/dist/workers/manifest.d.ts +67 -0
  108. package/dist/workers/manifest.d.ts.map +1 -0
  109. package/dist/workers/manifest.js +5 -0
  110. package/dist/workers/manifest.js.map +1 -0
  111. package/dist/workers/pool.d.ts +60 -0
  112. package/dist/workers/pool.d.ts.map +1 -0
  113. package/dist/workers/pool.js +313 -0
  114. package/dist/workers/pool.js.map +1 -0
  115. package/dist/workers/reservation.d.ts +18 -0
  116. package/dist/workers/reservation.d.ts.map +1 -0
  117. package/dist/workers/reservation.js +41 -0
  118. package/dist/workers/reservation.js.map +1 -0
  119. package/dist/workers/wave-sync.d.ts +18 -0
  120. package/dist/workers/wave-sync.d.ts.map +1 -0
  121. package/dist/workers/wave-sync.js +88 -0
  122. package/dist/workers/wave-sync.js.map +1 -0
  123. package/dist/workers/worker-entry.d.ts +2 -0
  124. package/dist/workers/worker-entry.d.ts.map +1 -0
  125. package/dist/workers/worker-entry.js +187 -0
  126. package/dist/workers/worker-entry.js.map +1 -0
  127. package/dist/workers/worker-system.d.ts +31 -0
  128. package/dist/workers/worker-system.d.ts.map +1 -0
  129. package/dist/workers/worker-system.js +20 -0
  130. package/dist/workers/worker-system.js.map +1 -0
  131. package/dist/workers/world-view.d.ts +39 -0
  132. package/dist/workers/world-view.d.ts.map +1 -0
  133. package/dist/workers/world-view.js +85 -0
  134. package/dist/workers/world-view.js.map +1 -0
  135. package/package.json +53 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andy Aragon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # @ecsia/scheduler
2
+
3
+ The frame scheduler for [**ecsia**](https://github.com/andymai/ecsia) — a fast, type-safe Entity
4
+ Component System for TypeScript.
5
+
6
+ `@ecsia/scheduler` derives an access-graph conflict DAG from each system's declared
7
+ `{read, write}` set, runs the resulting waves, and — opt-in — dispatches disjoint-write work
8
+ across a real `worker_threads` + `Atomics` pool whose result is **bit-identical** to the
9
+ serial path (a fixed worker-index command-buffer merge makes the merge order deterministic).
10
+
11
+ > **Status:** 0.1.0, unpublished. Most users want the umbrella package
12
+ > [`ecsia`](https://www.npmjs.com/package/ecsia), which re-exports `defineSystem`,
13
+ > `createScheduler`, and `WorkerPool`.
14
+ >
15
+ > **Known limitation (worker pool):** the pool is `node:worker_threads`-based and requires
16
+ > `SharedArrayBuffer`; without it, ecsia warns and runs single-threaded — never silently.
17
+ > A browser Web-Worker pool is future work.
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ pnpm add @ecsia/scheduler @ecsia/core # not yet published — local workspace for now
23
+ ```
24
+
25
+ ## Links
26
+
27
+ - Repository & full docs: https://github.com/andymai/ecsia
28
+ - Umbrella package: [`ecsia`](https://github.com/andymai/ecsia)
29
+
30
+ ## License
31
+
32
+ [MIT](./LICENSE) © Andy Aragon
@@ -0,0 +1,49 @@
1
+ import { Op } from './op.js';
2
+ import type { CommandBuffer } from './buffer.js';
3
+ import type { ComponentFieldCodec } from './fields.js';
4
+ import type { ComponentDef, ComponentId, RelationId, Schema } from '@ecsia/schema';
5
+ import type { EntityHandle } from '@ecsia/core';
6
+ /** A staged structural intent (legacy seam, kept for compatibility). */
7
+ export interface StructuralIntent {
8
+ readonly op: Op;
9
+ }
10
+ export interface CommandSink {
11
+ flushAll(): void;
12
+ }
13
+ /** The single-thread sink: structural ops never deferred, so the flush is empty work. */
14
+ export declare const directApplySink: CommandSink;
15
+ /**
16
+ * The main-thread world verbs the apply path drives. Built from the World's
17
+ * public surface by the scheduler. Every call runs serial/main-thread (PHASE-2 flush slot).
18
+ */
19
+ export interface WorldApply {
20
+ isAlive(h: EntityHandle): boolean;
21
+ handleIndex(h: EntityHandle): number;
22
+ /** Place an already-reserved (alive) handle into the EMPTY archetype + emit Create. */
23
+ spawnReserved(h: EntityHandle): void;
24
+ despawn(h: EntityHandle): void;
25
+ /** id → registered ComponentDef (for migration + write-view). */
26
+ defOf(id: ComponentId): ComponentDef<Schema> | undefined;
27
+ /** Field codec for a component id (payload decode). */
28
+ codecOf(id: ComponentId): ComponentFieldCodec | undefined;
29
+ /** One migration adding several components. */
30
+ addMany(h: EntityHandle, defs: readonly ComponentDef<Schema>[]): void;
31
+ removeMany(h: EntityHandle, defs: readonly ComponentDef<Schema>[]): void;
32
+ /** Is `def` currently present on `h`? (SET_PAYLOAD requires presence). */
33
+ has(h: EntityHandle, def: ComponentDef<Schema>): boolean;
34
+ /** Write decoded field values into `h`'s current row + emit the `.changed` write-log entry. */
35
+ writePayload(h: EntityHandle, def: ComponentDef<Schema>, values: Record<string, unknown>): void;
36
+ /** Reclaim the unconsumed reservation tail after this worker's creates are applied. */
37
+ returnUnused(cb: CommandBuffer): void;
38
+ /** Relation apply (best-effort; relations land at ). */
39
+ addPair?(subject: EntityHandle, relationId: RelationId, target: EntityHandle, payload: Record<string, unknown> | undefined): void;
40
+ removePair?(subject: EntityHandle, relationId: RelationId, target: EntityHandle): void;
41
+ warn(message: string): void;
42
+ }
43
+ /**
44
+ * Apply every worker's command buffer to the world in fixed worker-index order. `world.phase` MUST be
45
+ * 'serial' (the scheduler flips it back before calling this). Returns the number of records applied
46
+ * (diagnostics).
47
+ */
48
+ export declare function flushAll(world: WorldApply, buffers: readonly CommandBuffer[]): void;
49
+ //# sourceMappingURL=apply.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/commands/apply.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,EAAE,EAAa,MAAM,SAAS,CAAA;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAElF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAI/C,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAA;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,IAAI,IAAI,CAAA;CACjB;AAED,yFAAyF;AACzF,eAAO,MAAM,eAAe,EAAE,WAI7B,CAAA;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,CAAC,EAAE,YAAY,GAAG,OAAO,CAAA;IACjC,WAAW,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,CAAA;IACpC,uFAAuF;IACvF,aAAa,CAAC,CAAC,EAAE,YAAY,GAAG,IAAI,CAAA;IACpC,OAAO,CAAC,CAAC,EAAE,YAAY,GAAG,IAAI,CAAA;IAC9B,iEAAiE;IACjE,KAAK,CAAC,EAAE,EAAE,WAAW,GAAG,YAAY,CAAC,MAAM,CAAC,GAAG,SAAS,CAAA;IACxD,uDAAuD;IACvD,OAAO,CAAC,EAAE,EAAE,WAAW,GAAG,mBAAmB,GAAG,SAAS,CAAA;IACzD,+CAA+C;IAC/C,OAAO,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,YAAY,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,CAAA;IACrE,UAAU,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,YAAY,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,CAAA;IACxE,0EAA0E;IAC1E,GAAG,CAAC,CAAC,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY,CAAC,MAAM,CAAC,GAAG,OAAO,CAAA;IACxD,+FAA+F;IAC/F,YAAY,CAAC,CAAC,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IAC/F,uFAAuF;IACvF,YAAY,CAAC,EAAE,EAAE,aAAa,GAAG,IAAI,CAAA;IACrC,wDAAwD;IACxD,OAAO,CAAC,CAAC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,IAAI,CAAA;IACjI,UAAU,CAAC,CAAC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAAA;IACtF,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAoBD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CAQnF"}
@@ -0,0 +1,211 @@
1
+ // Command-buffer flush + deterministic merge + validate-then-apply.
2
+ // The main thread merges every worker's buffer in FIXED worker-index order, applies each record
3
+ // serially (validate-then-apply, drop-if-dead with an in-flush tombstone set), coalesces per-entity
4
+ // adds/removes into ONE migration each, and returns unused reserved ids. This is what makes a
5
+ // multi-worker run SERIAL-EQUIVALENT to the single-threaded executor: encoding order across workers is
6
+ // nondeterministic, but applying order is fixed (ascending worker index, then append order).
7
+ import { Op, recordLen } from './op.js';
8
+ import { NO_ENTITY } from '@ecsia/core';
9
+ const NO_ENTITY_BITS = NO_ENTITY >>> 0;
10
+ /** The single-thread sink: structural ops never deferred, so the flush is empty work. */
11
+ export const directApplySink = {
12
+ flushAll() {
13
+ // Zero workers → zero command buffers → no-op.
14
+ },
15
+ };
16
+ function newPending() {
17
+ return { adds: [], removes: [], payloads: new Map() };
18
+ }
19
+ function pushUnique(arr, c) {
20
+ if (!arr.includes(c))
21
+ arr.push(c);
22
+ }
23
+ function removeFrom(arr, c) {
24
+ const i = arr.indexOf(c);
25
+ if (i >= 0)
26
+ arr.splice(i, 1);
27
+ }
28
+ /**
29
+ * Apply every worker's command buffer to the world in fixed worker-index order. `world.phase` MUST be
30
+ * 'serial' (the scheduler flips it back before calling this). Returns the number of records applied
31
+ * (diagnostics).
32
+ */
33
+ export function flushAll(world, buffers) {
34
+ const newlyCreated = new Set(); // (by handle bit-pattern)
35
+ const tombstones = new Set(); //
36
+ // Ascending workerIndex is the deterministic merge order. Buffers are passed in index order
37
+ // by the caller; sort defensively so the invariant holds regardless of array order.
38
+ const ordered = [...buffers].sort((a, b) => a.workerIndex - b.workerIndex);
39
+ for (const cb of ordered)
40
+ applyBuffer(world, cb, newlyCreated, tombstones);
41
+ for (const cb of ordered)
42
+ world.returnUnused(cb);
43
+ }
44
+ function validateSubject(world, h, newlyCreated, tombstones) {
45
+ if (newlyCreated.has(h))
46
+ return true; // reserved-and-created THIS flush → alive
47
+ if (tombstones.has(world.handleIndex(h))) {
48
+ world.warn(`command references entity destroyed earlier this flush (handle ${h})`);
49
+ return false;
50
+ }
51
+ if (!world.isAlive(h)) {
52
+ world.warn(`command references dead entity (handle ${h})`);
53
+ return false;
54
+ }
55
+ return true;
56
+ }
57
+ function applyBuffer(world, cb, newlyCreated, tombstones) {
58
+ const pending = new Map();
59
+ const words = cb.words;
60
+ let at = 0;
61
+ let appliedCreates = 0;
62
+ const drain = (h) => {
63
+ const p = pending.get(h);
64
+ if (p === undefined)
65
+ return;
66
+ // removes before adds so a remove-then-add of distinct ids lands in the final archetype.
67
+ if (p.removes.length > 0) {
68
+ const defs = p.removes.map((c) => world.defOf(c)).filter((d) => d !== undefined);
69
+ if (defs.length > 0)
70
+ world.removeMany(h, defs);
71
+ }
72
+ if (p.adds.length > 0) {
73
+ const defs = p.adds.map((c) => world.defOf(c)).filter((d) => d !== undefined);
74
+ if (defs.length > 0)
75
+ world.addMany(h, defs);
76
+ }
77
+ for (const [cid, values] of p.payloads) {
78
+ const def = world.defOf(cid);
79
+ if (def !== undefined)
80
+ world.writePayload(h, def, values);
81
+ }
82
+ pending.delete(h);
83
+ };
84
+ while (at < cb.head) {
85
+ const op = words[at];
86
+ const len = recordLen(words, at);
87
+ switch (op) {
88
+ case Op.CREATE: {
89
+ const h = words[at + 1];
90
+ // Belt-and-suspenders: a reserved handle is ALWAYS alive, so a
91
+ // well-formed OP_CREATE never names NO_ENTITY. Guard anyway so a corrupt/exhaustion-fabricated
92
+ // record can never reach spawnReserved(0xffffffff) → handleIndex(NO_ENTITY) → record-table
93
+ // corruption. Dropped: emits nothing, not counted toward appliedCreates (no reservation slot
94
+ // was consumed for it).
95
+ if (h >>> 0 === NO_ENTITY_BITS) {
96
+ world.warn('OP_CREATE names NO_ENTITY (reservation exhausted upstream); dropped');
97
+ break;
98
+ }
99
+ world.spawnReserved(h);
100
+ newlyCreated.add(h);
101
+ appliedCreates += 1;
102
+ break;
103
+ }
104
+ case Op.DESTROY: {
105
+ const h = words[at + 1];
106
+ if (validateSubject(world, h, newlyCreated, tombstones)) {
107
+ drain(h); // flush any pending structure before the entity disappears
108
+ world.despawn(h);
109
+ tombstones.add(world.handleIndex(h));
110
+ }
111
+ break;
112
+ }
113
+ case Op.ADD: {
114
+ const h = words[at + 1];
115
+ const cid = words[at + 2];
116
+ const f = words[at + 3];
117
+ if (validateSubject(world, h, newlyCreated, tombstones)) {
118
+ const p = pending.get(h) ?? setPending(pending, h);
119
+ removeFrom(p.removes, cid);
120
+ pushUnique(p.adds, cid);
121
+ if (f > 0) {
122
+ const codec = world.codecOf(cid);
123
+ if (codec !== undefined)
124
+ p.payloads.set(cid, codec.decode(words, at + 4));
125
+ }
126
+ }
127
+ break;
128
+ }
129
+ case Op.REMOVE: {
130
+ const h = words[at + 1];
131
+ const cid = words[at + 2];
132
+ if (validateSubject(world, h, newlyCreated, tombstones)) {
133
+ const p = pending.get(h) ?? setPending(pending, h);
134
+ removeFrom(p.adds, cid);
135
+ pushUnique(p.removes, cid);
136
+ p.payloads.delete(cid);
137
+ }
138
+ break;
139
+ }
140
+ case Op.SET_PAYLOAD: {
141
+ const h = words[at + 1];
142
+ const cid = words[at + 2];
143
+ if (validateSubject(world, h, newlyCreated, tombstones)) {
144
+ const codec = world.codecOf(cid);
145
+ const def = world.defOf(cid);
146
+ if (codec !== undefined && def !== undefined) {
147
+ const values = codec.decode(words, at + 4);
148
+ const p = pending.get(h);
149
+ if (p !== undefined && p.adds.includes(cid)) {
150
+ p.payloads.set(cid, values); // fold into the add's payload
151
+ }
152
+ else {
153
+ drain(h); // ensure the component is present at its current row before overwriting
154
+ if (world.has(h, def))
155
+ world.writePayload(h, def, values);
156
+ else
157
+ world.warn(`SET_PAYLOAD on absent component (handle ${h}, id ${cid})`);
158
+ }
159
+ }
160
+ }
161
+ break;
162
+ }
163
+ case Op.ADD_PAIR: {
164
+ const s = words[at + 1];
165
+ const rid = words[at + 2];
166
+ const t = words[at + 3];
167
+ // ADD_PAIR drops if EITHER subject or target is dead (a relation to a
168
+ // destroyed target is meaningless). Both go through the drop-if-dead gate.
169
+ if (validateSubject(world, s, newlyCreated, tombstones) &&
170
+ validateSubject(world, t, newlyCreated, tombstones)) {
171
+ drain(s);
172
+ if (world.addPair !== undefined) {
173
+ // Worker-path pair PAYLOADS are deferred — the worker encoder
174
+ // emits payloadWordCount=0 (no relation-schema replication yet), so there are no payload
175
+ // words to decode and the pair is applied with an undefined payload. recordLen() still skips
176
+ // the (zero) payload words correctly. The pair/presence/back-ref/mint are all applied.
177
+ world.addPair(s, rid, t, undefined);
178
+ }
179
+ else {
180
+ world.warn('ADD_PAIR encountered but relations are not wired');
181
+ }
182
+ }
183
+ break;
184
+ }
185
+ case Op.REMOVE_PAIR: {
186
+ const s = words[at + 1];
187
+ const rid = words[at + 2];
188
+ const t = words[at + 3];
189
+ if (validateSubject(world, s, newlyCreated, tombstones)) {
190
+ drain(s);
191
+ if (world.removePair !== undefined)
192
+ world.removePair(s, rid, t);
193
+ }
194
+ break;
195
+ }
196
+ default:
197
+ throw new Error(`corrupt command buffer: bad opcode ${op} at ${at}`);
198
+ }
199
+ at += len;
200
+ }
201
+ // Drain any entity still pending at end-of-buffer.
202
+ for (const key of [...pending.keys()])
203
+ drain(key);
204
+ cb.appliedCreateCount = appliedCreates;
205
+ }
206
+ function setPending(map, h) {
207
+ const p = newPending();
208
+ map.set(h, p);
209
+ return p;
210
+ }
211
+ //# sourceMappingURL=apply.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply.js","sourceRoot":"","sources":["../../src/commands/apply.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ import type { EntityHandle, EntityReservation } from '@ecsia/core';
2
+ /** A per-worker reservation block consumed by OP_CREATE. */
3
+ export interface BufferReservation {
4
+ readonly handles: readonly EntityHandle[];
5
+ }
6
+ export interface CommandBuffer {
7
+ /** Worker index this buffer belongs to (0..workers-1). Fixes merge order. */
8
+ readonly workerIndex: number;
9
+ /** u32 words. SAB-backed in the threaded path, plain AB in the fallback. */
10
+ words: Uint32Array;
11
+ /** Write head: index of the next free u32 slot. Reset to 0 each wave. */
12
+ head: number;
13
+ /** Count of records appended this wave (diagnostics / merge bound). */
14
+ recordCount: number;
15
+ /** The worker's reservation block for this wave. Consumed by OP_CREATE in append order. */
16
+ reservation: BufferReservation;
17
+ /** Cursor into `reservation.handles`: next unused reserved handle. */
18
+ reservationCursor: number;
19
+ /** Count of OP_CREATE records actually applied (set by the apply path for returnUnused). */
20
+ appliedCreateCount: number;
21
+ /** The EntityReservation block reserved for this buffer's wave (main-thread bookkeeping for ). */
22
+ lastReservation?: EntityReservation;
23
+ /**
24
+ * When true the backing is a FIXED-SIZE SharedArrayBuffer that MUST NOT be reassigned mid-wave: the
25
+ * main thread reads records in place over the same SAB, so growing the worker-side view off the SAB
26
+ * would point `head` past the shared backing and either lose records or corrupt the apply decode
27
+ * (review issue #3). On a fixed buffer `ensureWords` caps instead of growing and sets `overflowed`.
28
+ */
29
+ fixed: boolean;
30
+ /** Set by `ensureWords` when a fixed (SAB) buffer could not fit a record: encoding was capped. */
31
+ overflowed: boolean;
32
+ /** Latch so the encoder emits the overflow diagnostic ONCE per wave; cleared by `resetBuffer`. */
33
+ overflowWarned?: boolean;
34
+ }
35
+ export declare function makeCommandBuffer(workerIndex: number, initialWords: number, shared: boolean): CommandBuffer;
36
+ /**: head→0, retain backing. Steady-state encoding is allocation-free. */
37
+ export declare function resetBuffer(cb: CommandBuffer): void;
38
+ /**
39
+ *: ensure room for `need` more words. Returns true iff the caller may now write
40
+ * `need` words at `cb.head`.
41
+ *
42
+ * Plain-AB (growable) buffers double on overflow and always return true — the no-sab
43
+ * transport reads `{ words, head }` after the fence so a reallocated private buffer is fine.
44
+ *
45
+ * FIXED (SAB-backed) buffers MUST NOT reassign `cb.words` off the shared backing (review issue #3): the
46
+ * main thread reads the same SAB in place up to `head`, so a worker-private grow would (a) hide the
47
+ * overflow records from the main thread and (b) push `head` past the SAB length → NaN-opcode crash in
48
+ * `recordLen`. Instead we CAP: set `overflowed`, leave `head` untouched, and return false so the encoder
49
+ * appends NOTHING. The buffer therefore always satisfies `head <= words.length`, and the overflow is a
50
+ * loud diagnostic rather than silent loss + crash.
51
+ */
52
+ export declare function ensureWords(cb: CommandBuffer, need: number): boolean;
53
+ //# sourceMappingURL=buffer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buffer.d.ts","sourceRoot":"","sources":["../../src/commands/buffer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAElE,4DAA4D;AAC5D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;CAC1C;AAED,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,4EAA4E;IAC5E,KAAK,EAAE,WAAW,CAAA;IAClB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAA;IACZ,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAA;IACnB,2FAA2F;IAC3F,WAAW,EAAE,iBAAiB,CAAA;IAC9B,sEAAsE;IACtE,iBAAiB,EAAE,MAAM,CAAA;IACzB,4FAA4F;IAC5F,kBAAkB,EAAE,MAAM,CAAA;IAC1B,kGAAkG;IAClG,eAAe,CAAC,EAAE,iBAAiB,CAAA;IACnC;;;;;OAKG;IACH,KAAK,EAAE,OAAO,CAAA;IACd,kGAAkG;IAClG,UAAU,EAAE,OAAO,CAAA;IACnB,kGAAkG;IAClG,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAID,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,aAAa,CAe3G;AAED,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,EAAE,EAAE,aAAa,GAAG,IAAI,CAOnD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAYpE"}
@@ -0,0 +1,66 @@
1
+ // Per-worker command buffer: a plain (or SAB-backed) Uint32Array a worker
2
+ // appends structural-intent records into mid-wave and the main thread replays at the serial flush.
3
+ //
4
+ // Backing choice: the buffer is written ONLY by its owning worker and read
5
+ // ONLY by the main thread AFTER the wave fence — no concurrent access ever — so no atomics are needed
6
+ // on it. In the SAB/Atomics runtime we back it with a *fixed-size* SharedArrayBuffer so the main
7
+ // thread reads the worker's records in place (no per-flush transfer); growth past that cap spills to
8
+ // a plain-AB overflow which the worker reports back via its `head`. In the
9
+ // postMessage fallback the backing is a plain ArrayBuffer transferred at flush. Either way the apply
10
+ // path (apply.ts) reads `{ words, head }` byte-for-byte identically.
11
+ const EMPTY_RESERVATION = { handles: [] };
12
+ export function makeCommandBuffer(workerIndex, initialWords, shared) {
13
+ const bytes = Math.max(initialWords, 16) * 4;
14
+ const backing = shared ? new SharedArrayBuffer(bytes) : new ArrayBuffer(bytes);
15
+ return {
16
+ workerIndex,
17
+ words: new Uint32Array(backing),
18
+ head: 0,
19
+ recordCount: 0,
20
+ reservation: EMPTY_RESERVATION,
21
+ reservationCursor: 0,
22
+ appliedCreateCount: 0,
23
+ fixed: shared,
24
+ overflowed: false,
25
+ overflowWarned: false,
26
+ };
27
+ }
28
+ /**: head→0, retain backing. Steady-state encoding is allocation-free. */
29
+ export function resetBuffer(cb) {
30
+ cb.head = 0;
31
+ cb.recordCount = 0;
32
+ cb.reservationCursor = 0;
33
+ cb.appliedCreateCount = 0;
34
+ cb.overflowed = false;
35
+ cb.overflowWarned = false;
36
+ }
37
+ /**
38
+ *: ensure room for `need` more words. Returns true iff the caller may now write
39
+ * `need` words at `cb.head`.
40
+ *
41
+ * Plain-AB (growable) buffers double on overflow and always return true — the no-sab
42
+ * transport reads `{ words, head }` after the fence so a reallocated private buffer is fine.
43
+ *
44
+ * FIXED (SAB-backed) buffers MUST NOT reassign `cb.words` off the shared backing (review issue #3): the
45
+ * main thread reads the same SAB in place up to `head`, so a worker-private grow would (a) hide the
46
+ * overflow records from the main thread and (b) push `head` past the SAB length → NaN-opcode crash in
47
+ * `recordLen`. Instead we CAP: set `overflowed`, leave `head` untouched, and return false so the encoder
48
+ * appends NOTHING. The buffer therefore always satisfies `head <= words.length`, and the overflow is a
49
+ * loud diagnostic rather than silent loss + crash.
50
+ */
51
+ export function ensureWords(cb, need) {
52
+ if (cb.head + need <= cb.words.length)
53
+ return true;
54
+ if (cb.fixed) {
55
+ cb.overflowed = true;
56
+ return false;
57
+ }
58
+ let newLen = cb.words.length;
59
+ while (cb.head + need > newLen)
60
+ newLen = newLen === 0 ? need : newLen * 2;
61
+ const next = new Uint32Array(newLen);
62
+ next.set(cb.words.subarray(0, cb.head));
63
+ cb.words = next;
64
+ return true;
65
+ }
66
+ //# sourceMappingURL=buffer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buffer.js","sourceRoot":"","sources":["../../src/commands/buffer.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,mGAAmG;AACnG,EAAE;AACF,2EAA2E;AAC3E,sGAAsG;AACtG,iGAAiG;AACjG,qGAAqG;AACrG,2EAA2E;AAC3E,qGAAqG;AACrG,qEAAqE;AAuCrE,MAAM,iBAAiB,GAAsB,EAAE,OAAO,EAAE,EAAE,EAAE,CAAA;AAE5D,MAAM,UAAU,iBAAiB,CAAC,WAAmB,EAAE,YAAoB,EAAE,MAAe;IAC1F,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,EAAE,CAAC,GAAG,CAAC,CAAA;IAC5C,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,KAAK,CAAC,CAAA;IAC9E,OAAO;QACL,WAAW;QACX,KAAK,EAAE,IAAI,WAAW,CAAC,OAAO,CAAC;QAC/B,IAAI,EAAE,CAAC;QACP,WAAW,EAAE,CAAC;QACd,WAAW,EAAE,iBAAiB;QAC9B,iBAAiB,EAAE,CAAC;QACpB,kBAAkB,EAAE,CAAC;QACrB,KAAK,EAAE,MAAM;QACb,UAAU,EAAE,KAAK;QACjB,cAAc,EAAE,KAAK;KACtB,CAAA;AACH,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,WAAW,CAAC,EAAiB;IAC3C,EAAE,CAAC,IAAI,GAAG,CAAC,CAAA;IACX,EAAE,CAAC,WAAW,GAAG,CAAC,CAAA;IAClB,EAAE,CAAC,iBAAiB,GAAG,CAAC,CAAA;IACxB,EAAE,CAAC,kBAAkB,GAAG,CAAC,CAAA;IACzB,EAAE,CAAC,UAAU,GAAG,KAAK,CAAA;IACrB,EAAE,CAAC,cAAc,GAAG,KAAK,CAAA;AAC3B,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,WAAW,CAAC,EAAiB,EAAE,IAAY;IACzD,IAAI,EAAE,CAAC,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAClD,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC;QACb,EAAE,CAAC,UAAU,GAAG,IAAI,CAAA;QACpB,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,MAAM,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,CAAA;IAC5B,OAAO,EAAE,CAAC,IAAI,GAAG,IAAI,GAAG,MAAM;QAAE,MAAM,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAA;IACzE,MAAM,IAAI,GAAG,IAAI,WAAW,CAAC,MAAM,CAAC,CAAA;IACpC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAA;IACvC,EAAE,CAAC,KAAK,GAAG,IAAI,CAAA;IACf,OAAO,IAAI,CAAA;AACb,CAAC"}
@@ -0,0 +1,30 @@
1
+ import type { CommandBuffer } from './buffer.js';
2
+ import type { ComponentFieldCodec } from './fields.js';
3
+ import type { ComponentDef, ComponentId, RelationId, Schema } from '@ecsia/schema';
4
+ import type { EntityHandle } from '@ecsia/core';
5
+ /** Per-component encode metadata the worker resolves from its replicated registry. */
6
+ export interface ComponentEncodeInfo {
7
+ readonly id: ComponentId;
8
+ readonly codec: ComponentFieldCodec;
9
+ }
10
+ export interface CommandEncoder {
11
+ /** Reserve-and-return a usable handle NOW; emits OP_CREATE. Mid-wave safe. */
12
+ create(): EntityHandle;
13
+ destroy(h: EntityHandle): void;
14
+ add(h: EntityHandle, def: ComponentDef<Schema>, init?: Record<string, unknown>): void;
15
+ remove(h: EntityHandle, def: ComponentDef<Schema>): void;
16
+ setPayload(h: EntityHandle, def: ComponentDef<Schema>, values: Record<string, unknown>): void;
17
+ setRelation(subject: EntityHandle, relationId: RelationId, target: EntityHandle, payload?: Record<string, unknown>): void;
18
+ unsetRelation(subject: EntityHandle, relationId: RelationId, target: EntityHandle): void;
19
+ }
20
+ export interface EncoderEnv {
21
+ readonly cb: CommandBuffer;
22
+ /** Resolve a registered ComponentDef → its dense id + field codec. */
23
+ infoOf(def: ComponentDef<Schema>): ComponentEncodeInfo;
24
+ /** Field codec for a relation payload schema (or undefined for a tag relation). */
25
+ relationCodec(relationId: RelationId): ComponentFieldCodec | undefined;
26
+ /** Dev diagnostic sink (e.g. reservation exhaustion). */
27
+ warn(message: string): void;
28
+ }
29
+ export declare function makeEncoder(env: EncoderEnv): CommandEncoder;
30
+ //# sourceMappingURL=encode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../src/commands/encode.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAElF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE/C,sFAAsF;AACtF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,WAAW,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAA;CACpC;AAED,MAAM,WAAW,cAAc;IAC7B,8EAA8E;IAC9E,MAAM,IAAI,YAAY,CAAA;IACtB,OAAO,CAAC,CAAC,EAAE,YAAY,GAAG,IAAI,CAAA;IAC9B,GAAG,CAAC,CAAC,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACrF,MAAM,CAAC,CAAC,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY,CAAC,MAAM,CAAC,GAAG,IAAI,CAAA;IACxD,UAAU,CAAC,CAAC,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IAC7F,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACzH,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAAA;CACzF;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,aAAa,CAAA;IAC1B,sEAAsE;IACtE,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,MAAM,CAAC,GAAG,mBAAmB,CAAA;IACtD,mFAAmF;IACnF,aAAa,CAAC,UAAU,EAAE,UAAU,GAAG,mBAAmB,GAAG,SAAS,CAAA;IACtE,yDAAyD;IACzD,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,UAAU,GAAG,cAAc,CAoG3D"}
@@ -0,0 +1,108 @@
1
+ // The worker-side encode API: the methods a system calls mid-wave. NONE
2
+ // mutates shared structure — each appends one record to the owning worker's buffer (and, for create,
3
+ // consumes a reserved handle). The ergonomic surface (entity.add, world.spawn inside a worker) routes
4
+ // here via the worker's structuralOp seam (workers/worker-context.ts).
5
+ import { Op } from './op.js';
6
+ import { ensureWords } from './buffer.js';
7
+ import { NO_ENTITY } from '@ecsia/core';
8
+ export function makeEncoder(env) {
9
+ const { cb } = env;
10
+ /** Emit the overflow diagnostic ONCE per wave (the first capped record); subsequent caps are silent. */
11
+ function onOverflow() {
12
+ if (!cb.overflowed || cb.overflowWarned)
13
+ return;
14
+ cb.overflowWarned = true;
15
+ env.warn('command-buffer: fixed (SAB) buffer full; record dropped (raise commandWords)');
16
+ }
17
+ function create() {
18
+ if (cb.reservationCursor >= cb.reservation.handles.length) {
19
+ env.warn('command-buffer: reservation exhausted; raise maxSpawnsPerWave (spawn capped)');
20
+ return NO_ENTITY;
21
+ }
22
+ // Check capacity BEFORE consuming the reservation handle: an overflow must not burn a reserved id
23
+ // (and must emit no OP_CREATE) so the handle is reclaimed by returnUnused, not leaked.
24
+ if (!ensureWords(cb, 2)) {
25
+ onOverflow();
26
+ return NO_ENTITY;
27
+ }
28
+ const h = cb.reservation.handles[cb.reservationCursor];
29
+ cb.reservationCursor += 1;
30
+ const w = cb.head;
31
+ cb.words[w] = Op.CREATE;
32
+ cb.words[w + 1] = h;
33
+ cb.head += 2;
34
+ cb.recordCount += 1;
35
+ return h;
36
+ }
37
+ function destroy(h) {
38
+ if (!ensureWords(cb, 2))
39
+ return onOverflow();
40
+ const w = cb.head;
41
+ cb.words[w] = Op.DESTROY;
42
+ cb.words[w + 1] = h;
43
+ cb.head += 2;
44
+ cb.recordCount += 1;
45
+ }
46
+ function fieldRecord(op, h, def, init) {
47
+ const info = env.infoOf(def);
48
+ const f = info.codec.totalWords;
49
+ if (!ensureWords(cb, 4 + f))
50
+ return onOverflow();
51
+ const w = cb.head;
52
+ cb.words[w] = op;
53
+ cb.words[w + 1] = h;
54
+ cb.words[w + 2] = info.id;
55
+ cb.words[w + 3] = f;
56
+ info.codec.encode(init, cb.words, w + 4);
57
+ cb.head += 4 + f;
58
+ cb.recordCount += 1;
59
+ }
60
+ function remove(h, def) {
61
+ const info = env.infoOf(def);
62
+ if (!ensureWords(cb, 3))
63
+ return onOverflow();
64
+ const w = cb.head;
65
+ cb.words[w] = Op.REMOVE;
66
+ cb.words[w + 1] = h;
67
+ cb.words[w + 2] = info.id;
68
+ cb.head += 3;
69
+ cb.recordCount += 1;
70
+ }
71
+ function setRelation(subject, relationId, target, payload) {
72
+ const codec = env.relationCodec(relationId);
73
+ const p = codec?.totalWords ?? 0;
74
+ if (!ensureWords(cb, 5 + p))
75
+ return onOverflow();
76
+ const w = cb.head;
77
+ cb.words[w] = Op.ADD_PAIR;
78
+ cb.words[w + 1] = subject;
79
+ cb.words[w + 2] = relationId;
80
+ cb.words[w + 3] = target;
81
+ cb.words[w + 4] = p;
82
+ if (codec !== undefined && p > 0)
83
+ codec.encode(payload, cb.words, w + 5);
84
+ cb.head += 5 + p;
85
+ cb.recordCount += 1;
86
+ }
87
+ function unsetRelation(subject, relationId, target) {
88
+ if (!ensureWords(cb, 4))
89
+ return onOverflow();
90
+ const w = cb.head;
91
+ cb.words[w] = Op.REMOVE_PAIR;
92
+ cb.words[w + 1] = subject;
93
+ cb.words[w + 2] = relationId;
94
+ cb.words[w + 3] = target;
95
+ cb.head += 4;
96
+ cb.recordCount += 1;
97
+ }
98
+ return {
99
+ create,
100
+ destroy,
101
+ add: (h, def, init) => fieldRecord(Op.ADD, h, def, init),
102
+ remove,
103
+ setPayload: (h, def, values) => fieldRecord(Op.SET_PAYLOAD, h, def, values),
104
+ setRelation,
105
+ unsetRelation,
106
+ };
107
+ }
108
+ //# sourceMappingURL=encode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encode.js","sourceRoot":"","sources":["../../src/commands/encode.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,qGAAqG;AACrG,sGAAsG;AACtG,uEAAuE;AAEvE,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAA;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAIzC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AA8BvC,MAAM,UAAU,WAAW,CAAC,GAAe;IACzC,MAAM,EAAE,EAAE,EAAE,GAAG,GAAG,CAAA;IAElB,wGAAwG;IACxG,SAAS,UAAU;QACjB,IAAI,CAAC,EAAE,CAAC,UAAU,IAAI,EAAE,CAAC,cAAc;YAAE,OAAM;QAC/C,EAAE,CAAC,cAAc,GAAG,IAAI,CAAA;QACxB,GAAG,CAAC,IAAI,CAAC,8EAA8E,CAAC,CAAA;IAC1F,CAAC;IAED,SAAS,MAAM;QACb,IAAI,EAAE,CAAC,iBAAiB,IAAI,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YAC1D,GAAG,CAAC,IAAI,CAAC,8EAA8E,CAAC,CAAA;YACxF,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,kGAAkG;QAClG,uFAAuF;QACvF,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;YACxB,UAAU,EAAE,CAAA;YACZ,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,MAAM,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,iBAAiB,CAAE,CAAA;QACvD,EAAE,CAAC,iBAAiB,IAAI,CAAC,CAAA;QACzB,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CAAA;QACjB,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,MAAM,CAAA;QACvB,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAW,CAAA;QAC7B,EAAE,CAAC,IAAI,IAAI,CAAC,CAAA;QACZ,EAAE,CAAC,WAAW,IAAI,CAAC,CAAA;QACnB,OAAO,CAAC,CAAA;IACV,CAAC;IAED,SAAS,OAAO,CAAC,CAAe;QAC9B,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,CAAC,CAAC;YAAE,OAAO,UAAU,EAAE,CAAA;QAC5C,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CAAA;QACjB,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,OAAO,CAAA;QACxB,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAW,CAAA;QAC7B,EAAE,CAAC,IAAI,IAAI,CAAC,CAAA;QACZ,EAAE,CAAC,WAAW,IAAI,CAAC,CAAA;IACrB,CAAC;IAED,SAAS,WAAW,CAAC,EAA2B,EAAE,CAAe,EAAE,GAAyB,EAAE,IAA8B;QAC1H,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAC5B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAA;QAC/B,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;YAAE,OAAO,UAAU,EAAE,CAAA;QAChD,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CAAA;QACjB,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAA;QAChB,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAW,CAAA;QAC7B,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,EAAY,CAAA;QACnC,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;QACnB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;QACxC,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAA;QAChB,EAAE,CAAC,WAAW,IAAI,CAAC,CAAA;IACrB,CAAC;IAED,SAAS,MAAM,CAAC,CAAe,EAAE,GAAyB;QACxD,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAC5B,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,CAAC,CAAC;YAAE,OAAO,UAAU,EAAE,CAAA;QAC5C,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CAAA;QACjB,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,MAAM,CAAA;QACvB,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAW,CAAA;QAC7B,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,EAAY,CAAA;QACnC,EAAE,CAAC,IAAI,IAAI,CAAC,CAAA;QACZ,EAAE,CAAC,WAAW,IAAI,CAAC,CAAA;IACrB,CAAC;IAED,SAAS,WAAW,CAAC,OAAqB,EAAE,UAAsB,EAAE,MAAoB,EAAE,OAAiC;QACzH,MAAM,KAAK,GAAG,GAAG,CAAC,aAAa,CAAC,UAAU,CAAC,CAAA;QAC3C,MAAM,CAAC,GAAG,KAAK,EAAE,UAAU,IAAI,CAAC,CAAA;QAChC,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;YAAE,OAAO,UAAU,EAAE,CAAA;QAChD,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CAAA;QACjB,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAA;QACzB,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,OAAiB,CAAA;QACnC,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,UAAoB,CAAA;QACtC,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,MAAgB,CAAA;QAClC,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;QACnB,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,GAAG,CAAC;YAAE,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;QACxE,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAA;QAChB,EAAE,CAAC,WAAW,IAAI,CAAC,CAAA;IACrB,CAAC;IAED,SAAS,aAAa,CAAC,OAAqB,EAAE,UAAsB,EAAE,MAAoB;QACxF,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,CAAC,CAAC;YAAE,OAAO,UAAU,EAAE,CAAA;QAC5C,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CAAA;QACjB,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,WAAW,CAAA;QAC5B,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,OAAiB,CAAA;QACnC,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,UAAoB,CAAA;QACtC,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,MAAgB,CAAA;QAClC,EAAE,CAAC,IAAI,IAAI,CAAC,CAAA;QACZ,EAAE,CAAC,WAAW,IAAI,CAAC,CAAA;IACrB,CAAC;IAED,OAAO;QACL,MAAM;QACN,OAAO;QACP,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC;QACxD,MAAM;QACN,UAAU,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC;QAC3E,WAAW;QACX,aAAa;KACd,CAAA;AACH,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { ComponentDef, Schema } from '@ecsia/schema';
2
+ interface FieldCodec {
3
+ readonly name: string;
4
+ /** Words this field occupies (1, or 2 for f64; `len`/`2*len` for vec). */
5
+ readonly words: number;
6
+ /** Encode the JS value into `out[at..at+words)`. */
7
+ readonly encode: (value: unknown, out: Uint32Array, at: number) => void;
8
+ /** Decode `words[at..at+words)` back to the JS value the accessor write-view expects. */
9
+ readonly decode: (words: Uint32Array, at: number) => unknown;
10
+ }
11
+ export interface ComponentFieldCodec {
12
+ /** Total payload words for the component (sum of field words; 0 for a tag). */
13
+ readonly totalWords: number;
14
+ readonly fields: readonly FieldCodec[];
15
+ /** Encode an `init` object → field words written at `out[at..]`; returns words written. */
16
+ encode(init: Record<string, unknown> | undefined, out: Uint32Array, at: number): number;
17
+ /** Decode `words[at..]` → a `{ field: value }` object for the accessor write-view. */
18
+ decode(words: Uint32Array, at: number): Record<string, unknown>;
19
+ }
20
+ export declare function buildFieldCodec(def: ComponentDef<Schema>): ComponentFieldCodec;
21
+ export {};
22
+ //# sourceMappingURL=fields.d.ts.map