@vibecook/ice 0.6.0 → 0.8.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 (69) hide show
  1. package/dist/{chunk-BFT7YWS2.js → chunk-3SUDKHUF.js} +3 -3
  2. package/dist/{chunk-JLXETATP.js → chunk-BE4F4X3Q.js} +485 -77
  3. package/dist/chunk-BE4F4X3Q.js.map +1 -0
  4. package/dist/{chunk-ELCBCTKY.js → chunk-CBCSIEXW.js} +2 -2
  5. package/dist/devtools.js +1 -1
  6. package/dist/dom.js +2 -2
  7. package/dist/ground.js +1 -1
  8. package/dist/index.js +3 -1
  9. package/dist/r3f.js +3 -3
  10. package/dist/react.js +3 -3
  11. package/dist/types/packages/core/src/behavior/define-behavior.d.ts +2 -1
  12. package/dist/types/packages/core/src/behavior/runtime.d.ts +16 -2
  13. package/dist/types/packages/core/src/behavior/types.d.ts +47 -1
  14. package/dist/types/packages/core/src/engine/guests.d.ts +5 -2
  15. package/dist/types/packages/core/src/facade/create-canvas-engine.d.ts +38 -6
  16. package/dist/types/packages/core/src/index.d.ts +4 -3
  17. package/dist/types/packages/core/src/interaction/install.d.ts +1 -1
  18. package/dist/types/packages/core/src/nav/nested-canvas.d.ts +1 -1
  19. package/dist/types/packages/core/src/ops/point-pick.d.ts +1 -1
  20. package/dist/types/packages/core/src/ops/port-geometry.d.ts +1 -1
  21. package/dist/types/packages/core/src/presence/presence-kit.d.ts +11 -0
  22. package/dist/types/packages/core/src/presence/remote-cursors.d.ts +10 -7
  23. package/dist/types/packages/core/src/systems/l1-pick.d.ts +1 -1
  24. package/dist/types/packages/core/src/systems/l1-wires.d.ts +1 -1
  25. package/dist/types/packages/core/src/systems/l3-connect.d.ts +1 -1
  26. package/dist/types/packages/core/src/systems/l3-drop.d.ts +1 -1
  27. package/dist/types/packages/core/src/systems/l3-marquee.d.ts +1 -1
  28. package/dist/types/packages/core/src/systems/l3-ports.d.ts +1 -1
  29. package/dist/types/packages/core/src/systems/l3-snap.d.ts +1 -1
  30. package/dist/types/packages/core/src/systems/nav-flight.d.ts +1 -1
  31. package/dist/types/packages/devtools/src/attach.d.ts +1 -1
  32. package/dist/types/packages/dom/src/loop.d.ts +1 -1
  33. package/dist/types/packages/dom/src/measure-adapter.d.ts +1 -1
  34. package/dist/types/packages/dom/src/measure-wiring.d.ts +1 -1
  35. package/dist/types/packages/dom/src/pointer-adapter.d.ts +1 -1
  36. package/dist/types/packages/dom/src/reflectors/chrome.d.ts +1 -1
  37. package/dist/types/packages/dom/src/reflectors/cursor.d.ts +1 -1
  38. package/dist/types/packages/dom/src/reflectors/dom-widgets.d.ts +1 -1
  39. package/dist/types/packages/dom/src/reflectors/graybox.d.ts +1 -1
  40. package/dist/types/packages/dom/src/reflectors/plane-transform.d.ts +1 -1
  41. package/dist/types/packages/dom/src/reflectors/remote-cursors.d.ts +1 -1
  42. package/dist/types/packages/dom/src/widget-focus.d.ts +1 -1
  43. package/dist/types/packages/ground/src/index.d.ts +2 -2
  44. package/dist/types/packages/ground/src/layer.d.ts +1 -1
  45. package/dist/types/packages/ground/src/pass.d.ts +1 -1
  46. package/dist/types/packages/ground/src/passes/grid.d.ts +1 -1
  47. package/dist/types/packages/ground/src/passes/guides-collect.d.ts +1 -1
  48. package/dist/types/packages/ground/src/passes/guides.d.ts +1 -1
  49. package/dist/types/packages/ground/src/passes/wires-collect.d.ts +1 -1
  50. package/dist/types/packages/ground/src/passes/wires.d.ts +1 -1
  51. package/dist/types/packages/r3f/src/bridge.d.ts +1 -1
  52. package/dist/types/packages/r3f/src/compositor-pass.d.ts +1 -1
  53. package/dist/types/packages/r3f/src/dev-write-trap.d.ts +1 -1
  54. package/dist/types/packages/r3f/src/gl-root.d.ts +1 -1
  55. package/dist/types/packages/r3f/src/gl-router.d.ts +2 -2
  56. package/dist/types/packages/r3f/src/island-state.d.ts +1 -1
  57. package/dist/types/packages/r3f/src/island.d.ts +1 -1
  58. package/dist/types/packages/r3f/src/use-island-frame.d.ts +1 -1
  59. package/dist/types/packages/react/src/engine-context.d.ts +1 -1
  60. package/dist/types/packages/react/src/hooks.d.ts +1 -1
  61. package/dist/types/packages/react/src/infinite-canvas.d.ts +2 -2
  62. package/dist/types/packages/react/src/keymap.d.ts +1 -1
  63. package/dist/types/packages/react/src/use-commit.d.ts +1 -1
  64. package/dist/types/packages/react/src/use-presence.d.ts +1 -1
  65. package/dist/types/packages/react/src/widget-root.d.ts +1 -1
  66. package/package.json +2 -2
  67. package/dist/chunk-JLXETATP.js.map +0 -1
  68. /package/dist/{chunk-BFT7YWS2.js.map → chunk-3SUDKHUF.js.map} +0 -0
  69. /package/dist/{chunk-ELCBCTKY.js.map → chunk-CBCSIEXW.js.map} +0 -0
@@ -843,6 +843,7 @@ function createGuestRegistry(world, opts) {
843
843
  },
