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 +20 -0
- package/README.md +7 -0
- package/dist/cjs/coordination.cjs +139 -17
- package/dist/cjs/coordination.d.cts.map +1 -1
- package/dist/esm/coordination.d.ts.map +1 -1
- package/dist/esm/coordination.js +140 -18
- package/docs/API.md +13 -0
- package/docs/ARCHITECTURE.md +22 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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,
|
|
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,
|
|
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"}
|
package/dist/esm/coordination.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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
|
|
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