koota-kit 0.9.1 → 0.9.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.3 (2026-10-15)
4
+
5
+ <!-- Release notes generated using configuration in .github/release.yml at main -->
6
+
7
+ ## What's Changed
8
+ * fix: guard coordination against mismatched Koota dispatch by @jbdevprimary in https://github.com/jbcom/koota-kit/pull/86
9
+
10
+
11
+ **Full Changelog**: https://github.com/jbcom/koota-kit/compare/v0.9.2...v0.9.3
12
+
13
+ ## 0.9.2 (2026-10-14)
14
+
15
+ <!-- Release notes generated using configuration in .github/release.yml at main -->
16
+
17
+ ## What's Changed
18
+ * test(spatial): preserve invalid-provider diagnostic precedence by @jbdevprimary in https://github.com/jbcom/koota-kit/pull/85
19
+
20
+
21
+ **Full Changelog**: https://github.com/jbcom/koota-kit/compare/v0.9.1...v0.9.2
22
+
3
23
  ## 0.9.1 (2026-10-14)
4
24
 
5
25
  <!-- Release notes generated using configuration in .github/release.yml at main -->
package/README.md CHANGED
@@ -138,6 +138,13 @@ ID. Listener errors occur after a committed publication; retrieve a token with
138
138
  `getRegistration(id)` if registration notification threw before `register`
139
139
  could return it.
140
140
 
141
+ Use one Koota runtime and module format per process. Koota 0.6.6 replaces numeric
142
+ entity dispatch when the opposite ESM/CommonJS peer format loads. Coordination
143
+ then rejects live reads, writes, and cleanup with a dispatch-realm error;
144
+ `getSnapshot()` still returns its last committed view. Raw callback writes may
145
+ already have happened, but failed cleanup never reports successful disposal.
146
+ The independent `koota-kit/spatial` entry can be imported in either format.
147
+
141
148
  The [caller composition proof](docs/INTEGRATION_PROOF.md) documents the optional
142
149
  registry consumer, copied figure inputs, immutable geometry captures and the
143
150
  limits of open-surface integration evidence.
