@vibecook/ice 0.6.0 → 0.7.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.
@@ -16,7 +16,7 @@ import {
16
16
  createSiblingOrderIndex,
17
17
  defineQuery,
18
18
  widgets
19
- } from "./chunk-JLXETATP.js";
19
+ } from "./chunk-LATXYFMO.js";
20
20
  import {
21
21
  planeCssTransform,
22
22
  worldToScreen
@@ -1160,4 +1160,4 @@ export {
1160
1160
  wireMeasurement,
1161
1161
  DOM_VERSION
1162
1162
  };
1163
- //# sourceMappingURL=chunk-ELCBCTKY.js.map
1163
+ //# sourceMappingURL=chunk-55QCOE74.js.map
@@ -12,7 +12,7 @@ import {
12
12
  keyboardClaimOf,
13
13
  startRafLoop,
14
14
  wireMeasurement
15
- } from "./chunk-ELCBCTKY.js";
15
+ } from "./chunk-55QCOE74.js";
16
16
  import {
17
17
  ActiveTool,
18
18
  DurableUndoStatus,
@@ -33,7 +33,7 @@ import {
33
33
  tools,
34
34
  widgets,
35
35
  writeRuntimeResource
36
- } from "./chunk-JLXETATP.js";
36
+ } from "./chunk-LATXYFMO.js";
37
37
 
38
38
  // ../react/src/hooks.ts
39
39
  import { createContext, useCallback, useContext, useMemo, useRef, useSyncExternalStore } from "react";
@@ -673,4 +673,4 @@ export {
673
673
  InfiniteCanvas,
674
674
  REACT_VERSION
675
675
  };