844
844
  fault(err) {
845
845
  if (!entry.alive || disposed) return;
846
+ if (entry.suspended) return;
846
847
  entry.faultedThisFrame = true;
847
848
  entry.accrued = true;
848
849
  entry.consecutiveThrows++;
@@ -1733,6 +1734,102 @@ function validate(name, spec, phase2) {
1733
1734
  }
1734
1735
  }
1735
1736
  }
1737
+ function cloneJsonShape(shape) {
1738
+ switch (shape.kind) {
1739
+ case "string":
1740
+ case "number":
1741
+ case "boolean":
1742
+ return { kind: shape.kind };
1743
+ case "enum":
1744
+ return { kind: "enum", options: [...shape.options] };
1745
+ case "array":
1746
+ return { kind: "array", item: cloneJsonShape(shape.item) };
1747
+ case "object":
1748
+ return {
1749
+ kind: "object",
1750
+ fields: Object.fromEntries(
1751
+ Object.entries(shape.fields).map(([name, field15]) => [name, cloneJsonShape(field15)])
1752
+ )
1753
+ };
1754
+ }
1755
+ }
1756
+ function describeProp(spec) {
1757
+ switch (spec.kind) {
1758
+ case "string":
1759
+ return {
1760
+ kind: "string",
1761
+ ...spec.default === void 0 ? {} : { default: spec.default }
1762
+ };
1763
+ case "number":
1764
+ return {
1765
+ kind: "number",
1766
+ ...spec.default === void 0 ? {} : { default: spec.default },
1767
+ ...spec.min === void 0 ? {} : { min: spec.min },
1768
+ ...spec.max === void 0 ? {} : { max: spec.max }
1769
+ };
1770
+ case "boolean":
1771
+ return {
1772
+ kind: "boolean",
1773
+ ...spec.default === void 0 ? {} : { default: spec.default }
1774
+ };
1775
+ case "enum":
1776
+ return {
1777
+ kind: "enum",
1778
+ options: [...spec.options],
1779
+ ...spec.default === void 0 ? {} : { default: spec.default }
1780
+ };
1781
+ case "json":
1782
+ return {
1783
+ kind: "json",
1784
+ inner: cloneJsonShape(spec.inner),
1785
+ ...spec.default === void 0 ? {} : { default: spec.default }
1786
+ };
1787
+ case "entityKey":
1788
+ return {
1789
+ kind: "entityKey",
1790
+ ...spec.default === void 0 ? {} : { default: spec.default }
1791
+ };
1792
+ }
1793
+ }
1794
+ var DESCRIPTION_HOOK_ORDER = [
1795
+ "init",
1796
+ "update",
1797
+ "changed",
1798
+ "tick",
1799
+ "dispose"
1800
+ ];
1801
+ function describeBehavior(behavior) {
1802
+ const reads = [];
1803
+ for (const read of behavior.reads) {
1804
+ const description = classifyRead(read);
1805
+ if (description !== void 0) reads.push(description);
1806
+ }
1807
+ const writes = [];
1808
+ for (const component of behavior.writes) {
1809
+ const meta = schemaMeta.component(component);
1810
+ if (meta !== void 0) writes.push(meta.name);
1811
+ }
1812
+ return {
1813
+ id: behavior.name,
1814
+ store: behavior.store,
1815
+ derived: behavior.derived,
1816
+ deriveDuringGesture: behavior.deriveDuringGesture,
1817
+ version: behavior.version,
1818
+ phase: behavior.phase,
1819
+ ...behavior.budgetMs === void 0 ? {} : { budgetMs: behavior.budgetMs },
1820
+ tickWhile: behavior.tickWhile,
1821
+ schema: Object.entries(behavior.schema).map(([name, spec]) => ({
1822
+ name,
1823
+ spec: describeProp(spec)
1824
+ })),
1825
+ reads,
1826
+ writes,
1827
+ migrationFrom: Object.keys(behavior.migrate).map(Number).sort((left, right) => left - right),
1828
+ hooks: DESCRIPTION_HOOK_ORDER.filter(
1829
+ (hook) => typeof behavior.on[hook] === "function"
1830
+ )
1831
+ };
1832
+ }
1736
1833
  function defineBehavior(name, spec) {
1737
1834
  const phase2 = spec.phase ?? BEHAVIOR_DEFAULT_PHASE[spec.store];
1738
1835
  if (devGuardsEnabled()) validate(name, spec, phase2);
@@ -7399,6 +7496,19 @@ function allowedQueryTerms(behavior, readComponents, readTags) {
7399
7496
  // ../core/src/behavior/runtime.ts
7400
7497
  var INSTANCE_THROW_LIMIT = 3;
7401
7498
  var SUPPRESSION_WARN_FRAMES = 600;
7499
+ function isThenable(value) {
7500
+ return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
7501
+ }
7502
+ var BehaviorHookThenableError = class extends TypeError {
7503
+ };
7504
+ function invokeHook(behavior, hook, body) {
7505
+ const result = body();
7506
+ if (!isThenable(result)) return;
7507
+ void Promise.resolve(result).catch(() => void 0);
7508
+ throw new BehaviorHookThenableError(
7509
+ `ice: behavior "${behavior}" ${hook} returned a thenable; behavior hooks are synchronous`
7510
+ );
7511
+ }
7402
7512
  function isClaimed(world, e) {
7403
7513
  if (!world.isAlive(e)) return false;
7404
7514
  for (const claimant of world.getReverse(e, Captures)) {
@@ -7426,6 +7536,7 @@ function cellForBehavior(world, b, e, patch) {
7426
7536
  function createBehaviorRuntime(opts) {
7427
7537
  const { world } = opts;
7428
7538
  const nodes = /* @__PURE__ */ new Map();
7539
+ let nextRegistrationSequence = 0;
7429
7540
  const onFault = opts.onFault ?? ((behavior, hook, entity, err) => {
7430
7541
  console.error(`[ice] behavior "${behavior}" ${hook}${entity === void 0 ? "" : ` on entity ${entity}`} threw`, err);
7431
7542
  });
@@ -7437,8 +7548,42 @@ function createBehaviorRuntime(opts) {
7437
7548
  for (const n of nodes.values()) n.markGeneration();
7438
7549
  }
7439
7550
  });
7551
+ const publishOrder = [];
7552
+ const pendingWithdrawals = [];
7553
+ let publishHookRemover;
7554
+ function ensurePublishHook() {
7555
+ if (publishHookRemover !== void 0) return;
7556
+ publishHookRemover = opts.engine.onPublish(() => {
7557
+ if (pendingWithdrawals.length > 0) {
7558
+ const jobs = pendingWithdrawals.splice(0, pendingWithdrawals.length);
7559
+ for (const job of jobs) job();
7560
+ }
7561
+ if (publishOrder.length > 0) {
7562
+ const pass = publishOrder.slice();
7563
+ for (const node of pass) if (!node.isStopped()) node.runPublish();
7564
+ }
7565
+ if (publishOrder.length === 0 && pendingWithdrawals.length === 0) {
7566
+ publishHookRemover?.();
7567
+ publishHookRemover = void 0;
7568
+ }
7569
+ });
7570
+ }
7571
+ function installPublishNode(node) {
7572
+ publishOrder.push(node);
7573
+ ensurePublishHook();
7574
+ return () => {
7575
+ const i = publishOrder.indexOf(node);
7576
+ if (i !== -1) publishOrder.splice(i, 1);
7577
+ };
7578
+ }
7579
+ function deferWithdrawal(job) {
7580
+ pendingWithdrawals.push(job);
7581
+ ensurePublishHook();
7582
+ }
7440
7583
  class Node {
7441
7584
  b;
7585
+ orderKey;
7586
+ registrationSequence;
7442
7587
  compiled;
7443
7588
  own;
7444
7589
  /** The same handle, typed for `readField`'s keyof-based field parameter. */
@@ -7449,6 +7594,28 @@ function createBehaviorRuntime(opts) {
7449
7594
  peersQuery;
7450
7595
  /** Ephemeral only: the local peer this node last published a facet on. */
7451
7596
  facetPeer;
7597
+ /**
7598
+ * The entity whose HOOK is currently executing (review finding 2, 0.8.0).
7599
+ * `writeFacet` consults it so a hook writes only the peer whose instance
7600
+ * it runs for — a same-gap presence swap otherwise lets the OLD peer's
7601
+ * dispose resolve `localPeer` LIVE and land a corpse's farewell on the
7602
+ * fresh peer. Unset outside hooks (captured `ctx.write` closures keep the
7603
+ * blessed behavior: they publish the CURRENT peer).
7604
+ */
7605
+ hookEntity;
7606
+ /** The ephemeral singleton was quarantined THIS generation (I17). */
7607
+ singletonQuarantined = false;
7608
+ /** Uninstalled — a captured ctx.write closure must go inert (I17). */
7609
+ stopped = false;
7610
+ /** Per-FRAME strike dedupe (review finding 4): the ladder counts FRAMES,
7611
+ * not calls — 500 async instances must cost ONE strike, not write 500
7612
+ * into a ledger hosts persist across engine generations. Keyed by
7613
+ * FrameInfo.tick because no single pass entry point runs every frame
7614
+ * (the delivery system is runIf-gated off on no-churn frames while the
7615
+ * tick system keeps running). */
7616
+ strikeFrame = -1;
7617
+ thenableStruckPass = false;
7618
+ spanStruckPass = false;
7452
7619
  /** The session whose version state has already been checked (§5.6). */
7453
7620
  checkedSession;
7454
7621
  /**
@@ -7519,8 +7686,10 @@ function createBehaviorRuntime(opts) {
7519
7686
  /** Set for the duration of a delivery/tick pass; see BehaviorWalker. */
7520
7687
  walker;
7521
7688
  ctx;
7522
- constructor(b) {
7689
+ constructor(b, registration, registrationSequence) {
7523
7690
  this.b = b;
7691
+ this.orderKey = registration.orderKey;
7692
+ this.registrationSequence = registrationSequence;
7524
7693
  this.own = b.component;
7525
7694
  this.ownFields = b.component;
7526
7695
  this.schema = b.schema;
@@ -7543,6 +7712,7 @@ function createBehaviorRuntime(opts) {
7543
7712
  this.guest = opts.engine.guests.addDriven({
7544
7713
  id: `behavior:${b.name}`,
7545
7714
  ...b.budgetMs !== void 0 ? { budgetMs: b.budgetMs } : {},
7715
+ ...registration.ledger === void 0 ? {} : { ledger: registration.ledger },
7546
7716
  busy: () => this.owed()
7547
7717
  });
7548
7718
  this.ctx = this.buildCtx();
@@ -7576,6 +7746,7 @@ function createBehaviorRuntime(opts) {
7576
7746
  this.readsCollector?.dispose();
7577
7747
  this.ownCollector = void 0;
7578
7748
  this.readsCollector = void 0;
7749
+ this.withdrawFacet();
7579
7750
  }
7580
7751
  /**
7581
7752
  * Resumed. Fresh collectors journal from NOW, so everything that happened
@@ -7597,18 +7768,10 @@ function createBehaviorRuntime(opts) {
7597
7768
  if (this.compiled.readRelations.length > 0) {
7598
7769
  world.orderStamp(0, this.compiled.readRelations[0]);
7599
7770
  }
7771
+ }
7772
+ installExecution() {
7600
7773
  if (this.b.phase === "publish") {
7601
- this.removeSystems = opts.engine.onPublish(() => {
7602
- if (this.guest.suspended()) {
7603
- this.goCold();
7604
- return;
7605
- }
7606
- this.guest.run(() => {
7607
- if (this.b.store === "ephemeral") this.ensureFacet();
7608
- if (this.shouldDeliver()) this.runDeliver(publishWalker(world));
7609
- this.publishTick();
7610
- });
7611
- });
7774
+ this.removeSystems = installPublishNode(this);
7612
7775
  } else {
7613
7776
  this.removeSystems = opts.engine.addSystems(
7614
7777
  this.b.phase,
@@ -7616,6 +7779,25 @@ function createBehaviorRuntime(opts) {
7616
7779
  );
7617
7780
  }
7618
7781
  }
7782
+ /** The publish-slot body — driven by the runtime's one stable hook. */
7783
+ runPublish() {
7784
+ if (this.guest.suspended()) {
7785
+ this.goCold();
7786
+ return;
7787
+ }
7788
+ this.guest.run(() => {
7789
+ if (this.b.store === "ephemeral") this.ensureFacet();
7790
+ if (this.shouldDeliver()) this.runDeliver(publishWalker(world));
7791
+ this.publishTick();
7792
+ });
7793
+ }
7794
+ isStopped() {
7795
+ return this.stopped;
7796
+ }
7797
+ uninstallExecution() {
7798
+ this.removeSystems?.();
7799
+ this.removeSystems = void 0;
7800
+ }
7619
7801
  /** The version-marker surface for the current session, if it has one. */
7620
7802
  metaOf(session) {
7621
7803
  const snapshot = session?.store.snapshot;
@@ -7701,6 +7883,14 @@ function createBehaviorRuntime(opts) {
7701
7883
  this.facetPeer = void 0;
7702
7884
  return;
7703
7885
  }
7886
+ if (this.singletonQuarantined) {
7887
+ this.facetPeer = void 0;
7888
+ return;
7889
+ }
7890
+ if (this.instances.size > 0 && !this.instances.has(peer)) {
7891
+ this.facetPeer = void 0;
7892
+ return;
7893
+ }
7704
7894
  if (peer === this.facetPeer && world.has(peer, this.own)) return;
7705
7895
  this.facetPeer = peer;
7706
7896
  if (!world.has(peer, this.own)) {
@@ -7711,6 +7901,48 @@ function createBehaviorRuntime(opts) {
7711
7901
  );
7712
7902
  }
7713
7903
  }
7904
+ /**
7905
+ * Withdraw the published facet (petition I17). An ephemeral facet is LIVE
7906
+ * PUBLICATION — the local peer telling every remote peer "this value is
7907
+ * current" — so every edge where the producer stops executing must reverse
7908
+ * the publication too, or a disabled, suspended, or quarantined behavior
7909
+ * keeps asserting its last value forever (remote peers have no TTL for a
7910
+ * peer that is still connected). Idempotent by construction: presence
7911
+ * absent, a dead peer (mid re-mint window), and an already-gone facet are
7912
+ * all quiet no-ops. The removal goes through the PRESENCE writer, never
7913
+ * `world.removeComponent`, because only the presence path encodes a
7914
+ * tombstone remote projections honor.
7915
+ */
7916
+ withdrawFacet() {
7917
+ if (this.b.store !== "ephemeral") return;
7918
+ this.facetPeer = void 0;
7919
+ const presence = opts.presence?.();
7920
+ if (presence === void 0) return;
7921
+ const peer = presence.localPeer;
7922
+ if (!(world.isAlive(peer) && world.has(peer, this.own))) return;
7923
+ const midTick = world.inImmediateProjectionUnsafeContext ?? false;
7924
+ if (midTick) {
7925
+ const behaviorName = this.b.name;
7926
+ const own = this.own;
7927
+ deferWithdrawal(() => {
7928
+ const p2 = opts.presence?.();
7929
+ if (p2 === void 0) return;
7930
+ if (p2.localPeer !== peer) return;
7931
+ if (!(world.isAlive(peer) && world.has(peer, own))) return;
7932
+ try {
7933
+ p2.eph.removeComponent(peer, own);
7934
+ } catch (err) {
7935
+ onFault(behaviorName, "withdraw", peer, err);
7936
+ }
7937
+ });
7938
+ return;
7939
+ }
7940
+ try {
7941
+ presence.eph.removeComponent(peer, this.own);
7942
+ } catch (err) {
7943
+ onFault(this.b.name, "withdraw", peer, err);
7944
+ }
7945
+ }
7714
7946
  /** The publish-slot twin of the tick SYSTEM (no query, no chunks). */
7715
7947
  publishTick() {
7716
7948
  const on = this.b.on;
@@ -7723,16 +7955,19 @@ function createBehaviorRuntime(opts) {
7723
7955
  const inst = this.instances.get(e);
7724
7956
  if (inst === void 0 || inst.failed || !inst.inited) continue;
7725
7957
  const data = this.dataOf(e);
7726
- this.callHook("tick", inst, faulted, () => {
7727
- on.tick?.(e, data, frame2, this.ctx);
7728
- });
7958
+ this.callHook(
7959
+ "tick",
7960
+ inst,
7961
+ faulted,
7962
+ () => on.tick?.(e, data, frame2, this.ctx)
7963
+ );
7729
7964
  }
7730
7965
  this.walker = void 0;
7731
7966
  }
7732
7967
  uninstall() {
7968
+ this.stopped = true;
7733
7969
  this.endGeneration();
7734
- this.removeSystems?.();
7735
- this.removeSystems = void 0;
7970
+ this.uninstallExecution();
7736
7971
  this.ownCollector?.dispose();
7737
7972
  this.readsCollector?.dispose();
7738
7973
  this.ownCollector = void 0;
@@ -7995,9 +8230,12 @@ function createBehaviorRuntime(opts) {
7995
8230
  for (const e of appeared) {
7996
8231
  const inst = this.instances.get(e);
7997
8232
  if (inst === void 0 || inst.failed) continue;
7998
- if (this.callHook("init", inst, faultedEntities, () => {
7999
- on.init?.(e, this.dataOf(e), this.ctx);
8000
- })) {
8233
+ if (this.callHook(
8234
+ "init",
8235
+ inst,
8236
+ faultedEntities,
8237
+ () => on.init?.(e, this.dataOf(e), this.ctx)
8238
+ )) {
8001
8239
  inst.inited = true;
8002
8240
  }
8003
8241
  }
@@ -8019,16 +8257,19 @@ function createBehaviorRuntime(opts) {
8019
8257
  if (valueEquals2(this.own, this.scratchCell, inst.prevCell)) continue;
8020
8258
  const data = this.viewOf(this.scratchCell, this.scratchData);
8021
8259
  const prev = this.viewOf(inst.prevCell, this.scratchPrevData);
8022
- this.callHook("update", inst, faultedEntities, () => {
8023
- on.update?.(e, data, prev, this.ctx);
8024
- });
8260
+ this.callHook(
8261
+ "update",
8262
+ inst,
8263
+ faultedEntities,
8264
+ () => on.update?.(e, data, prev, this.ctx)
8265
+ );
8025
8266
  this.copyInto(this.scratchCell, inst.prevCell);
8026
8267
  }
8027
8268
  }
8028
8269
  const anyVisible = this.visible.length > 0;
8029
8270
  if (on.changed !== void 0 && (full || (work?.readsFired ?? false) || lifted) && anyVisible) {
8030
8271
  try {
8031
- on.changed(this.ctx);
8272
+ invokeHook(this.b.name, "changed", () => on.changed?.(this.ctx));
8032
8273
  } catch (err) {
8033
8274
  onFault(this.b.name, "changed", void 0, err);
8034
8275
  this.guest.fault(err);
@@ -8036,10 +8277,17 @@ function createBehaviorRuntime(opts) {
8036
8277
  }
8037
8278
  if (on.dispose !== void 0) {
8038
8279
  for (const inst of departed) {
8280
+ this.hookEntity = inst.e;
8039
8281
  try {
8040
- on.dispose(inst.e, this.ctx);
8282
+ invokeHook(
8283
+ this.b.name,
8284
+ "dispose",
8285
+ () => on.dispose?.(inst.e, this.ctx)
8286
+ );
8041
8287
  } catch (err) {
8042
8288
  onFault(this.b.name, "dispose", inst.e, err);
8289
+ } finally {
8290
+ this.hookEntity = void 0;
8043
8291
  }
8044
8292
  }
8045
8293
  }
@@ -8062,9 +8310,12 @@ function createBehaviorRuntime(opts) {
8062
8310
  const inst = this.instances.get(e);
8063
8311
  if (inst === void 0 || inst.failed || !inst.inited) continue;
8064
8312
  const data = this.dataOf(e);
8065
- this.callHook("tick", inst, faulted, () => {
8066
- on.tick?.(e, data, frame2, this.ctx);
8067
- });
8313
+ this.callHook(
8314
+ "tick",
8315
+ inst,
8316
+ faulted,
8317
+ () => on.tick?.(e, data, frame2, this.ctx)
8318
+ );
8068
8319
  }
8069
8320
  this.walker = void 0;
8070
8321
  }
@@ -8075,17 +8326,38 @@ function createBehaviorRuntime(opts) {
8075
8326
  * kill a 1000-node layout.
8076
8327
  */
8077
8328
  callHook(hook, inst, faulted, body) {
8329
+ this.hookEntity = inst.e;
8078
8330
  try {
8079
- body();
8331
+ invokeHook(this.b.name, hook, body);
8080
8332
  inst.consecutiveThrows = 0;
8081
8333
  return true;
8082
8334
  } catch (err) {
8083
8335
  inst.consecutiveThrows++;
8084
8336
  onFault(this.b.name, hook, inst.e, err);
8085
- if (inst.consecutiveThrows >= INSTANCE_THROW_LIMIT) inst.failed = true;
8337
+ if (inst.consecutiveThrows >= INSTANCE_THROW_LIMIT) {
8338
+ inst.failed = true;
8339
+ if (this.b.store === "ephemeral") this.singletonQuarantined = true;
8340
+ this.withdrawFacet();
8341
+ }
8086
8342
  faulted.add(inst.e);
8087
- if (faulted.size > 1) this.guest.fault(err);
8343
+ const frameTick = world.getResource(FrameInfo)?.tick ?? -1;
8344
+ if (frameTick !== this.strikeFrame) {
8345
+ this.strikeFrame = frameTick;
8346
+ this.thenableStruckPass = false;
8347
+ this.spanStruckPass = false;
8348
+ }
8349
+ if (err instanceof BehaviorHookThenableError) {
8350
+ if (!this.thenableStruckPass) {
8351
+ this.thenableStruckPass = true;
8352
+ this.guest.fault(err);
8353
+ }
8354
+ } else if (faulted.size > 1 && !this.spanStruckPass) {
8355
+ this.spanStruckPass = true;
8356
+ this.guest.fault(err);
8357
+ }
8088
8358
  return false;
8359
+ } finally {
8360
+ this.hookEntity = void 0;
8089
8361
  }
8090
8362
  }
8091
8363
  // --- generation --------------------------------------------------------
@@ -8093,15 +8365,23 @@ function createBehaviorRuntime(opts) {
8093
8365
  const on = this.b.on;
8094
8366
  if (on.dispose !== void 0) {
8095
8367
  for (const inst of this.instances.values()) {
8368
+ this.hookEntity = inst.e;
8096
8369
  try {
8097
- on.dispose(inst.e, this.ctx);
8370
+ invokeHook(
8371
+ this.b.name,
8372
+ "dispose",
8373
+ () => on.dispose?.(inst.e, this.ctx)
8374
+ );
8098
8375
  } catch (err) {
8099
8376
  onFault(this.b.name, "dispose", inst.e, err);
8377
+ } finally {
8378
+ this.hookEntity = void 0;
8100
8379
  }
8101
8380
  }
8102
8381
  }
8103
8382
  this.instances.clear();
8104
- this.facetPeer = void 0;
8383
+ this.withdrawFacet();
8384
+ this.singletonQuarantined = false;
8105
8385
  this.suppressed.clear();
8106
8386
  this.suppressedFrames.clear();
8107
8387
  this.carry.clear();
@@ -8199,10 +8479,12 @@ function createBehaviorRuntime(opts) {
8199
8479
  }
8200
8480
  /** `ctx.write(patch)` for the ephemeral class — publish THIS peer's facet. */
8201
8481
  writeFacet(patch) {
8482
+ if (this.stopped || this.cold || this.singletonQuarantined) return;
8202
8483
  const presence = opts.presence?.();
8203
8484
  if (presence === void 0) return;
8204
8485
  const peer = presence.localPeer;
8205
8486
  if (!world.isAlive(peer)) return;
8487
+ if (this.hookEntity !== void 0 && this.hookEntity !== peer) return;
8206
8488
  const cell = cellForBehavior(world, this.b, peer, patch);
8207
8489
  if (world.has(peer, this.own)) presence.eph.edit(peer).set(this.own, cell);
8208
8490
  else presence.eph.addComponent(peer, this.own, cell);
@@ -8368,10 +8650,32 @@ function createBehaviorRuntime(opts) {
8368
8650
  instances: this.instances.size,
8369
8651
  failed,
8370
8652
  ticking: this.b.on.tick === void 0 ? 0 : this.instances.size - failed,
8371
- suspended: this.guest.suspended()
8653
+ suspended: this.guest.suspended(),
8654
+ quarantined: this.singletonQuarantined
8372
8655
  };
8373
8656
  }
8374
8657
  }
8658
+ function orderedNodes() {
8659
+ return [...nodes.values()].sort((left, right) => {
8660
+ if (left.orderKey !== void 0 || right.orderKey !== void 0) {
8661
+ if (left.orderKey === void 0) return 1;
8662
+ if (right.orderKey === void 0) return -1;
8663
+ if (left.orderKey < right.orderKey) return -1;
8664
+ if (left.orderKey > right.orderKey) return 1;
8665
+ }
8666
+ return left.registrationSequence - right.registrationSequence;
8667
+ });
8668
+ }
8669
+ function reorderExecution(inserted) {
8670
+ const order = orderedNodes();
8671
+ const at = order.indexOf(inserted);
8672
+ if (at === order.length - 1) {
8673
+ inserted.installExecution();
8674
+ return;
8675
+ }
8676
+ for (let i = order.length - 1; i > at; i--) order[i]?.uninstallExecution();
8677
+ for (let i = at; i < order.length; i++) order[i]?.installExecution();
8678
+ }
8375
8679
  function safeParse(raw) {
8376
8680
  try {
8377
8681
  return JSON.parse(raw);
@@ -8380,13 +8684,17 @@ function createBehaviorRuntime(opts) {
8380
8684
  }
8381
8685
  }
8382
8686
  return {
8383
- register(b) {
8687
+ register(b, options = {}) {
8688
+ if (options.orderKey !== void 0 && (typeof options.orderKey !== "string" || options.orderKey.length === 0)) {
8689
+ throw new TypeError("ice: behavior registration orderKey must be a non-empty string");
8690
+ }
8384
8691
  if (nodes.has(b.name)) {
8385
8692
  throw new Error(`ice: behavior "${b.name}" is already registered with this engine.`);
8386
8693
  }
8387
- const node = new Node(b);
8694
+ const node = new Node(b, options, nextRegistrationSequence++);
8388
8695
  nodes.set(b.name, node);
8389
8696
  node.install();
8697
+ reorderExecution(node);
8390
8698
  return () => {
8391
8699
  if (nodes.get(b.name) !== node) return;
8392
8700
  nodes.delete(b.name);
@@ -8394,7 +8702,7 @@ function createBehaviorRuntime(opts) {
8394
8702
  };
8395
8703
  },
8396
8704
  registered() {
8397
- return [...nodes.values()].map((n) => n.b);
8705
+ return orderedNodes().map((n) => n.b);
8398
8706
  },
8399
8707
  attach(e, b, data) {
8400
8708
  if (b.store === "durable") {
@@ -8427,12 +8735,18 @@ function createBehaviorRuntime(opts) {
8427
8735
  return cell;
8428
8736
  },
8429
8737
  list() {
8430
- return [...nodes.values()].map((n) => n.status());
8738
+ return orderedNodes().map((n) => n.status());
8431
8739
  },
8432
8740
  dispose() {
8433
8741
  stopResetWatch();
8434
8742
  for (const n of nodes.values()) n.uninstall();
8435
8743
  nodes.clear();
8744
+ if (pendingWithdrawals.length > 0) {
8745
+ const jobs = pendingWithdrawals.splice(0, pendingWithdrawals.length);
8746
+ for (const job of jobs) job();
8747
+ }
8748
+ publishHookRemover?.();
8749
+ publishHookRemover = void 0;
8436
8750
  }
8437
8751
  };
8438
8752
  }
@@ -9416,6 +9730,10 @@ var DEFAULT_PRESENCE_TTL_MS = 5e3;
9416
9730
  function attachPresence(world, opts) {
9417
9731
  const peerId = opts.peerId ?? crypto.randomUUID();
9418
9732
  const ttlMs = opts.ttlMs ?? DEFAULT_PRESENCE_TTL_MS;
9733
+ const onFault = opts.onFault ?? ((where, error) => {
9734
+ const detail = where === "outbound" ? "presence outbound subscriber threw \u2014 bytes dropped for that subscriber (transport handlers must not throw)" : where === "leave" ? "presence leave failed \u2014 tearing the binding down anyway (remote peers drop us at TTL instead of immediately)" : "presence reset-heal leave failed \u2014 rebinding anyway (stale keys age out at TTL)";
9735
+ console.error(`[ice] ${detail}`, error);
9736
+ });
9419
9737
  const loro = new LoroEphemeralStore(ttlMs);
9420
9738
  const source = new LoroEphemeralSnapshot(loro);
9421
9739
  const outbound = /* @__PURE__ */ new Set();
@@ -9423,7 +9741,13 @@ function attachPresence(world, opts) {
9423
9741
  peerId,
9424
9742
  ttlMs,
9425
9743
  send: (bytes) => {
9426
- for (const fn of [...outbound]) fn(bytes);
9744
+ for (const fn of [...outbound]) {
9745
+ try {
9746
+ fn(bytes);
9747
+ } catch (err) {
9748
+ onFault("outbound", err);
9749
+ }
9750
+ }
9427
9751
  }
9428
9752
  });
9429
9753
  let attachment = attachEphemeral(world, eph);
@@ -9441,7 +9765,11 @@ function attachPresence(world, opts) {
9441
9765
  queueMicrotask(() => {
9442
9766
  healQueued = false;
9443
9767
  if (detached) return;
9444
- eph.leave();
9768
+ try {
9769
+ eph.leave();
9770
+ } catch (err) {
9771
+ onFault("heal", err);
9772
+ }
9445
9773
  attachment.detach();
9446
9774
  attachment = attachEphemeral(world, eph);
9447
9775
  localPeer = spawnLocalPeer();
@@ -9467,7 +9795,11 @@ function attachPresence(world, opts) {
9467
9795
  if (detached) return;
9468
9796
  detached = true;
9469
9797
  stopResetHeal();
9470
- eph.leave();
9798
+ try {
9799
+ eph.leave();
9800
+ } catch (err) {
9801
+ onFault("leave", err);
9802
+ }
9471
9803
  attachment.detach();
9472
9804
  outbound.clear();
9473
9805
  loro.destroy();
@@ -9576,9 +9908,9 @@ function summaryEquals(a, b) {
9576
9908
  import { Not as Not9, defineQuery as defineQuery38, defineTickSystem as defineTickSystem15 } from "@vibecook/strata-ecs";
9577
9909
  import { Local as Local2 } from "@vibecook/strata-ecs";
9578
9910
  var remotePeerCursorsQ = defineQuery38([PresenceCursor, PresencePeer, Not9(Local2)]);
9579
- function createRemoteCursorsSystem(_world) {
9911
+ function createRemoteCursorsRig(world) {
9580
9912
  const pool = /* @__PURE__ */ new Map();
9581
- return defineTickSystem15(
9913
+ const system = defineTickSystem15(
9582
9914
  (ctx) => {
9583
9915
  const live = /* @__PURE__ */ new Set();
9584
9916
  ctx.query(remotePeerCursorsQ).each((b) => {
@@ -9612,16 +9944,38 @@ function createRemoteCursorsSystem(_world) {
9612
9944
  },
9613
9945
  {
9614
9946
  name: "remoteCursors",
9615
- access: { write: [Position, CursorVisual] }
9947
+ // orderIndependent: every row this system writes is its OWN pool's
9948
+ // cursor entity (one per remote peer) — row-disjoint from any other
9949
+ // Position writer by construction. The attestation is what lets a
9950
+ // userland `present`-phase behavior that co-writes Position silence
9951
+ // strata's double-writer advisory from its own side: suppression is
9952
+ // conjunctive over ALL co-writers, so an un-attested engine system
9953
+ // would make the warning unsilenceable for authors doing it right
9954
+ // (0.8.0 review finding).
9955
+ access: { write: [Position, CursorVisual], orderIndependent: [Position] }
9616
9956
  }
9617
9957
  );
9958
+ return {
9959
+ system,
9960
+ reap() {
9961
+ for (const [, entry] of pool) {
9962
+ if (world.isAlive(entry.cursor)) world.destroy(entry.cursor);
9963
+ }
9964
+ pool.clear();
9965
+ }
9966
+ };
9967
+ }
9968
+ function createRemoteCursorsSystem(world) {
9969
+ return createRemoteCursorsRig(world).system;
9618
9970
  }
9619
9971
  function installPresence(engine, session, opts = {}) {
9620
9972
  const removePublish = engine.onPublish(createPresencePublish(engine.world, session, opts));
9621
- const removeSystem = engine.addSystems("derive", createRemoteCursorsSystem(engine.world));
9973
+ const rig = createRemoteCursorsRig(engine.world);
9974
+ const removeSystem = engine.addSystems("present", rig.system);
9622
9975
  return () => {
9623
9976
  removePublish();
9624
9977
  removeSystem();
9978
+ rig.reap();
9625
9979
  };
9626
9980
  }
9627
9981
 
@@ -10030,7 +10384,10 @@ function createCanvasEngine(opts = {}) {
10030
10384
  }
10031
10385
  }
10032
10386
  const world = createWorld2();
10033
- const engine = createEngine(world);
10387
+ const engine = createEngine(world, {
10388
+ ...opts.onGuestFault === void 0 ? {} : { onGuestFault: opts.onGuestFault },
10389
+ ...opts.onGuestNotice === void 0 ? {} : { onGuestNotice: opts.onGuestNotice }
10390
+ });
10034
10391
  const sink = createForwardingSink();
10035
10392
  const stack = installInteractionStack(engine, { sink });
10036
10393
  const budgets = {
@@ -10087,15 +10444,36 @@ function createCanvasEngine(opts = {}) {
10087
10444
  sink.target = next.sink;
10088
10445
  return next;
10089
10446
  };
10447
+ const acquirePresence = (o) => {
10448
+ const p2 = attachPresence(world, o);
10449
+ presence = p2;
10450
+ uninstallPresence = installPresence(engine, p2, {
10451
+ keyOf: (e) => session?.store.keyOf(e)
10452
+ });
10453
+ return p2;
10454
+ };
10455
+ const releasePresence = () => {
10456
+ const uninstall = uninstallPresence;
10457
+ const p2 = presence;
10458
+ uninstallPresence = void 0;
10459
+ presence = void 0;
10460
+ try {
10461
+ uninstall?.();
10462
+ } catch (err) {
10463
+ console.error("[ice] presence uninstall threw \u2014 teardown continues", err);
10464
+ }
10465
+ try {
10466
+ p2?.detach();
10467
+ } catch (err) {
10468
+ console.error("[ice] presence detach threw \u2014 teardown continues", err);
10469
+ }
10470
+ };
10090
10471
  const closeDoc = () => {
10091
10472
  joinAbort?.abort(new Error("ice: docs.join superseded \u2014 another document was opened while joining."));
10092
10473
  joinAbort = void 0;
10093
10474
  for (const a of [...liveAutosaves]) a.stop();
10094
10475
  liveAutosaves.clear();
10095
- uninstallPresence?.();
10096
- uninstallPresence = void 0;
10097
- presence?.detach();
10098
- presence = void 0;
10476
+ releasePresence();
10099
10477
  joined?.leave();
10100
10478
  joined = void 0;
10101
10479
  session?.close();
@@ -10140,29 +10518,35 @@ function createCanvasEngine(opts = {}) {
10140
10518
  joinAbort = ac;
10141
10519
  const { presence: presenceOpts, ...joinOpts } = o;
10142
10520
  if (presenceOpts !== void 0) {
10143
- presence = attachPresence(world, { name: presenceOpts.name, color: presenceOpts.color });
10144
- uninstallPresence = installPresence(engine, presence, {
10145
- keyOf: (e) => session?.store.keyOf(e)
10146
- });
10521
+ acquirePresence({ name: presenceOpts.name, color: presenceOpts.color });
10147
10522
  }
10148
- const result = await joinDoc(world, channel, {
10149
- ...gatePolicy !== void 0 ? { docOpts: { onGate: gatePolicy } } : {},
10150
- ...presence !== void 0 ? { presence } : {},
10151
- ...joinOpts,
10152
- signal: ac.signal,
10153
- // Re-bootstrap swaps the session inside joinDoc (PendingImportError
10154
- // recovery) — re-target the forwarding sink or ops keep committing
10155
- // into the quarantined session.
10156
- onSession: (s) => {
10157
- if (joinAbort !== ac) return;
10158
- if (s !== void 0) {
10159
- adoptSession(s);
10160
- } else {
10161
- session = void 0;
10162
- sink.target = void 0;
10523
+ let result;
10524
+ try {
10525
+ result = await joinDoc(world, channel, {
10526
+ ...gatePolicy !== void 0 ? { docOpts: { onGate: gatePolicy } } : {},
10527
+ ...presence !== void 0 ? { presence } : {},
10528
+ ...joinOpts,
10529
+ signal: ac.signal,
10530
+ // Re-bootstrap swaps the session inside joinDoc (PendingImportError
10531
+ // recovery) — re-target the forwarding sink or ops keep committing
10532
+ // into the quarantined session.
10533
+ onSession: (s) => {
10534
+ if (joinAbort !== ac) return;
10535
+ if (s !== void 0) {
10536
+ adoptSession(s);
10537
+ } else {
10538
+ session = void 0;
10539
+ sink.target = void 0;
10540
+ }
10163
10541
  }
10542
+ });
10543
+ } catch (err) {
10544
+ if (joinAbort === ac) {
10545
+ joinAbort = void 0;
10546
+ releasePresence();
10164
10547
  }
10165
- });
10548
+ throw err;
10549
+ }
10166
10550
  if (joinAbort !== ac) {
10167
10551
  result.leave();
10168
10552
  throw new Error("ice: docs.join superseded \u2014 another document was opened while joining.");
@@ -10172,6 +10556,26 @@ function createCanvasEngine(opts = {}) {
10172
10556
  return result;
10173
10557
  },
10174
10558
  current: () => session,
10559
+ attachPresence(o) {
10560
+ if (session === void 0) {
10561
+ throw new Error(
10562
+ "ice: docs.attachPresence needs a live document \u2014 call docs.create()/open()/join() first."
10563
+ );
10564
+ }
10565
+ if (presence !== void 0) {
10566
+ throw new Error(
10567
+ "ice: docs.attachPresence \u2014 a presence session is already live (join({presence}) or a prior attach); detach it (its inverse, or docs.close()) before attaching another."
10568
+ );
10569
+ }
10570
+ const mine = acquirePresence(o);
10571
+ let done = false;
10572
+ return () => {
10573
+ if (done) return;
10574
+ done = true;
10575
+ if (presence !== mine) return;
10576
+ releasePresence();
10577
+ };
10578
+ },
10175
10579
  presence: () => presence,
10176
10580
  close: closeDoc,
10177
10581
  // Read-only documents must not mutate through history either — undo/redo
@@ -10201,11 +10605,14 @@ function createCanvasEngine(opts = {}) {
10201
10605
  world,
10202
10606
  engine,
10203
10607
  session: () => session,
10204
- // Presence attaches with `docs.join({presence})` and detaches with the
10205
- // document, so this reads through per publish: an ephemeral behavior
10206
- // installed before anyone joins simply lies dormant until there is a peer
10207
- // for it to be.
10208
- presence: () => presence
10608
+ // Presence attaches with `docs.join({presence})` OR `docs.attachPresence`
10609
+ // (petition I18) and detaches with the document / the attach inverse, so
10610
+ // this reads through per publish: an ephemeral behavior installed before
10611
+ // there is a presence session simply lies dormant until there is a peer
10612
+ // for it to be — and goes dormant again if the session detaches under it.
10613
+ presence: () => presence,
10614
+ ...opts.onBehaviorFault === void 0 ? {} : { onFault: opts.onBehaviorFault },
10615
+ ...opts.onBehaviorLog === void 0 ? {} : { onLog: opts.onBehaviorLog }
10209
10616
  });
10210
10617
  for (const b of opts.behaviors ?? []) behaviors2.register(b);
10211
10618
  const widgetQuery = () => {
@@ -10437,7 +10844,7 @@ import {
10437
10844
  Local as Local3
10438
10845
  } from "@vibecook/strata-ecs";
10439
10846
  import { DurableUndoStatus } from "@vibecook/strata-ecs/durable";
10440
- var CORE_VERSION = "0.6.0";
10847
+ var CORE_VERSION = "0.8.0";
10441
10848
 
10442
10849
  export {
10443
10850
  defineSchemaOnce,
@@ -10579,6 +10986,7 @@ export {
10579
10986
  BEHAVIOR_PHASES,
10580
10987
  BEHAVIOR_DEFAULT_PHASE,
10581
10988
  behaviors,
10989
+ describeBehavior,
10582
10990
  defineBehavior,
10583
10991
  isDurableEligible,
10584
10992
  compileBehavior,
@@ -10721,4 +11129,4 @@ export {
10721
11129
  Local3 as Local,
10722
11130
  DurableUndoStatus
10723
11131
  };
10724
- //# sourceMappingURL=chunk-JLXETATP.js.map
11132
+ //# sourceMappingURL=chunk-BE4F4X3Q.js.map