@@ -121,16 +121,79 @@ function bindCoordination(world) {
121
121
  if (!world?.isInitialized) {
122
122
  fail("a live Koota World is required.");
123
123
  }
124
+ // Koota's numeric Entity methods close over their imported peer universe.
125
+ // Opposite ESM/CJS imports replace those global methods, even before this
126
+ // module first loads. Pointer stability alone therefore cannot attest them.
127
+ const dispatch = {
128
+ get: 0..get,
129
+ has: 0..has,
130
+ add: 0..add,
131
+ set: 0..set,
132
+ remove: 0..remove,
133
+ };
134
+ const assertRealm = () => {
135
+ if (koota_1.universe.worlds[world.id] !== world ||
136
+ 0..get !== dispatch.get ||
137
+ 0..has !== dispatch.has ||
138
+ 0..add !== dispatch.add ||
139
+ 0..set !== dispatch.set ||
140
+ 0..remove !== dispatch.remove) {
141
+ fail("the Koota entity dispatch realm no longer matches the borrowed World.");
142
+ }
143
+ };
144
+ assertRealm();
124
145
  const marker = Object.freeze({});
146
+ let holder;
147
+ let holderId;
148
+ let store;
149
+ const assertAttested = () => {
150
+ assertRealm();
151
+ const markers = world.get(exports.CoordinationBinding)?.markers;
152
+ let matches = false;
153
+ try {
154
+ matches =
155
+ world.has(holder) &&
156
+ (0, koota_1.getStore)(world, exports.CoordinationBinding) === store &&
157
+ store.markers[holderId] === markers &&
158
+ dispatch.get.call(holder, exports.CoordinationBinding)?.markers === markers;
159
+ }
160
+ catch {
161
+ // A foreign peer may have no World at this numeric ID at all.
162
+ }
163
+ assertRealm();
164
+ if (!matches)
165
+ fail("the Koota entity dispatch realm cannot attest the World marker.");
166
+ };
167
+ const assertOwned = () => {
168
+ assertAttested();
169
+ const markers = world.get(exports.CoordinationBinding)?.markers;
170
+ if (!markers?.includes(marker)) {
171
+ fail("the binding no longer owns its World marker.");
172
+ }
173
+ return markers;
174
+ };
125
175
  const existingMarkers = world.get(exports.CoordinationBinding)?.markers ?? [];
126
176
  const markers = [...existingMarkers, marker];
127
177
  try {
178
+ assertRealm();
128
179
  if (world.has(exports.CoordinationBinding))
129
180
  world.set(exports.CoordinationBinding, { markers });
130
181
  else
131
182
  world.add((0, exports.CoordinationBinding)({ markers }));
183
+ assertRealm();
184
+ store = (0, koota_1.getStore)(world, exports.CoordinationBinding);
185
+ // World traits live on an existing excluded entity. Find only our marker
186
+ // by direct store identity once; later checks use this cached holder in O(1).
187
+ const holders = world.entities.filter((entity) => store.markers[(0, koota_1.unpackEntity)(entity).entityId] === markers);
188
+ if (holders.length !== 1)
189
+ fail("the Koota entity dispatch realm has no unique marker holder.");
190
+ holder = holders[0];
191
+ holderId = (0, koota_1.unpackEntity)(holder).entityId;
192
+ assertOwned();
132
193
  }
133
194
  catch (error) {
195
+ // Direct World APIs close over the original World even if a callback
196
+ // changed entity dispatch before we could attest its numeric holder.
134
197
  const currentMarkers = world.get(exports.CoordinationBinding)?.markers;
135
198
  if (world.isInitialized && currentMarkers?.includes(marker)) {
136
199
  const remaining = currentMarkers.filter((candidate) => candidate !== marker);
@@ -142,6 +205,7 @@ function bindCoordination(world) {
142
205
  throw error;
143
206
  }
144
207
  let disposed = false;
208
+ let cleanupMarkerRemoved = false;
145
209
  let notifying = false;
146
210
  let mutating = false;
147
211
  let nextRegistrationGeneration = 0;
@@ -158,12 +222,15 @@ function bindCoordination(world) {
158
222
  };
159
223
  const assertMutable = () => {
160
224
  assertActive();
161
- if (!world.isInitialized || !world.get(exports.CoordinationBinding)?.markers.includes(marker)) {
225
+ if (!world.isInitialized)
162
226
  fail("the binding no longer owns its World marker.");
163
- }
227
+ assertOwned();
164
228
  };
165
229
  const isCurrent = (registration) => {
166
- if (!world.isInitialized || !world.has(registration.entity))
230
+ if (!world.isInitialized)
231
+ return false;
232
+ assertOwned();
233
+ if (!world.has(registration.entity))
167
234
  return false;
168
235
  return stateIsOwned(registration.entity.get(exports.CoordinationRegistration), marker, registration.state.generation);
169
236
  };
@@ -190,6 +257,16 @@ function bindCoordination(world) {
190
257
  errors.push(error);
191
258
  }
192
259
  }
260
+ // Listener failures occur after commit. A listener may also change peer
261
+ // dispatch without throwing; report that boundary to the caller too.
262
+ if (world.isInitialized) {
263
+ try {
264
+ assertOwned();
265
+ }
266
+ catch (error) {
267
+ errors.push(error);
268
+ }
269
+ }
193
270
  }
194
271
  finally {
195
272
  notifying = false;
@@ -226,6 +303,7 @@ function bindCoordination(world) {
226
303
  const capabilities = copyCapabilities(descriptor.capabilities);
227
304
  if (registrationsById.has(id))
228
305
  fail(`stable id "${id}" is already registered.`);
306
+ assertOwned();
229
307
  if (entity.has(exports.CoordinationRegistration)) {
230
308
  fail("entity already carries a coordination registration trait.");
231
309
  }
@@ -254,15 +332,20 @@ function bindCoordination(world) {
254
332
  mutating = true;
255
333
  try {
256
334
  try {
335
+ assertOwned();
257
336
  entity.add((0, exports.CoordinationRegistration)(state));
337
+ assertOwned();
258
338
  }
259
339
  catch (error) {
260
- const current = entity.get(exports.CoordinationRegistration);
340
+ // These captured peer functions use their original lexical universe;
341
+ // rollback only the trait this attempted registration placed there.
342
+ const current = dispatch.get.call(entity, exports.CoordinationRegistration);
261
343
  if (stateIsOwned(current, marker, registrationGeneration)) {
262
- entity.remove(exports.CoordinationRegistration);
344
+ dispatch.remove.call(entity, exports.CoordinationRegistration);
263
345
  }
264
346
  throw error;
265
347
  }
348
+ assertOwned();
266
349
  nextRegistrationGeneration = registrationGeneration;
267
350
  registrationsById.set(id, registration);
268
351
  registrationsByToken.set(token, registration);
@@ -294,6 +377,7 @@ function bindCoordination(world) {
294
377
  }
295
378
  const changed = prepared.some(({ registration, completed }) => !registration.completed ||
296
379
  !sameRevisions(registration.completed.revisions, completed.revisions));
380
+ assertOwned();
297
381
  if (!changed)
298
382
  return snapshot;
299
383
  const snapshotRevision = nextCounter(nextSnapshotRevision, "snapshot revision");
@@ -312,8 +396,11 @@ function bindCoordination(world) {
312
396
  mutating = true;
313
397
  try {
314
398
  for (const { registration, state } of preparedStates) {
399
+ assertOwned();
315
400
  registration.entity.set(exports.CoordinationRegistration, state);
401
+ assertOwned();
316
402
  }
403
+ assertOwned();
317
404
  for (const { registration, completed } of preparedStates) {
318
405
  registration.completed = completed;
319
406
  }
@@ -329,7 +416,9 @@ function bindCoordination(world) {
329
416
  const snapshotRevision = nextCounter(nextSnapshotRevision, "snapshot revision");
330
417
  mutating = true;
331
418
  try {
419
+ assertOwned();
332
420
  registration.entity.remove(exports.CoordinationRegistration);
421
+ assertOwned();
333
422
  registrationsById.delete(registration.id);
334
423
  registrationsByToken.delete(token);
335
424
  return commit(snapshotRevision);
@@ -340,12 +429,16 @@ function bindCoordination(world) {
340
429
  };
341
430
  const prune = () => {
342
431
  assertActive();
432
+ if (world.isInitialized)
433
+ assertOwned();
343
434
  const stale = [...registrationsById.values()].filter((registration) => !isCurrent(registration));
344
435
  if (stale.length === 0)
345
436
  return snapshot;
346
437
  const snapshotRevision = nextCounter(nextSnapshotRevision, "snapshot revision");
347
438
  mutating = true;
348
439
  try {
440
+ if (world.isInitialized)
441
+ assertOwned();
349
442
  for (const registration of stale) {
350
443
  registrationsById.delete(registration.id);
351
444
  registrationsByToken.delete(registration.token);
@@ -357,10 +450,14 @@ function bindCoordination(world) {
357
450
  }
358
451
  };
359
452
  const getEntity = (id) => {
453
+ if (world.isInitialized && !disposed)
454
+ assertOwned();
360
455
  const registration = registrationsById.get(id);
361
456
  return registration && isCurrent(registration) ? registration.entity : undefined;
362
457
  };
363
458
  const getRegistration = (id) => {
459
+ if (world.isInitialized && !disposed)
460
+ assertOwned();
364
461
  const registration = registrationsById.get(id);
365
462
  return registration && isCurrent(registration) ? registration.token : undefined;
366
463
  };
@@ -377,24 +474,49 @@ function bindCoordination(world) {
377
474
  if (disposed)
378
475
  return;
379
476
  assertActive();
477
+ if (world.isInitialized) {
478
+ if (cleanupMarkerRemoved)
479
+ assertRealm();
480
+ else
481
+ assertOwned();
482
+ }
380
483
  const snapshotRevision = nextCounter(nextSnapshotRevision, "snapshot revision");
381
484
  mutating = true;
382
485
  try {
383
- listeners.clear();
384
- for (const registration of registrationsById.values()) {
385
- if (isCurrent(registration))
386
- registration.entity.remove(exports.CoordinationRegistration);
486
+ if (!cleanupMarkerRemoved) {
487
+ for (const registration of registrationsById.values()) {
488
+ if (isCurrent(registration)) {
489
+ assertOwned();
490
+ registration.entity.remove(exports.CoordinationRegistration);
491
+ assertOwned();
492
+ }
493
+ }
494
+ if (world.isInitialized) {
495
+ const remaining = assertOwned().filter((candidate) => candidate !== marker);
496
+ try {
497
+ if (remaining.length === 0)
498
+ world.remove(exports.CoordinationBinding);
499
+ else
500
+ world.set(exports.CoordinationBinding, { markers: remaining });
501
+ }
502
+ catch (error) {
503
+ assertRealm();
504
+ // A same-peer callback can throw after the marker write completed.
505
+ // Remember only this disposal's confirmed removal, so a retry can
506
+ // finish without treating an externally lost marker as ownership.
507
+ cleanupMarkerRemoved = !world.get(exports.CoordinationBinding)?.markers.includes(marker);
508
+ throw error;
509
+ }
510
+ // Final removal intentionally makes owned-marker attestation absent.
511
+ assertRealm();
512
+ cleanupMarkerRemoved = true;
513
+ }
387
514
  }
515
+ if (world.isInitialized)
516
+ assertRealm();
517
+ listeners.clear();
388
518
  registrationsById.clear();
389
519
  registrationsByToken.clear();
390
- const markerState = world.get(exports.CoordinationBinding);
391
- if (world.isInitialized && markerState?.markers.includes(marker)) {
392
- const remaining = markerState.markers.filter((candidate) => candidate !== marker);
393
- if (remaining.length === 0)
394
- world.remove(exports.CoordinationBinding);
395
- else
396
- world.set(exports.CoordinationBinding, { markers: remaining });
397
- }
398
520
  nextSnapshotRevision = snapshotRevision;
399
521
  snapshot = makeSnapshot(nextSnapshotRevision, []);
400
522
  disposed = true;
@@ -1 +1 @@
1
- {"version":3,"file":"coordination.d.ts","sourceRoot":"","sources":["../../src/coordination.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAS,KAAK,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,OAAO,CAAC;AAEvD,KAAK,aAAa,GAAG,MAAM,CAAC;AAW5B;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;mBAAgC,aAAa,EAAE;EAAG,CAAC;AAEnF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;kBACb,aAAa;;;;;;EAMnC,CAAC;AAEH,OAAO,CAAC,MAAM,sBAAsB,EAAE,OAAO,MAAM,CAAC;AAEpD,2EAA2E;AAC3E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,CAAC,sBAAsB,CAAC,EAAE,KAAK,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,SAAS,EAAE,kBAAkB,CAAC;CAC/B,CAAC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,4EAA4E;IAC5E,EAAE,EAAE,MAAM,CAAC;IACX,4EAA4E;IAC5E,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC,CAAC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,6EAA6E;IAC7E,SAAS,EAAE,iBAAiB,GAAG,SAAS,CAAC;CAC1C,CAAC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACvC,CAAC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,KAAK,EAAE,iBAAiB,CAAC;IACzB,SAAS,EAAE,iBAAiB,CAAC;CAC9B,CAAC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,oBAAoB,KAAK,IAAI,CAAC;AAE5E,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,sBAAsB,KAAK,iBAAiB,CAAC;IACpF,OAAO,EAAE,CAAC,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,iBAAiB,KAAK,oBAAoB,CAAC;IAC1F,YAAY,EAAE,CAAC,OAAO,EAAE,SAAS,kBAAkB,EAAE,KAAK,oBAAoB,CAAC;IAC/E,UAAU,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,oBAAoB,CAAC;IAC/D,6EAA6E;IAC7E,KAAK,EAAE,MAAM,oBAAoB,CAAC;IAClC,0EAA0E;IAC1E,eAAe,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,iBAAiB,GAAG,SAAS,CAAC;IAC/D,+EAA+E;IAC/E,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAC9C,WAAW,EAAE,MAAM,oBAAoB,CAAC;IACxC,SAAS,EAAE,CAAC,QAAQ,EAAE,oBAAoB,KAAK,MAAM,IAAI,CAAC;IAC1D,mFAAmF;IACnF,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC,CAAC;AA+HH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,YAAY,CAgT3D"}
1
+ {"version":3,"file":"coordination.d.ts","sourceRoot":"","sources":["../../src/coordination.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAA2C,KAAK,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,OAAO,CAAC;AAEzF,KAAK,aAAa,GAAG,MAAM,CAAC;AAW5B;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;mBAAgC,aAAa,EAAE;EAAG,CAAC;AAEnF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;kBACb,aAAa;;;;;;EAMnC,CAAC;AAEH,OAAO,CAAC,MAAM,sBAAsB,EAAE,OAAO,MAAM,CAAC;AAEpD,2EAA2E;AAC3E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,CAAC,sBAAsB,CAAC,EAAE,KAAK,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,SAAS,EAAE,kBAAkB,CAAC;CAC/B,CAAC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,4EAA4E;IAC5E,EAAE,EAAE,MAAM,CAAC;IACX,4EAA4E;IAC5E,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC,CAAC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,6EAA6E;IAC7E,SAAS,EAAE,iBAAiB,GAAG,SAAS,CAAC;CAC1C,CAAC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACvC,CAAC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,KAAK,EAAE,iBAAiB,CAAC;IACzB,SAAS,EAAE,iBAAiB,CAAC;CAC9B,CAAC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,oBAAoB,KAAK,IAAI,CAAC;AAE5E,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,sBAAsB,KAAK,iBAAiB,CAAC;IACpF,OAAO,EAAE,CAAC,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,iBAAiB,KAAK,oBAAoB,CAAC;IAC1F,YAAY,EAAE,CAAC,OAAO,EAAE,SAAS,kBAAkB,EAAE,KAAK,oBAAoB,CAAC;IAC/E,UAAU,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,oBAAoB,CAAC;IAC/D,6EAA6E;IAC7E,KAAK,EAAE,MAAM,oBAAoB,CAAC;IAClC,0EAA0E;IAC1E,eAAe,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,iBAAiB,GAAG,SAAS,CAAC;IAC/D,+EAA+E;IAC/E,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAC9C,WAAW,EAAE,MAAM,oBAAoB,CAAC;IACxC,SAAS,EAAE,CAAC,QAAQ,EAAE,oBAAoB,KAAK,MAAM,IAAI,CAAC;IAC1D,mFAAmF;IACnF,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC,CAAC;AA+HH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,YAAY,CAoa3D"}
@@ -1 +1 @@
1
- {"version":3,"file":"coordination.d.ts","sourceRoot":"","sources":["../../src/coordination.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAS,KAAK,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,OAAO,CAAC;AAEvD,KAAK,aAAa,GAAG,MAAM,CAAC;AAW5B;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;mBAAgC,aAAa,EAAE;EAAG,CAAC;AAEnF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;kBACb,aAAa;;;;;;EAMnC,CAAC;AAEH,OAAO,CAAC,MAAM,sBAAsB,EAAE,OAAO,MAAM,CAAC;AAEpD,2EAA2E;AAC3E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,CAAC,sBAAsB,CAAC,EAAE,KAAK,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,SAAS,EAAE,kBAAkB,CAAC;CAC/B,CAAC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,4EAA4E;IAC5E,EAAE,EAAE,MAAM,CAAC;IACX,4EAA4E;IAC5E,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC,CAAC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,6EAA6E;IAC7E,SAAS,EAAE,iBAAiB,GAAG,SAAS,CAAC;CAC1C,CAAC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACvC,CAAC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,KAAK,EAAE,iBAAiB,CAAC;IACzB,SAAS,EAAE,iBAAiB,CAAC;CAC9B,CAAC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,oBAAoB,KAAK,IAAI,CAAC;AAE5E,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,sBAAsB,KAAK,iBAAiB,CAAC;IACpF,OAAO,EAAE,CAAC,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,iBAAiB,KAAK,oBAAoB,CAAC;IAC1F,YAAY,EAAE,CAAC,OAAO,EAAE,SAAS,kBAAkB,EAAE,KAAK,oBAAoB,CAAC;IAC/E,UAAU,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,oBAAoB,CAAC;IAC/D,6EAA6E;IAC7E,KAAK,EAAE,MAAM,oBAAoB,CAAC;IAClC,0EAA0E;IAC1E,eAAe,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,iBAAiB,GAAG,SAAS,CAAC;IAC/D,+EAA+E;IAC/E,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAC9C,WAAW,EAAE,MAAM,oBAAoB,CAAC;IACxC,SAAS,EAAE,CAAC,QAAQ,EAAE,oBAAoB,KAAK,MAAM,IAAI,CAAC;IAC1D,mFAAmF;IACnF,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC,CAAC;AA+HH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,YAAY,CAgT3D"}
1
+ {"version":3,"file":"coordination.d.ts","sourceRoot":"","sources":["../../src/coordination.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAA2C,KAAK,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,OAAO,CAAC;AAEzF,KAAK,aAAa,GAAG,MAAM,CAAC;AAW5B;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;mBAAgC,aAAa,EAAE;EAAG,CAAC;AAEnF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;kBACb,aAAa;;;;;;EAMnC,CAAC;AAEH,OAAO,CAAC,MAAM,sBAAsB,EAAE,OAAO,MAAM,CAAC;AAEpD,2EAA2E;AAC3E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,CAAC,sBAAsB,CAAC,EAAE,KAAK,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,SAAS,EAAE,kBAAkB,CAAC;CAC/B,CAAC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,4EAA4E;IAC5E,EAAE,EAAE,MAAM,CAAC;IACX,4EAA4E;IAC5E,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC,CAAC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,6EAA6E;IAC7E,SAAS,EAAE,iBAAiB,GAAG,SAAS,CAAC;CAC1C,CAAC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACvC,CAAC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,KAAK,EAAE,iBAAiB,CAAC;IACzB,SAAS,EAAE,iBAAiB,CAAC;CAC9B,CAAC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,oBAAoB,KAAK,IAAI,CAAC;AAE5E,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,sBAAsB,KAAK,iBAAiB,CAAC;IACpF,OAAO,EAAE,CAAC,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,iBAAiB,KAAK,oBAAoB,CAAC;IAC1F,YAAY,EAAE,CAAC,OAAO,EAAE,SAAS,kBAAkB,EAAE,KAAK,oBAAoB,CAAC;IAC/E,UAAU,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,oBAAoB,CAAC;IAC/D,6EAA6E;IAC7E,KAAK,EAAE,MAAM,oBAAoB,CAAC;IAClC,0EAA0E;IAC1E,eAAe,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,iBAAiB,GAAG,SAAS,CAAC;IAC/D,+EAA+E;IAC/E,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAC9C,WAAW,EAAE,MAAM,oBAAoB,CAAC;IACxC,SAAS,EAAE,CAAC,QAAQ,EAAE,oBAAoB,KAAK,MAAM,IAAI,CAAC;IAC1D,mFAAmF;IACnF,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC,CAAC;AA+HH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,YAAY,CAoa3D"}
@@ -5,7 +5,7 @@
5
5
  * own marker traits and exposes a committed, immutable projection. It does
6
6
  * not create a World, run systems, serialize entities, or read geometry.
7
7
  */
8
- import { trait } from "koota";
8
+ import { getStore, trait, universe, unpackEntity } from "koota";
9
9
  /**
10
10
  * World-held ownership markers for live bindings. Each binding appends one
11
11
  * opaque marker and removes only that marker at disposal, so no module-global
@@ -117,16 +117,79 @@ export function bindCoordination(world) {
117
117
  if (!world?.isInitialized) {
118
118
  fail("a live Koota World is required.");
119
119
  }
120
+ // Koota's numeric Entity methods close over their imported peer universe.
121
+ // Opposite ESM/CJS imports replace those global methods, even before this
122
+ // module first loads. Pointer stability alone therefore cannot attest them.
123
+ const dispatch = {
124
+ get: 0..get,
125
+ has: 0..has,
126
+ add: 0..add,
127
+ set: 0..set,
128
+ remove: 0..remove,
129
+ };
130
+ const assertRealm = () => {
131
+ if (universe.worlds[world.id] !== world ||
132
+ 0..get !== dispatch.get ||
133
+ 0..has !== dispatch.has ||
134
+ 0..add !== dispatch.add ||
135
+ 0..set !== dispatch.set ||
136
+ 0..remove !== dispatch.remove) {
137
+ fail("the Koota entity dispatch realm no longer matches the borrowed World.");
138
+ }
139
+ };
140
+ assertRealm();
120
141
  const marker = Object.freeze({});
142
+ let holder;
143
+ let holderId;
144
+ let store;
145
+ const assertAttested = () => {
146
+ assertRealm();
147
+ const markers = world.get(CoordinationBinding)?.markers;
148
+ let matches = false;
149
+ try {
150
+ matches =
151
+ world.has(holder) &&
152
+ getStore(world, CoordinationBinding) === store &&
153
+ store.markers[holderId] === markers &&
154
+ dispatch.get.call(holder, CoordinationBinding)?.markers === markers;
155
+ }
156
+ catch {
157
+ // A foreign peer may have no World at this numeric ID at all.
158
+ }
159
+ assertRealm();
160
+ if (!matches)
161
+ fail("the Koota entity dispatch realm cannot attest the World marker.");
162
+ };
163
+ const assertOwned = () => {
164
+ assertAttested();
165
+ const markers = world.get(CoordinationBinding)?.markers;
166
+ if (!markers?.includes(marker)) {
167
+ fail("the binding no longer owns its World marker.");
168
+ }
169
+ return markers;
170
+ };
121
171
  const existingMarkers = world.get(CoordinationBinding)?.markers ?? [];
122
172
  const markers = [...existingMarkers, marker];
123
173
  try {
174
+ assertRealm();
124
175
  if (world.has(CoordinationBinding))
125
176
  world.set(CoordinationBinding, { markers });
126
177
  else
127
178
  world.add(CoordinationBinding({ markers }));
179
+ assertRealm();
180
+ store = getStore(world, CoordinationBinding);
181
+ // World traits live on an existing excluded entity. Find only our marker
182
+ // by direct store identity once; later checks use this cached holder in O(1).
183
+ const holders = world.entities.filter((entity) => store.markers[unpackEntity(entity).entityId] === markers);
184
+ if (holders.length !== 1)
185
+ fail("the Koota entity dispatch realm has no unique marker holder.");
186
+ holder = holders[0];
187
+ holderId = unpackEntity(holder).entityId;
188
+ assertOwned();
128
189
  }
129
190
  catch (error) {
191
+ // Direct World APIs close over the original World even if a callback
192
+ // changed entity dispatch before we could attest its numeric holder.
130
193
  const currentMarkers = world.get(CoordinationBinding)?.markers;
131
194
  if (world.isInitialized && currentMarkers?.includes(marker)) {
132
195
  const remaining = currentMarkers.filter((candidate) => candidate !== marker);
@@ -138,6 +201,7 @@ export function bindCoordination(world) {
138
201
  throw error;
139
202
  }
140
203
  let disposed = false;
204
+ let cleanupMarkerRemoved = false;
141
205
  let notifying = false;
142
206
  let mutating = false;
143
207
  let nextRegistrationGeneration = 0;
@@ -154,12 +218,15 @@ export function bindCoordination(world) {
154
218
  };
155
219
  const assertMutable = () => {
156
220
  assertActive();
157
- if (!world.isInitialized || !world.get(CoordinationBinding)?.markers.includes(marker)) {
221
+ if (!world.isInitialized)
158
222
  fail("the binding no longer owns its World marker.");
159
- }
223
+ assertOwned();
160
224
  };
161
225
  const isCurrent = (registration) => {
162
- if (!world.isInitialized || !world.has(registration.entity))
226
+ if (!world.isInitialized)
227
+ return false;
228
+ assertOwned();
229
+ if (!world.has(registration.entity))
163
230
  return false;
164
231
  return stateIsOwned(registration.entity.get(CoordinationRegistration), marker, registration.state.generation);
165
232
  };
@@ -186,6 +253,16 @@ export function bindCoordination(world) {
186
253
  errors.push(error);
187
254
  }
188
255
  }
256
+ // Listener failures occur after commit. A listener may also change peer
257
+ // dispatch without throwing; report that boundary to the caller too.
258
+ if (world.isInitialized) {
259
+ try {
260
+ assertOwned();
261
+ }
262
+ catch (error) {
263
+ errors.push(error);
264
+ }
265
+ }
189
266
  }
190
267
  finally {
191
268
  notifying = false;
@@ -222,6 +299,7 @@ export function bindCoordination(world) {
222
299
  const capabilities = copyCapabilities(descriptor.capabilities);
223
300
  if (registrationsById.has(id))
224
301
  fail(`stable id "${id}" is already registered.`);
302
+ assertOwned();
225
303
  if (entity.has(CoordinationRegistration)) {
226
304
  fail("entity already carries a coordination registration trait.");
227
305
  }
@@ -250,15 +328,20 @@ export function bindCoordination(world) {
250
328
  mutating = true;
251
329
  try {
252
330
  try {
331
+ assertOwned();
253
332
  entity.add(CoordinationRegistration(state));
333
+ assertOwned();
254
334
  }
255
335
  catch (error) {
256
- const current = entity.get(CoordinationRegistration);
336
+ // These captured peer functions use their original lexical universe;
337
+ // rollback only the trait this attempted registration placed there.
338
+ const current = dispatch.get.call(entity, CoordinationRegistration);
257
339
  if (stateIsOwned(current, marker, registrationGeneration)) {
258
- entity.remove(CoordinationRegistration);
340
+ dispatch.remove.call(entity, CoordinationRegistration);
259
341
  }
260
342
  throw error;
261
343
  }
344
+ assertOwned();
262
345
  nextRegistrationGeneration = registrationGeneration;
263
346
  registrationsById.set(id, registration);
264
347
  registrationsByToken.set(token, registration);
@@ -290,6 +373,7 @@ export function bindCoordination(world) {
290
373
  }
291
374
  const changed = prepared.some(({ registration, completed }) => !registration.completed ||
292
375
  !sameRevisions(registration.completed.revisions, completed.revisions));
376
+ assertOwned();
293
377
  if (!changed)
294
378
  return snapshot;
295
379
  const snapshotRevision = nextCounter(nextSnapshotRevision, "snapshot revision");
@@ -308,8 +392,11 @@ export function bindCoordination(world) {
308
392
  mutating = true;
309
393
  try {
310
394
  for (const { registration, state } of preparedStates) {
395
+ assertOwned();
311
396
  registration.entity.set(CoordinationRegistration, state);
397
+ assertOwned();
312
398
  }
399
+ assertOwned();
313
400
  for (const { registration, completed } of preparedStates) {
314
401
  registration.completed = completed;
315
402
  }
@@ -325,7 +412,9 @@ export function bindCoordination(world) {
325
412
  const snapshotRevision = nextCounter(nextSnapshotRevision, "snapshot revision");
326
413
  mutating = true;
327
414
  try {
415
+ assertOwned();
328
416
  registration.entity.remove(CoordinationRegistration);
417
+ assertOwned();
329
418
  registrationsById.delete(registration.id);
330
419
  registrationsByToken.delete(token);
331
420
  return commit(snapshotRevision);
@@ -336,12 +425,16 @@ export function bindCoordination(world) {
336
425
  };
337
426
  const prune = () => {
338
427
  assertActive();
428
+ if (world.isInitialized)
429
+ assertOwned();
339
430
  const stale = [...registrationsById.values()].filter((registration) => !isCurrent(registration));
340
431
  if (stale.length === 0)
341
432
  return snapshot;
342
433
  const snapshotRevision = nextCounter(nextSnapshotRevision, "snapshot revision");
343
434
  mutating = true;
344
435
  try {
436
+ if (world.isInitialized)
437
+ assertOwned();
345
438
  for (const registration of stale) {
346
439
  registrationsById.delete(registration.id);
347
440
  registrationsByToken.delete(registration.token);
@@ -353,10 +446,14 @@ export function bindCoordination(world) {
353
446
  }
354
447
  };
355
448
  const getEntity = (id) => {
449
+ if (world.isInitialized && !disposed)
450
+ assertOwned();
356
451
  const registration = registrationsById.get(id);
357
452
  return registration && isCurrent(registration) ? registration.entity : undefined;
358
453
  };
359
454
  const getRegistration = (id) => {
455
+ if (world.isInitialized && !disposed)
456
+ assertOwned();
360
457
  const registration = registrationsById.get(id);
361
458
  return registration && isCurrent(registration) ? registration.token : undefined;
362
459
  };
@@ -373,24 +470,49 @@ export function bindCoordination(world) {
373
470
  if (disposed)
374
471
  return;
375
472
  assertActive();
473
+ if (world.isInitialized) {
474
+ if (cleanupMarkerRemoved)
475
+ assertRealm();
476
+ else
477
+ assertOwned();
478
+ }
376
479
  const snapshotRevision = nextCounter(nextSnapshotRevision, "snapshot revision");
377
480
  mutating = true;
378
481
  try {
379
- listeners.clear();
380
- for (const registration of registrationsById.values()) {
381
- if (isCurrent(registration))
382
- registration.entity.remove(CoordinationRegistration);
482
+ if (!cleanupMarkerRemoved) {
483
+ for (const registration of registrationsById.values()) {
484
+ if (isCurrent(registration)) {
485
+ assertOwned();
486
+ registration.entity.remove(CoordinationRegistration);
487
+ assertOwned();
488
+ }
489
+ }
490
+ if (world.isInitialized) {
491
+ const remaining = assertOwned().filter((candidate) => candidate !== marker);
492
+ try {
493
+ if (remaining.length === 0)
494
+ world.remove(CoordinationBinding);
495
+ else
496
+ world.set(CoordinationBinding, { markers: remaining });
497
+ }
498
+ catch (error) {
499
+ assertRealm();
500
+ // A same-peer callback can throw after the marker write completed.
501
+ // Remember only this disposal's confirmed removal, so a retry can
502
+ // finish without treating an externally lost marker as ownership.
503
+ cleanupMarkerRemoved = !world.get(CoordinationBinding)?.markers.includes(marker);
504
+ throw error;
505
+ }
506
+ // Final removal intentionally makes owned-marker attestation absent.
507
+ assertRealm();
508
+ cleanupMarkerRemoved = true;
509
+ }
383
510
  }
511
+ if (world.isInitialized)
512
+ assertRealm();
513
+ listeners.clear();
384
514
  registrationsById.clear();
385
515
  registrationsByToken.clear();
386
- const markerState = world.get(CoordinationBinding);
387
- if (world.isInitialized && markerState?.markers.includes(marker)) {
388
- const remaining = markerState.markers.filter((candidate) => candidate !== marker);
389
- if (remaining.length === 0)
390
- world.remove(CoordinationBinding);
391
- else
392
- world.set(CoordinationBinding, { markers: remaining });
393
- }
394
516
  nextSnapshotRevision = snapshotRevision;
395
517
  snapshot = makeSnapshot(nextSnapshotRevision, []);
396
518
  disposed = true;
package/docs/API.md CHANGED
@@ -360,6 +360,19 @@ trait before rethrowing, so an existing peer binding remains intact and the
360
360
  caller may retry registration. `getEntity` and `getRegistration` return `undefined` for a stale
361
361
  registration.
362
362
 
363
+ Use one Koota runtime and module format per process. Koota
364
+ 0.6.6 replaces numeric entity dispatch when its opposite ESM/CommonJS format
365
+ loads. Binding rejects a foreign peer World before adding its marker, and
366
+ attests the existing marker holder to detect contamination that predates the
367
+ first kit import. Later live reads, mutations, and cleanup throw a
368
+ `coordination:` dispatch-realm error if that attestation fails; they preserve
369
+ the last committed snapshot and subscribers. `getSnapshot()` remains readable.
370
+ A subscriber that changes dispatch after publication triggers an error after
371
+ all listeners receive the newly committed snapshot; that publication stays
372
+ committed. An opposite-format `koota-kit/spatial` import is safe because that entry does
373
+ not load Koota. This guard detects the known peer-format conflict; it does not
374
+ repair global methods or make mixed-format entity operations interoperable.
375
+
363
376
  `publishBatch` validates every token and metadata value before it changes any
364
377
  owned trait or snapshot. Its completed projection changes only after every raw
365
378
  Koota trait write succeeds; a Koota callback error can leave partial raw mirror
@@ -67,7 +67,28 @@ authority over caller entities and traits. Each binding owns an opaque marker
67
67
  held on that World and registration records tagged with that marker, so it can
68
68
  remove only its own records. Bindings may coexist only for disjoint entities.
69
69
  `CoordinationBinding` and `CoordinationRegistration` have one identity per
70
- module format; consumers should not mix package formats in a single Koota World.
70
+ module format. Applications must use one Koota runtime and module format per
71
+ process. Koota 0.6.6 installs numeric entity methods globally; loading
72
+ its opposite ESM/CommonJS format replaces dispatch for already live entities.
73
+ Coordination rejects a World outside its imported peer universe before adding
74
+ markers. It then attests dispatch against the existing World marker holder,
75
+ using the public `getStore`, `unpackEntity`, and `universe` APIs. The initial
76
+ holder lookup inspects only its owned marker store; subsequent checks cache the
77
+ holder and require constant work. There is no probe entity, hidden World,
78
+ prototype repair, or second World registry.
79
+
80
+ Every live read and raw mutation verifies that dispatch and marker ownership
81
+ still agree. Callbackful writes are checked before and after the raw operation
82
+ and before committing metadata or cleanup. A mismatch throws while preserving
83
+ the previous completed snapshot and subscribers. A dispatch change during listener
84
+ delivery is a postcommit error: all listeners receive the new committed
85
+ snapshot, and that snapshot stays published. A raw callback may already
86
+ have written or removed an owned trait; that partial raw state is outside the
87
+ completed projection. Registration rollback uses the previously attested peer
88
+ functions solely for its just-added owned trait. Initial binding rollback uses
89
+ direct World methods. Final marker removal checks dispatch without requiring
90
+ the removed marker, then commits cleanup. Destroyed-World cleanup needs no
91
+ entity dispatch, and an already disposed binding stays idempotent.
71
92
 
72
93
  Before a publication, coordination validates every update and produces a new
73
94
  immutable view sorted by caller-authored stable ID. Koota raw trait callbacks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koota-kit",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Deterministic Koota simulation conventions: world lifecycle, dual RNG streams, safe object traits, and world-scoped event logs.",
5
5
  "type": "module",
6
6
  "license": "MIT",