676
- //# sourceMappingURL=chunk-BFT7YWS2.js.map
676
+ //# sourceMappingURL=chunk-5OU3OWDH.js.map
@@ -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,19 @@ function createBehaviorRuntime(opts) {
7449
7594
  peersQuery;
7450
7595
  /** Ephemeral only: the local peer this node last published a facet on. */
7451
7596
  facetPeer;
7597
+ /** The ephemeral singleton was quarantined THIS generation (I17). */
7598
+ singletonQuarantined = false;
7599
+ /** Uninstalled — a captured ctx.write closure must go inert (I17). */
7600
+ stopped = false;
7601
+ /** Per-FRAME strike dedupe (review finding 4): the ladder counts FRAMES,
7602
+ * not calls — 500 async instances must cost ONE strike, not write 500
7603
+ * into a ledger hosts persist across engine generations. Keyed by
7604
+ * FrameInfo.tick because no single pass entry point runs every frame
7605
+ * (the delivery system is runIf-gated off on no-churn frames while the
7606
+ * tick system keeps running). */
7607
+ strikeFrame = -1;
7608
+ thenableStruckPass = false;
7609
+ spanStruckPass = false;
7452
7610
  /** The session whose version state has already been checked (§5.6). */
7453
7611
  checkedSession;
7454
7612
  /**
@@ -7519,8 +7677,10 @@ function createBehaviorRuntime(opts) {
7519
7677
  /** Set for the duration of a delivery/tick pass; see BehaviorWalker. */
7520
7678
  walker;
7521
7679
  ctx;
7522
- constructor(b) {
7680
+ constructor(b, registration, registrationSequence) {
7523
7681
  this.b = b;
7682
+ this.orderKey = registration.orderKey;
7683
+ this.registrationSequence = registrationSequence;
7524
7684
  this.own = b.component;
7525
7685
  this.ownFields = b.component;
7526
7686
  this.schema = b.schema;
@@ -7543,6 +7703,7 @@ function createBehaviorRuntime(opts) {
7543
7703
  this.guest = opts.engine.guests.addDriven({
7544
7704
  id: `behavior:${b.name}`,
7545
7705
  ...b.budgetMs !== void 0 ? { budgetMs: b.budgetMs } : {},
7706
+ ...registration.ledger === void 0 ? {} : { ledger: registration.ledger },
7546
7707
  busy: () => this.owed()
7547
7708
  });
7548
7709
  this.ctx = this.buildCtx();
@@ -7576,6 +7737,7 @@ function createBehaviorRuntime(opts) {
7576
7737
  this.readsCollector?.dispose();
7577
7738
  this.ownCollector = void 0;
7578
7739
  this.readsCollector = void 0;
7740
+ this.withdrawFacet();
7579
7741
  }
7580
7742
  /**
7581
7743
  * Resumed. Fresh collectors journal from NOW, so everything that happened
@@ -7597,18 +7759,10 @@ function createBehaviorRuntime(opts) {
7597
7759
  if (this.compiled.readRelations.length > 0) {
7598
7760
  world.orderStamp(0, this.compiled.readRelations[0]);
7599
7761
  }
7762
+ }
7763
+ installExecution() {
7600
7764
  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
- });
7765
+ this.removeSystems = installPublishNode(this);
7612
7766
  } else {
7613
7767
  this.removeSystems = opts.engine.addSystems(
7614
7768
  this.b.phase,
@@ -7616,6 +7770,25 @@ function createBehaviorRuntime(opts) {
7616
7770
  );
7617
7771
  }
7618
7772
  }
7773
+ /** The publish-slot body — driven by the runtime's one stable hook. */
7774
+ runPublish() {
7775
+ if (this.guest.suspended()) {
7776
+ this.goCold();
7777
+ return;
7778
+ }
7779
+ this.guest.run(() => {
7780
+ if (this.b.store === "ephemeral") this.ensureFacet();
7781
+ if (this.shouldDeliver()) this.runDeliver(publishWalker(world));
7782
+ this.publishTick();
7783
+ });
7784
+ }
7785
+ isStopped() {
7786
+ return this.stopped;
7787
+ }
7788
+ uninstallExecution() {
7789
+ this.removeSystems?.();
7790
+ this.removeSystems = void 0;
7791
+ }
7619
7792
  /** The version-marker surface for the current session, if it has one. */
7620
7793
  metaOf(session) {
7621
7794
  const snapshot = session?.store.snapshot;
@@ -7701,6 +7874,10 @@ function createBehaviorRuntime(opts) {
7701
7874
  this.facetPeer = void 0;
7702
7875
  return;
7703
7876
  }
7877
+ if (this.singletonQuarantined) {
7878
+ this.facetPeer = void 0;
7879
+ return;
7880
+ }
7704
7881
  if (peer === this.facetPeer && world.has(peer, this.own)) return;
7705
7882
  this.facetPeer = peer;
7706
7883
  if (!world.has(peer, this.own)) {
@@ -7711,6 +7888,48 @@ function createBehaviorRuntime(opts) {
7711
7888
  );
7712
7889
  }
7713
7890
  }
7891
+ /**
7892
+ * Withdraw the published facet (petition I17). An ephemeral facet is LIVE
7893
+ * PUBLICATION — the local peer telling every remote peer "this value is
7894
+ * current" — so every edge where the producer stops executing must reverse
7895
+ * the publication too, or a disabled, suspended, or quarantined behavior
7896
+ * keeps asserting its last value forever (remote peers have no TTL for a
7897
+ * peer that is still connected). Idempotent by construction: presence
7898
+ * absent, a dead peer (mid re-mint window), and an already-gone facet are
7899
+ * all quiet no-ops. The removal goes through the PRESENCE writer, never
7900
+ * `world.removeComponent`, because only the presence path encodes a
7901
+ * tombstone remote projections honor.
7902
+ */
7903
+ withdrawFacet() {
7904
+ if (this.b.store !== "ephemeral") return;
7905
+ this.facetPeer = void 0;
7906
+ const presence = opts.presence?.();
7907
+ if (presence === void 0) return;
7908
+ const peer = presence.localPeer;
7909
+ if (!(world.isAlive(peer) && world.has(peer, this.own))) return;
7910
+ const midTick = world.inImmediateProjectionUnsafeContext ?? false;
7911
+ if (midTick) {
7912
+ const behaviorName = this.b.name;
7913
+ const own = this.own;
7914
+ deferWithdrawal(() => {
7915
+ const p2 = opts.presence?.();
7916
+ if (p2 === void 0) return;
7917
+ if (p2.localPeer !== peer) return;
7918
+ if (!(world.isAlive(peer) && world.has(peer, own))) return;
7919
+ try {
7920
+ p2.eph.removeComponent(peer, own);
7921
+ } catch (err) {
7922
+ onFault(behaviorName, "withdraw", peer, err);
7923
+ }
7924
+ });
7925
+ return;
7926
+ }
7927
+ try {
7928
+ presence.eph.removeComponent(peer, this.own);
7929
+ } catch (err) {
7930
+ onFault(this.b.name, "withdraw", peer, err);
7931
+ }
7932
+ }
7714
7933
  /** The publish-slot twin of the tick SYSTEM (no query, no chunks). */
7715
7934
  publishTick() {
7716
7935
  const on = this.b.on;
@@ -7723,16 +7942,19 @@ function createBehaviorRuntime(opts) {
7723
7942
  const inst = this.instances.get(e);
7724
7943
  if (inst === void 0 || inst.failed || !inst.inited) continue;
7725
7944
  const data = this.dataOf(e);
7726
- this.callHook("tick", inst, faulted, () => {
7727
- on.tick?.(e, data, frame2, this.ctx);
7728
- });
7945
+ this.callHook(
7946
+ "tick",
7947
+ inst,
7948
+ faulted,
7949
+ () => on.tick?.(e, data, frame2, this.ctx)
7950
+ );
7729
7951
  }
7730
7952
  this.walker = void 0;
7731
7953
  }
7732
7954
  uninstall() {
7955
+ this.stopped = true;
7733
7956
  this.endGeneration();
7734
- this.removeSystems?.();
7735
- this.removeSystems = void 0;
7957
+ this.uninstallExecution();
7736
7958
  this.ownCollector?.dispose();
7737
7959
  this.readsCollector?.dispose();
7738
7960
  this.ownCollector = void 0;
@@ -7995,9 +8217,12 @@ function createBehaviorRuntime(opts) {
7995
8217
  for (const e of appeared) {
7996
8218
  const inst = this.instances.get(e);
7997
8219
  if (inst === void 0 || inst.failed) continue;
7998
- if (this.callHook("init", inst, faultedEntities, () => {
7999
- on.init?.(e, this.dataOf(e), this.ctx);
8000
- })) {
8220
+ if (this.callHook(
8221
+ "init",
8222
+ inst,
8223
+ faultedEntities,
8224
+ () => on.init?.(e, this.dataOf(e), this.ctx)
8225
+ )) {
8001
8226
  inst.inited = true;
8002
8227
  }
8003
8228
  }
@@ -8019,16 +8244,19 @@ function createBehaviorRuntime(opts) {
8019
8244
  if (valueEquals2(this.own, this.scratchCell, inst.prevCell)) continue;
8020
8245
  const data = this.viewOf(this.scratchCell, this.scratchData);
8021
8246
  const prev = this.viewOf(inst.prevCell, this.scratchPrevData);
8022
- this.callHook("update", inst, faultedEntities, () => {
8023
- on.update?.(e, data, prev, this.ctx);
8024
- });
8247
+ this.callHook(
8248
+ "update",
8249
+ inst,
8250
+ faultedEntities,
8251
+ () => on.update?.(e, data, prev, this.ctx)
8252
+ );
8025
8253
  this.copyInto(this.scratchCell, inst.prevCell);
8026
8254
  }
8027
8255
  }
8028
8256
  const anyVisible = this.visible.length > 0;
8029
8257
  if (on.changed !== void 0 && (full || (work?.readsFired ?? false) || lifted) && anyVisible) {
8030
8258
  try {
8031
- on.changed(this.ctx);
8259
+ invokeHook(this.b.name, "changed", () => on.changed?.(this.ctx));
8032
8260
  } catch (err) {
8033
8261
  onFault(this.b.name, "changed", void 0, err);
8034
8262
  this.guest.fault(err);
@@ -8037,7 +8265,11 @@ function createBehaviorRuntime(opts) {
8037
8265
  if (on.dispose !== void 0) {
8038
8266
  for (const inst of departed) {
8039
8267
  try {
8040
- on.dispose(inst.e, this.ctx);
8268
+ invokeHook(
8269
+ this.b.name,
8270
+ "dispose",
8271
+ () => on.dispose?.(inst.e, this.ctx)
8272
+ );
8041
8273
  } catch (err) {
8042
8274
  onFault(this.b.name, "dispose", inst.e, err);
8043
8275
  }
@@ -8062,9 +8294,12 @@ function createBehaviorRuntime(opts) {
8062
8294
  const inst = this.instances.get(e);
8063
8295
  if (inst === void 0 || inst.failed || !inst.inited) continue;
8064
8296
  const data = this.dataOf(e);
8065
- this.callHook("tick", inst, faulted, () => {
8066
- on.tick?.(e, data, frame2, this.ctx);
8067
- });
8297
+ this.callHook(
8298
+ "tick",
8299
+ inst,
8300
+ faulted,
8301
+ () => on.tick?.(e, data, frame2, this.ctx)
8302
+ );
8068
8303
  }
8069
8304
  this.walker = void 0;
8070
8305
  }
@@ -8076,15 +8311,33 @@ function createBehaviorRuntime(opts) {
8076
8311
  */
8077
8312
  callHook(hook, inst, faulted, body) {
8078
8313
  try {
8079
- body();
8314
+ invokeHook(this.b.name, hook, body);
8080
8315
  inst.consecutiveThrows = 0;
8081
8316
  return true;
8082
8317
  } catch (err) {
8083
8318
  inst.consecutiveThrows++;
8084
8319
  onFault(this.b.name, hook, inst.e, err);
8085
- if (inst.consecutiveThrows >= INSTANCE_THROW_LIMIT) inst.failed = true;
8320
+ if (inst.consecutiveThrows >= INSTANCE_THROW_LIMIT) {
8321
+ inst.failed = true;
8322
+ if (this.b.store === "ephemeral") this.singletonQuarantined = true;
8323
+ this.withdrawFacet();
8324
+ }
8086
8325
  faulted.add(inst.e);
8087
- if (faulted.size > 1) this.guest.fault(err);
8326
+ const frameTick = world.getResource(FrameInfo)?.tick ?? -1;
8327
+ if (frameTick !== this.strikeFrame) {
8328
+ this.strikeFrame = frameTick;
8329
+ this.thenableStruckPass = false;
8330
+ this.spanStruckPass = false;
8331
+ }
8332
+ if (err instanceof BehaviorHookThenableError) {
8333
+ if (!this.thenableStruckPass) {
8334
+ this.thenableStruckPass = true;
8335
+ this.guest.fault(err);
8336
+ }
8337
+ } else if (faulted.size > 1 && !this.spanStruckPass) {
8338
+ this.spanStruckPass = true;
8339
+ this.guest.fault(err);
8340
+ }
8088
8341
  return false;
8089
8342
  }
8090
8343
  }
@@ -8094,14 +8347,19 @@ function createBehaviorRuntime(opts) {
8094
8347
  if (on.dispose !== void 0) {
8095
8348
  for (const inst of this.instances.values()) {
8096
8349
  try {
8097
- on.dispose(inst.e, this.ctx);
8350
+ invokeHook(
8351
+ this.b.name,
8352
+ "dispose",
8353
+ () => on.dispose?.(inst.e, this.ctx)
8354
+ );
8098
8355
  } catch (err) {
8099
8356
  onFault(this.b.name, "dispose", inst.e, err);
8100
8357
  }
8101
8358
  }
8102
8359
  }
8103
8360
  this.instances.clear();
8104
- this.facetPeer = void 0;
8361
+ this.withdrawFacet();
8362
+ this.singletonQuarantined = false;
8105
8363
  this.suppressed.clear();
8106
8364
  this.suppressedFrames.clear();
8107
8365
  this.carry.clear();
@@ -8199,6 +8457,7 @@ function createBehaviorRuntime(opts) {
8199
8457
  }
8200
8458
  /** `ctx.write(patch)` for the ephemeral class — publish THIS peer's facet. */
8201
8459
  writeFacet(patch) {
8460
+ if (this.stopped || this.cold || this.singletonQuarantined) return;
8202
8461
  const presence = opts.presence?.();
8203
8462
  if (presence === void 0) return;
8204
8463
  const peer = presence.localPeer;
@@ -8368,10 +8627,32 @@ function createBehaviorRuntime(opts) {
8368
8627
  instances: this.instances.size,
8369
8628
  failed,
8370
8629
  ticking: this.b.on.tick === void 0 ? 0 : this.instances.size - failed,
8371
- suspended: this.guest.suspended()
8630
+ suspended: this.guest.suspended(),
8631
+ quarantined: this.singletonQuarantined
8372
8632
  };
8373
8633
  }
8374
8634
  }
8635
+ function orderedNodes() {
8636
+ return [...nodes.values()].sort((left, right) => {
8637
+ if (left.orderKey !== void 0 || right.orderKey !== void 0) {
8638
+ if (left.orderKey === void 0) return 1;
8639
+ if (right.orderKey === void 0) return -1;
8640
+ if (left.orderKey < right.orderKey) return -1;
8641
+ if (left.orderKey > right.orderKey) return 1;
8642
+ }
8643
+ return left.registrationSequence - right.registrationSequence;
8644
+ });
8645
+ }
8646
+ function reorderExecution(inserted) {
8647
+ const order = orderedNodes();
8648
+ const at = order.indexOf(inserted);
8649
+ if (at === order.length - 1) {
8650
+ inserted.installExecution();
8651
+ return;
8652
+ }
8653
+ for (let i = order.length - 1; i > at; i--) order[i]?.uninstallExecution();
8654
+ for (let i = at; i < order.length; i++) order[i]?.installExecution();
8655
+ }
8375
8656
  function safeParse(raw) {
8376
8657
  try {
8377
8658
  return JSON.parse(raw);
@@ -8380,13 +8661,17 @@ function createBehaviorRuntime(opts) {
8380
8661
  }
8381
8662
  }
8382
8663
  return {
8383
- register(b) {
8664
+ register(b, options = {}) {
8665
+ if (options.orderKey !== void 0 && (typeof options.orderKey !== "string" || options.orderKey.length === 0)) {
8666
+ throw new TypeError("ice: behavior registration orderKey must be a non-empty string");
8667
+ }
8384
8668
  if (nodes.has(b.name)) {
8385
8669
  throw new Error(`ice: behavior "${b.name}" is already registered with this engine.`);
8386
8670
  }
8387
- const node = new Node(b);
8671
+ const node = new Node(b, options, nextRegistrationSequence++);
8388
8672
  nodes.set(b.name, node);
8389
8673
  node.install();
8674
+ reorderExecution(node);
8390
8675
  return () => {
8391
8676
  if (nodes.get(b.name) !== node) return;
8392
8677
  nodes.delete(b.name);
@@ -8394,7 +8679,7 @@ function createBehaviorRuntime(opts) {
8394
8679
  };
8395
8680
  },
8396
8681
  registered() {
8397
- return [...nodes.values()].map((n) => n.b);
8682
+ return orderedNodes().map((n) => n.b);
8398
8683
  },
8399
8684
  attach(e, b, data) {
8400
8685
  if (b.store === "durable") {
@@ -8427,12 +8712,18 @@ function createBehaviorRuntime(opts) {
8427
8712
  return cell;
8428
8713
  },
8429
8714
  list() {
8430
- return [...nodes.values()].map((n) => n.status());
8715
+ return orderedNodes().map((n) => n.status());
8431
8716
  },
8432
8717
  dispose() {
8433
8718
  stopResetWatch();
8434
8719
  for (const n of nodes.values()) n.uninstall();
8435
8720
  nodes.clear();
8721
+ if (pendingWithdrawals.length > 0) {
8722
+ const jobs = pendingWithdrawals.splice(0, pendingWithdrawals.length);
8723
+ for (const job of jobs) job();
8724
+ }
8725
+ publishHookRemover?.();
8726
+ publishHookRemover = void 0;
8436
8727
  }
8437
8728
  };
8438
8729
  }
@@ -10030,7 +10321,10 @@ function createCanvasEngine(opts = {}) {
10030
10321
  }
10031
10322
  }
10032
10323
  const world = createWorld2();
10033
- const engine = createEngine(world);
10324
+ const engine = createEngine(world, {
10325
+ ...opts.onGuestFault === void 0 ? {} : { onGuestFault: opts.onGuestFault },
10326
+ ...opts.onGuestNotice === void 0 ? {} : { onGuestNotice: opts.onGuestNotice }
10327
+ });
10034
10328
  const sink = createForwardingSink();
10035
10329
  const stack = installInteractionStack(engine, { sink });
10036
10330
  const budgets = {
@@ -10205,7 +10499,9 @@ function createCanvasEngine(opts = {}) {
10205
10499
  // document, so this reads through per publish: an ephemeral behavior
10206
10500
  // installed before anyone joins simply lies dormant until there is a peer
10207
10501
  // for it to be.
10208
- presence: () => presence
10502
+ presence: () => presence,
10503
+ ...opts.onBehaviorFault === void 0 ? {} : { onFault: opts.onBehaviorFault },
10504
+ ...opts.onBehaviorLog === void 0 ? {} : { onLog: opts.onBehaviorLog }
10209
10505
  });
10210
10506
  for (const b of opts.behaviors ?? []) behaviors2.register(b);
10211
10507
  const widgetQuery = () => {
@@ -10437,7 +10733,7 @@ import {
10437
10733
  Local as Local3
10438
10734
  } from "@vibecook/strata-ecs";
10439
10735
  import { DurableUndoStatus } from "@vibecook/strata-ecs/durable";
10440
- var CORE_VERSION = "0.6.0";
10736
+ var CORE_VERSION = "0.7.0";
10441
10737
 
10442
10738
  export {
10443
10739
  defineSchemaOnce,
@@ -10579,6 +10875,7 @@ export {
10579
10875
  BEHAVIOR_PHASES,
10580
10876
  BEHAVIOR_DEFAULT_PHASE,
10581
10877
  behaviors,
10878
+ describeBehavior,
10582
10879
  defineBehavior,
10583
10880
  isDurableEligible,
10584
10881
  compileBehavior,
@@ -10721,4 +11018,4 @@ export {
10721
11018
  Local3 as Local,
10722
11019
  DurableUndoStatus
10723
11020
  };
10724
- //# sourceMappingURL=chunk-JLXETATP.js.map
11021
+ //# sourceMappingURL=chunk-LATXYFMO.js.map