tutuca 0.9.115 → 0.10.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.
@@ -4944,8 +4944,8 @@ function parseProvide(s, px) {
4944
4944
  function parseMacroAttr(s, px) {
4945
4945
  return _parseSingle(s, px, G_ALL);
4946
4946
  }
4947
- function parseInputHandler(s, px) {
4948
- return _parseHandler(s, px, "input", true, true);
4947
+ function parseReceiveHandler(s, px) {
4948
+ return _parseHandler(s, px, "receive", true, true);
4949
4949
  }
4950
4950
  function parseAlterHandler(s, px) {
4951
4951
  const r = _parseHandler(s, px, "alter", false, false);
@@ -5488,11 +5488,11 @@ var EventHandler = class _EventHandler {
5488
5488
  return [this.handlerVal.evalAsHandler(stack), argValues];
5489
5489
  }
5490
5490
  static parse(s, px) {
5491
- const r = parseInputHandler(s, px);
5491
+ const r = parseReceiveHandler(s, px);
5492
5492
  return r === null ? null : new _EventHandler(r.handlerVal, r.args);
5493
5493
  }
5494
5494
  };
5495
- var RequestHandler = class {
5495
+ var IntentHandler = class {
5496
5496
  constructor(name, fn) {
5497
5497
  this.name = name;
5498
5498
  this.fn = fn;
@@ -6860,8 +6860,11 @@ var Components = class {
6860
6860
  getHandlerFor(v, name, key) {
6861
6861
  return this.getCompFor(v)?.[key][name] ?? null;
6862
6862
  }
6863
- getRequestFor(v, name) {
6864
- return this.getCompFor(v)?.scope.lookupRequest(name) ?? null;
6863
+ // The `lex` leg of an intent's route: the handlers registered on the scope chain of
6864
+ // the component that raised it, innermost first. A list, not a first match — see
6865
+ // ComponentStack.lookupIntentChain.
6866
+ getIntentChainFor(v, name) {
6867
+ return this.getCompFor(v)?.scope.lookupIntentChain(name) ?? [];
6865
6868
  }
6866
6869
  compileStyles() {
6867
6870
  const styles = [];
@@ -6874,7 +6877,7 @@ var ComponentStack = class _ComponentStack {
6874
6877
  this.comps = comps;
6875
6878
  this.parent = parent;
6876
6879
  this.byName = {};
6877
- this.reqsByName = {};
6880
+ this.intentsByName = {};
6878
6881
  this.macros = {};
6879
6882
  }
6880
6883
  enter() {
@@ -6906,11 +6909,21 @@ var ComponentStack = class _ComponentStack {
6906
6909
  getCompFor(v) {
6907
6910
  return this.comps.getCompFor(v);
6908
6911
  }
6909
- registerRequestHandlers(handlers) {
6910
- for (const name in handlers) this.reqsByName[name] = new RequestHandler(name, handlers[name]);
6912
+ // Handlers that answer an intent's `lex` leg. A name maps to a LIST, because the leg
6913
+ // walks: a handler that returns PASS declines and hands the intent to the next one.
6914
+ // A bare function is accepted as a one-element list.
6915
+ registerIntentHandlers(handlers) {
6916
+ for (const name in handlers) {
6917
+ const fns = Array.isArray(handlers[name]) ? handlers[name] : [handlers[name]];
6918
+ this.intentsByName[name] = fns.map((fn) => new IntentHandler(name, fn));
6919
+ }
6911
6920
  }
6912
- lookupRequest(name) {
6913
- return this.reqsByName[name] ?? this.parent?.lookupRequest(name) ?? null;
6921
+ // Innermost scope first, then outward. Concatenated rather than first-match, so a
6922
+ // declining handler can hand the intent to one further up.
6923
+ lookupIntentChain(name) {
6924
+ const here = this.intentsByName[name] ?? [];
6925
+ const up = this.parent?.lookupIntentChain(name) ?? [];
6926
+ return up.length === 0 ? here : here.concat(up);
6914
6927
  }
6915
6928
  lookupComponent(name) {
6916
6929
  return this.byName[name] ?? this.parent?.lookupComponent(name) ?? null;
@@ -6941,7 +6954,7 @@ var LookupInfo = class {
6941
6954
  }
6942
6955
  };
6943
6956
  var isString = (v) => typeof v === "string";
6944
- var _rawSpecKeys = "name view style commonStyle globalStyle input receive bubble response alter views provide lookup fields methods statics";
6957
+ var _rawSpecKeys = "name view style commonStyle globalStyle receive intent alter views provide lookup fields methods statics";
6945
6958
  var KNOWN_SPEC_KEYS = new Set(_rawSpecKeys.split(" "));
6946
6959
  var _compId = 0;
6947
6960
  var Component = class {
@@ -6952,10 +6965,8 @@ var Component = class {
6952
6965
  this.views = { main: new View("main", o.view, o.style) };
6953
6966
  this.commonStyle = o.commonStyle ?? "";
6954
6967
  this.globalStyle = o.globalStyle ?? "";
6955
- this.input = o.input ?? {};
6956
6968
  this.receive = o.receive ?? {};
6957
- this.bubble = o.bubble ?? {};
6958
- this.response = o.response ?? {};
6969
+ this.intent = o.intent ?? {};
6959
6970
  this.alter = o.alter ?? {};
6960
6971
  for (const name in o.views ?? {}) {
6961
6972
  const v = o.views[name];
@@ -7182,8 +7193,11 @@ var Transactor = class {
7182
7193
  this.transactions.push(t);
7183
7194
  this.onTransactionPushed(t);
7184
7195
  }
7185
- // Subscribe to a normalized record for every handler invocation (send/receive,
7186
- // bubble, request/response, input). Returns an unsubscribe function. Records
7196
+ // Subscribe to a normalized record for every handler invocation. `kind` is the
7197
+ // handler bucket (`receive` / `intent`), plus `answer` — an observation-only kind for
7198
+ // a message the runtime wrote to answer an intent. The BUCKET does not know about
7199
+ // that distinction, and must not: a handler cannot tell an answer from a message its
7200
+ // parent sent. Returns an unsubscribe function. Records
7187
7201
  // carry: kind, name, args, path, pathKeys, targetPath, handler, handlerName,
7188
7202
  // matched, before, after, parent, timestamp. Purely observational.
7189
7203
  observe(cb) {
@@ -7236,23 +7250,14 @@ var Transactor = class {
7236
7250
  this.pushTransaction(t);
7237
7251
  return this._link(t, parent);
7238
7252
  }
7239
- pushInput(path, name, args = [], opts = {}, parent = null) {
7240
- const t = new InputDispatchEvent(path, this, name, args, parent, opts);
7241
- this.pushTransaction(t);
7242
- return this._link(t, parent);
7243
- }
7244
- pushBubble(path, name, args = [], opts = {}, parent = null, targetPath = null) {
7245
- const newOpts = opts.skipSelf ? { ...opts, skipSelf: false } : opts;
7246
- const t = new BubbleEvent(path, this, name, args, parent, newOpts, targetPath);
7247
- this.pushTransaction(t);
7248
- return this._link(t, parent);
7249
- }
7250
- pushRequest(path, name, args = [], opts = {}, parent = null) {
7253
+ // Raise an intent: a job the sender did not address, walked along a route until
7254
+ // something answers. `opts.route` is a list of legs (see DEFAULT_ROUTE) and
7255
+ // `opts.livePath` opts out of pinning the answer's destination keys.
7256
+ pushIntent(path, name, args = [], opts = {}, parent = null) {
7251
7257
  const release = parent ? parent.completion.track() : null;
7252
- const p = this._runRequest(path, name, args, opts, parent, release);
7253
- this._inflight.add(p);
7254
- p.finally(() => this._inflight.delete(p));
7255
- return p;
7258
+ const walk = new IntentWalk(this, path, name, args, opts, parent, release);
7259
+ walk.advance();
7260
+ return walk;
7256
7261
  }
7257
7262
  // Drain queued transactions and await in-flight requests until quiescent. Each
7258
7263
  // awaited request enqueues a ResponseEvent, which may dispatch more work, so we
@@ -7263,57 +7268,6 @@ var Transactor = class {
7263
7268
  if (this._inflight.size) await Promise.allSettled([...this._inflight]);
7264
7269
  }
7265
7270
  }
7266
- async _runRequest(path, name, args = [], opts = {}, parent = null, release = null) {
7267
- let released = false;
7268
- const transfer = (t) => {
7269
- if (release) {
7270
- released = true;
7271
- t.completion.whenSubtreeSettled().then(release);
7272
- }
7273
- };
7274
- try {
7275
- const curRoot = this.state.val;
7276
- const txnPath = path.toTransactionPath();
7277
- const curLeaf = txnPath.lookup(curRoot);
7278
- const found = this.comps.getRequestFor(curLeaf, name);
7279
- const handler = found ?? mkReq404(name);
7280
- if (this._observers.length > 0) {
7281
- const reqPath = txnPath.pinKeys(curRoot);
7282
- this._emit({
7283
- kind: "request",
7284
- name,
7285
- args,
7286
- path: reqPath,
7287
- pathKeys: reqPath.toKeys(),
7288
- targetPath: path,
7289
- handler: found?.fn ?? null,
7290
- handlerName: found?.fn?.name || name,
7291
- matched: found ? "exact" : "none",
7292
- before: curLeaf,
7293
- after: void 0,
7294
- parent,
7295
- timestamp: Date.now()
7296
- });
7297
- }
7298
- const reqCtx = new RequestContext(path, this, parent, curRoot);
7299
- const resHandlerName = opts?.onResName ?? name;
7300
- const resPath = opts?.livePath ? null : txnPath.pinKeys(curRoot);
7301
- const push = (specificName, baseName, singleArg, result, error) => {
7302
- const resArgs = specificName ? [singleArg] : [result, error];
7303
- const t = new ResponseEvent(path, this, specificName ?? baseName, resArgs, parent, resPath);
7304
- transfer(t);
7305
- this.pushTransaction(t);
7306
- };
7307
- try {
7308
- const result = await handler.fn.apply(null, [...args, reqCtx]);
7309
- push(opts?.onOkName, resHandlerName, result, result, null);
7310
- } catch (error) {
7311
- push(opts?.onErrorName, resHandlerName, error, null, error);
7312
- }
7313
- } finally {
7314
- if (release && !released) release();
7315
- }
7316
- }
7317
7271
  get hasPendingTransactions() {
7318
7272
  return this.transactions.length > 0;
7319
7273
  }
@@ -7330,6 +7284,7 @@ var Transactor = class {
7330
7284
  this._emitTransaction(transaction, curState);
7331
7285
  } else console.warn("undefined new state", { curState, transaction });
7332
7286
  } finally {
7287
+ transaction.ensureWalkAdvanced?.();
7333
7288
  transaction._completion?.ensureSelfSettled();
7334
7289
  transaction._completion?.releaseSelf();
7335
7290
  }
@@ -7338,12 +7293,6 @@ var Transactor = class {
7338
7293
  this.transact(new InputEvent(path, event, eventHandler, this, dragInfo));
7339
7294
  }
7340
7295
  };
7341
- function mkReq404(name) {
7342
- const fn = () => {
7343
- throw new Error(`Request not found: ${name}`);
7344
- };
7345
- return { fn };
7346
- }
7347
7296
  function nullHandler() {
7348
7297
  return this;
7349
7298
  }
@@ -7374,6 +7323,15 @@ var Transaction = class {
7374
7323
  }
7375
7324
  afterTransaction() {
7376
7325
  }
7326
+ // Ending a walk and forwarding belong to the two dispatch buckets; the base warns
7327
+ // rather than throwing, so a stray call from the wrong place is a message and not a
7328
+ // crash. `walk` is undefined here, which is how ctx.reply/ctx.fail tell the two apart.
7329
+ stop() {
7330
+ console.warn('ctx.stop() is only meaningful in an "intent" handler - ignored');
7331
+ }
7332
+ forward(_opts) {
7333
+ console.warn('ctx.forward() needs a "receive" or "intent" handler - ignored');
7334
+ }
7377
7335
  buildRootStack(root, comps) {
7378
7336
  return Stack2.root(comps, root);
7379
7337
  }
@@ -7435,18 +7393,40 @@ var InputEvent = class extends Transaction {
7435
7393
  this._dispatchPath ??= this.path.compact();
7436
7394
  return this._dispatchPath;
7437
7395
  }
7396
+ // A DOM event is an ADDRESSED message like any other — it reaches the component that
7397
+ // owns the view and stops. It keeps its own class only because it resolves its handler
7398
+ // from the compiled view and runs synchronously, not because it is a different channel.
7438
7399
  get observeKind() {
7439
- return "input";
7400
+ return "receive";
7440
7401
  }
7441
7402
  get observeName() {
7442
7403
  return this.e?.type ?? null;
7443
7404
  }
7405
+ // A view's name is a message. `forward` is where that name can LEAVE the component:
7406
+ // it becomes an intent with the same name and the same arguments, so a view that says
7407
+ // `@on.click="saveDraft .text"` never has to change when an ancestor takes the job over.
7408
+ // (A `$method` handler has a name too, so forwarding one raises an intent under it.)
7409
+ forward(opts) {
7410
+ this._forward = opts ?? {};
7411
+ }
7412
+ afterTransaction() {
7413
+ const f = this._forward;
7414
+ if (f === void 0) return;
7415
+ this._forward = void 0;
7416
+ const { args = this._handlerArgs ?? [], ...rest } = f;
7417
+ const hv = this.handler?.handlerCall?.handlerVal ?? this.handler?.handlerVal;
7418
+ const name = hv?.name;
7419
+ if (name === void 0)
7420
+ return console.warn("ctx.forward() from a handler with no name - ignored");
7421
+ this.transactor.pushIntent(this.dispatchPath, name, args, rest, this);
7422
+ }
7444
7423
  buildRootStack(root, comps) {
7445
7424
  return Stack2.root(comps, root, this);
7446
7425
  }
7447
7426
  getHandlerAndArgs(root, _instance, comps) {
7448
7427
  const stack = this.buildStack(root, comps);
7449
7428
  const [handler, args] = this.handler.getHandlerAndArgs(stack, this);
7429
+ this._handlerArgs = [...args];
7450
7430
  const path = this.dispatchPath;
7451
7431
  args.push(new EventContext(path, this.transactor, this));
7452
7432
  return [handler, args];
@@ -7531,39 +7511,190 @@ var NameArgsTransaction = class extends Transaction {
7531
7511
  return [handler, [...this.args, new EventContext(this.path, this.transactor, this)]];
7532
7512
  }
7533
7513
  };
7534
- var ResponseEvent = class extends NameArgsTransaction {
7535
- handlerProp = "response";
7536
- constructor(path, transactor, name, args, parent, txnPath = null) {
7537
- super(path, transactor, name, args, parent);
7538
- this._txnPath = txnPath;
7539
- }
7540
- getTransactionPath() {
7541
- return this._txnPath ?? super.getTransactionPath();
7542
- }
7543
- };
7544
7514
  var SendEvent = class extends NameArgsTransaction {
7545
7515
  handlerProp = "receive";
7546
- run(rootVal, comps) {
7547
- return this.opts.skipSelf ? rootVal : this.updateRootValue(rootVal, comps);
7516
+ // `receive` is the ADDRESSED bucket, and it is the only one: a view's `@on.click`,
7517
+ // a parent's ctx.send, the host's sendAtRoot and an answer to an intent all arrive
7518
+ // here, and a handler cannot tell them apart. Splitting on where a message came from
7519
+ // would let the view's name through and turn the parent's away, though the two say
7520
+ // the same thing — and neither a test nor a parent could drive the component.
7521
+ get observeKind() {
7522
+ return this._isAnswer ? "answer" : "receive";
7523
+ }
7524
+ // `forward` in a RECEIVE body starts a walk: the message that arrived becomes an
7525
+ // intent, keeping its name and payload. This is what lets a view's name leave the
7526
+ // component without the view changing.
7527
+ forward(opts) {
7528
+ this._forward = opts ?? {};
7548
7529
  }
7549
7530
  afterTransaction() {
7550
- const { path, name, args, opts, targetPath } = this;
7551
- if (opts.bubbles && path.steps.length > 0)
7552
- this.transactor.pushBubble(path.popStep(), name, args, opts, this, targetPath);
7531
+ const f = this._forward;
7532
+ if (f === void 0) return;
7533
+ this._forward = void 0;
7534
+ const { args = this.args, ...rest } = f;
7535
+ this.transactor.pushIntent(this.path, this.name, args, rest, this);
7553
7536
  }
7554
7537
  };
7555
- var BubbleEvent = class extends SendEvent {
7556
- handlerProp = "bubble";
7557
- constructor(path, transactor, name, args, parent, opts, targetPath) {
7558
- super(path, transactor, name, args, parent, opts);
7559
- this.targetPath = targetPath ?? path;
7538
+ var IntentEvent = class extends NameArgsTransaction {
7539
+ handlerProp = "intent";
7540
+ constructor(path, transactor, walk) {
7541
+ super(path, transactor, walk.name, walk.args, walk.parent, {});
7542
+ this.walk = walk;
7543
+ this.targetPath = walk.origin;
7560
7544
  }
7561
- stopPropagation() {
7562
- this.opts.bubbles = false;
7545
+ // `forward` in an INTENT body amends the hop about to be pushed — it does not push
7546
+ // one itself, because a walk advances on its own.
7547
+ forward(opts) {
7548
+ this.walk.amend(opts, this.path);
7549
+ }
7550
+ stop() {
7551
+ this.walk.finish(null, null);
7552
+ }
7553
+ afterTransaction() {
7554
+ this.ensureWalkAdvanced();
7555
+ }
7556
+ // Idempotent, because both afterTransaction and the transactor's `finally` call it —
7557
+ // whichever gets there first advances the walk exactly once.
7558
+ ensureWalkAdvanced() {
7559
+ if (this._advanced) return;
7560
+ this._advanced = true;
7561
+ this.walk.advance();
7563
7562
  }
7564
7563
  };
7565
- var InputDispatchEvent = class extends NameArgsTransaction {
7566
- handlerProp = "input";
7564
+ var PASS = /* @__PURE__ */ Symbol.for("tutuca.intent.pass");
7565
+ var DEFAULT_ROUTE = ["dyn", "lex"];
7566
+ var INTENT_DEPTH = 64;
7567
+ var IntentWalk = class {
7568
+ constructor(transactor, path, name, args, opts, parent, release) {
7569
+ this.transactor = transactor;
7570
+ this.name = name;
7571
+ this.args = args;
7572
+ this.route = opts?.route ?? DEFAULT_ROUTE;
7573
+ this.parent = parent;
7574
+ this.release = release;
7575
+ this.origin = path;
7576
+ this.answerPath = opts?.livePath ? null : path.toTransactionPath().pinKeys(transactor.state.val);
7577
+ this.legIndex = 0;
7578
+ this.dynAt = path;
7579
+ this.hops = 0;
7580
+ this.ended = false;
7581
+ }
7582
+ // Offer the intent to the next hop. Called once at dispatch, and again by every hop
7583
+ // that ran without answering.
7584
+ advance() {
7585
+ if (this.ended) return;
7586
+ if (this.hops++ >= INTENT_DEPTH) return this.exhaust("intentDepth");
7587
+ while (this.legIndex < this.route.length) {
7588
+ const leg = this.route[this.legIndex];
7589
+ if (leg === "dyn") {
7590
+ if (this.dynAt.steps.length === 0) {
7591
+ this.legIndex++;
7592
+ continue;
7593
+ }
7594
+ this.dynAt = this.dynAt.popStep();
7595
+ this.transactor.pushTransaction(new IntentEvent(this.dynAt, this.transactor, this));
7596
+ return;
7597
+ }
7598
+ if (leg === "lex") {
7599
+ this.legIndex++;
7600
+ return this._tryLex();
7601
+ }
7602
+ console.warn("unknown intent route leg", leg, '- expected "dyn" or "lex"');
7603
+ this.legIndex++;
7604
+ }
7605
+ this.exhaust("noHandler");
7606
+ }
7607
+ // The `lex` leg: the handlers registered on the scope chain of the component that
7608
+ // raised the intent, in order.
7609
+ _tryLex() {
7610
+ const root = this.transactor.state.val;
7611
+ const txnPath = this.origin.toTransactionPath();
7612
+ const leaf = txnPath.lookup(root);
7613
+ const chain = this.transactor.comps.getIntentChainFor(leaf, this.name);
7614
+ if (this.transactor._observers.length > 0) {
7615
+ const path = txnPath.pinKeys(root);
7616
+ this.transactor._emit({
7617
+ kind: "intent",
7618
+ name: this.name,
7619
+ args: this.args,
7620
+ path,
7621
+ pathKeys: path.toKeys(),
7622
+ targetPath: this.origin,
7623
+ handler: chain[0]?.fn ?? null,
7624
+ handlerName: chain[0]?.fn?.name || this.name,
7625
+ matched: chain.length > 0 ? "exact" : "none",
7626
+ before: leaf,
7627
+ after: void 0,
7628
+ parent: this.parent,
7629
+ timestamp: Date.now()
7630
+ });
7631
+ }
7632
+ if (chain.length === 0) return this.advance();
7633
+ const p = this._runLex(chain, 0);
7634
+ this.transactor._inflight.add(p);
7635
+ p.finally(() => this.transactor._inflight.delete(p));
7636
+ }
7637
+ async _runLex(chain, i) {
7638
+ if (this.ended) return;
7639
+ if (i >= chain.length) return this.advance();
7640
+ const ctx = new IntentContext(this.origin, this.transactor, this.parent);
7641
+ try {
7642
+ const res = await chain[i].fn.apply(null, [...this.args, ctx]);
7643
+ if (res === PASS) return this._runLex(chain, i + 1);
7644
+ this.answer("Ok", res);
7645
+ } catch (error) {
7646
+ this.answer("Error", error);
7647
+ }
7648
+ }
7649
+ // A hop answered. Ends the walk.
7650
+ answer(suffix, value) {
7651
+ this.finish(`${this.name}${suffix}`, [value]);
7652
+ }
7653
+ // `forward` from an intent body: keep walking, optionally with new arguments or a
7654
+ // narrowed route.
7655
+ amend(opts, from) {
7656
+ if (opts?.args !== void 0) this.args = opts.args;
7657
+ if (opts?.route !== void 0) {
7658
+ this.route = opts.route;
7659
+ this.legIndex = 0;
7660
+ this.dynAt = from ?? this.dynAt;
7661
+ }
7662
+ }
7663
+ // The route ran out. What the sender hears depends only on what it DECLARES, which is
7664
+ // how "a sender expects an answer if and only if it declares an answer arm" becomes
7665
+ // code — nobody writes it down twice, and there is no schema to keep in step.
7666
+ exhaust(reason) {
7667
+ if (this.ended) return;
7668
+ const { name, args } = this;
7669
+ const comp = this.transactor.comps.getCompFor(
7670
+ this.origin.toTransactionPath().lookup(this.transactor.state.val)
7671
+ );
7672
+ const declares = (n) => comp?.receive?.[n] !== void 0;
7673
+ if (declares(`${name}Unhandled`)) {
7674
+ this.finish(`${name}Unhandled`, args);
7675
+ } else if (declares(`${name}Error`)) {
7676
+ this.finish(`${name}Error`, [reason]);
7677
+ } else {
7678
+ if (declares(`${name}Ok`))
7679
+ console.warn(
7680
+ `intent "${name}" was not answered (${reason}) and this component declares only "${name}Ok" - add "${name}Unhandled" or "${name}Error" to handle it`
7681
+ );
7682
+ this.finish(null, null);
7683
+ }
7684
+ }
7685
+ // End the walk. `name === null` means no answer goes out, which is what `stop` and a
7686
+ // fire-and-forget notification both are — the tracked unit is released here instead
7687
+ // of following an answer's subtree.
7688
+ finish(name, args) {
7689
+ if (this.ended) return;
7690
+ this.ended = true;
7691
+ if (name === null) return this.release?.();
7692
+ const path = this.answerPath ?? this.origin;
7693
+ const t = new SendEvent(path, this.transactor, name, args, this.parent, {});
7694
+ t._isAnswer = true;
7695
+ this.transactor.pushTransaction(t);
7696
+ if (this.release) t.completion.whenSubtreeSettled().then(this.release);
7697
+ }
7567
7698
  };
7568
7699
  var Completion = class {
7569
7700
  constructor() {
@@ -7644,23 +7775,21 @@ var Dispatcher = class {
7644
7775
  get at() {
7645
7776
  return new PathChanges(this);
7646
7777
  }
7778
+ // A message is ADDRESSED: it names one component and stops there.
7647
7779
  send(name, args, opts) {
7648
7780
  return this.sendAtPath(this.path, name, args, opts);
7649
7781
  }
7650
- bubble(name, args, opts) {
7651
- return this.send(name, args, { skipSelf: true, bubbles: true, ...opts });
7652
- }
7653
7782
  sendAtPath(path, name, args, opts) {
7654
7783
  return this.transactor.pushSend(path, name, args, opts, this.parent);
7655
7784
  }
7656
- request(name, args, opts) {
7657
- return this.requestAtPath(this.path, name, args, opts);
7785
+ // An intent is ROUTED: it names a job and walks until something answers. The verb
7786
+ // does not decide which scope answers — `opts.route` does, written here at the call
7787
+ // site, which is where the decision actually is.
7788
+ intent(name, args, opts) {
7789
+ return this.intentAtPath(this.path, name, args, opts);
7658
7790
  }
7659
- requestAtPath(path, name, args, opts) {
7660
- return this.transactor.pushRequest(path, name, args, opts, this.parent);
7661
- }
7662
- inputAtPath(path, name, args, opts) {
7663
- return this.transactor.pushInput(path, name, args, opts, this.parent);
7791
+ intentAtPath(path, name, args, opts) {
7792
+ return this.transactor.pushIntent(path, name, args, opts, this.parent);
7664
7793
  }
7665
7794
  lookupTypeFor(name, inst) {
7666
7795
  return this.transactor.comps.getCompFor(inst).scope.lookupComponent(name);
@@ -7673,12 +7802,30 @@ var EventContext = class extends Dispatcher {
7673
7802
  get targetPath() {
7674
7803
  return this.parent.targetPath;
7675
7804
  }
7676
- stopPropagation() {
7677
- return this.parent.stopPropagation();
7805
+ // Answer the intent being handled, with a result or with an error. Either ENDS the
7806
+ // walk. Legal only in an `intent` handler: a message has no sender waiting on it.
7807
+ reply(value) {
7808
+ return this.parent.walk === void 0 ? warnNotIntent("reply") : this.parent.walk.answer("Ok", value);
7809
+ }
7810
+ fail(error) {
7811
+ return this.parent.walk === void 0 ? warnNotIntent("fail") : this.parent.walk.answer("Error", error);
7812
+ }
7813
+ // End the walk answering nothing — "served, and no answer".
7814
+ stop() {
7815
+ return this.parent.stop();
7816
+ }
7817
+ // From an `intent` body: hand the intent to the next hop, optionally amending it.
7818
+ // From a `receive` body: turn the message that arrived into an intent. One word from
7819
+ // both ends of a walk; which one you get depends on which bucket you are in.
7820
+ forward(opts) {
7821
+ return this.parent.forward(opts);
7678
7822
  }
7679
7823
  };
7680
- var RequestContext = class extends Dispatcher {
7824
+ var IntentContext = class extends Dispatcher {
7681
7825
  };
7826
+ function warnNotIntent(verb) {
7827
+ console.warn(`ctx.${verb}() is only meaningful in an "intent" handler - ignored`);
7828
+ }
7682
7829
  var PathChanges = class extends PathBuilder {
7683
7830
  constructor(dispatcher) {
7684
7831
  super();
@@ -7687,8 +7834,8 @@ var PathChanges = class extends PathBuilder {
7687
7834
  send(name, args, opts) {
7688
7835
  return this.dispatcher.sendAtPath(this.buildPath(), name, args, opts);
7689
7836
  }
7690
- bubble(name, args, opts) {
7691
- return this.send(name, args, { skipSelf: true, bubbles: true, ...opts });
7837
+ intent(name, args, opts) {
7838
+ return this.dispatcher.intentAtPath(this.buildPath(), name, args, opts);
7692
7839
  }
7693
7840
  buildPath() {
7694
7841
  return this.dispatcher.path.concat(this.pathChanges);
@@ -8026,7 +8173,7 @@ function collectAppClassesInSet(app) {
8026
8173
  }
8027
8174
 
8028
8175
  // src/on.js
8029
- var OP_KINDS = ["send", "bubble", "request", "input"];
8176
+ var OP_KINDS = ["send", "intent"];
8030
8177
  function phaseOps(phase) {
8031
8178
  const ops = [];
8032
8179
  for (const type of OP_KINDS) for (const a of phase[type] ?? []) ops.push({ type, ...a });
@@ -8036,11 +8183,6 @@ function phaseOps(phase) {
8036
8183
  function resolveArgs(args, self) {
8037
8184
  return typeof args === "function" ? args(self) ?? [] : args ?? [];
8038
8185
  }
8039
- function phaseHasBubble(phase) {
8040
- if (!phase) return false;
8041
- if (phase.bubble?.length) return true;
8042
- return (phase.do ?? []).some((op) => op.type === "bubble");
8043
- }
8044
8186
  function dispatchPhase(dispatcher, targetPath, phase, self) {
8045
8187
  if (!phase) return;
8046
8188
  for (const op of phaseOps(phase)) {
@@ -8049,18 +8191,8 @@ function dispatchPhase(dispatcher, targetPath, phase, self) {
8049
8191
  case "send":
8050
8192
  dispatcher.sendAtPath(targetPath, op.name, args, op.opts);
8051
8193
  break;
8052
- case "bubble":
8053
- dispatcher.sendAtPath(targetPath, op.name, args, {
8054
- skipSelf: true,
8055
- bubbles: true,
8056
- ...op.opts
8057
- });
8058
- break;
8059
- case "request":
8060
- dispatcher.requestAtPath(targetPath, op.name, args, op.opts);
8061
- break;
8062
- case "input":
8063
- dispatcher.inputAtPath(targetPath, op.name, args, op.opts);
8194
+ case "intent":
8195
+ dispatcher.intentAtPath(targetPath, op.name, args, op.opts);
8064
8196
  break;
8065
8197
  }
8066
8198
  }
@@ -8490,6 +8622,7 @@ export {
8490
8622
  OrderedMap as OMap,
8491
8623
  OrderedMap,
8492
8624
  OrderedSet,
8625
+ PASS,
8493
8626
  PairSorting,
8494
8627
  ParseContext,
8495
8628
  Range,
@@ -8538,7 +8671,6 @@ export {
8538
8671
  mergeDeep$1 as mergeDeep,
8539
8672
  mergeDeepWith$1 as mergeDeepWith,
8540
8673
  mergeWith$1 as mergeWith,
8541
- phaseHasBubble,
8542
8674
  phaseOps,
8543
8675
  remove,
8544
8676
  removeIn,