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.
@@ -615,8 +615,8 @@ function parseProvide(s, px) {
615
615
  function parseMacroAttr(s, px) {
616
616
  return _parseSingle(s, px, G_ALL);
617
617
  }
618
- function parseInputHandler(s, px) {
619
- return _parseHandler(s, px, "input", true, true);
618
+ function parseReceiveHandler(s, px) {
619
+ return _parseHandler(s, px, "receive", true, true);
620
620
  }
621
621
  function parseAlterHandler(s, px) {
622
622
  const r = _parseHandler(s, px, "alter", false, false);
@@ -1159,11 +1159,11 @@ var EventHandler = class _EventHandler {
1159
1159
  return [this.handlerVal.evalAsHandler(stack), argValues];
1160
1160
  }
1161
1161
  static parse(s, px) {
1162
- const r = parseInputHandler(s, px);
1162
+ const r = parseReceiveHandler(s, px);
1163
1163
  return r === null ? null : new _EventHandler(r.handlerVal, r.args);
1164
1164
  }
1165
1165
  };
1166
- var RequestHandler = class {
1166
+ var IntentHandler = class {
1167
1167
  constructor(name, fn) {
1168
1168
  this.name = name;
1169
1169
  this.fn = fn;
@@ -2534,8 +2534,11 @@ var Components = class {
2534
2534
  getHandlerFor(v, name, key) {
2535
2535
  return this.getCompFor(v)?.[key][name] ?? null;
2536
2536
  }
2537
- getRequestFor(v, name) {
2538
- return this.getCompFor(v)?.scope.lookupRequest(name) ?? null;
2537
+ // The `lex` leg of an intent's route: the handlers registered on the scope chain of
2538
+ // the component that raised it, innermost first. A list, not a first match — see
2539
+ // ComponentStack.lookupIntentChain.
2540
+ getIntentChainFor(v, name) {
2541
+ return this.getCompFor(v)?.scope.lookupIntentChain(name) ?? [];
2539
2542
  }
2540
2543
  compileStyles() {
2541
2544
  const styles = [];
@@ -2548,7 +2551,7 @@ var ComponentStack = class _ComponentStack {
2548
2551
  this.comps = comps;
2549
2552
  this.parent = parent;
2550
2553
  this.byName = {};
2551
- this.reqsByName = {};
2554
+ this.intentsByName = {};
2552
2555
  this.macros = {};
2553
2556
  }
2554
2557
  enter() {
@@ -2580,11 +2583,21 @@ var ComponentStack = class _ComponentStack {
2580
2583
  getCompFor(v) {
2581
2584
  return this.comps.getCompFor(v);
2582
2585
  }
2583
- registerRequestHandlers(handlers) {
2584
- for (const name in handlers) this.reqsByName[name] = new RequestHandler(name, handlers[name]);
2586
+ // Handlers that answer an intent's `lex` leg. A name maps to a LIST, because the leg
2587
+ // walks: a handler that returns PASS declines and hands the intent to the next one.
2588
+ // A bare function is accepted as a one-element list.
2589
+ registerIntentHandlers(handlers) {
2590
+ for (const name in handlers) {
2591
+ const fns = Array.isArray(handlers[name]) ? handlers[name] : [handlers[name]];
2592
+ this.intentsByName[name] = fns.map((fn) => new IntentHandler(name, fn));
2593
+ }
2585
2594
  }
2586
- lookupRequest(name) {
2587
- return this.reqsByName[name] ?? this.parent?.lookupRequest(name) ?? null;
2595
+ // Innermost scope first, then outward. Concatenated rather than first-match, so a
2596
+ // declining handler can hand the intent to one further up.
2597
+ lookupIntentChain(name) {
2598
+ const here = this.intentsByName[name] ?? [];
2599
+ const up = this.parent?.lookupIntentChain(name) ?? [];
2600
+ return up.length === 0 ? here : here.concat(up);
2588
2601
  }
2589
2602
  lookupComponent(name) {
2590
2603
  return this.byName[name] ?? this.parent?.lookupComponent(name) ?? null;
@@ -2615,7 +2628,7 @@ var LookupInfo = class {
2615
2628
  }
2616
2629
  };
2617
2630
  var isString = (v) => typeof v === "string";
2618
- var _rawSpecKeys = "name view style commonStyle globalStyle input receive bubble response alter views provide lookup fields methods statics";
2631
+ var _rawSpecKeys = "name view style commonStyle globalStyle receive intent alter views provide lookup fields methods statics";
2619
2632
  var KNOWN_SPEC_KEYS = new Set(_rawSpecKeys.split(" "));
2620
2633
  var _compId = 0;
2621
2634
  var Component = class {
@@ -2626,10 +2639,8 @@ var Component = class {
2626
2639
  this.views = { main: new View("main", o.view, o.style) };
2627
2640
  this.commonStyle = o.commonStyle ?? "";
2628
2641
  this.globalStyle = o.globalStyle ?? "";
2629
- this.input = o.input ?? {};
2630
2642
  this.receive = o.receive ?? {};
2631
- this.bubble = o.bubble ?? {};
2632
- this.response = o.response ?? {};
2643
+ this.intent = o.intent ?? {};
2633
2644
  this.alter = o.alter ?? {};
2634
2645
  for (const name in o.views ?? {}) {
2635
2646
  const v = o.views[name];
@@ -2856,8 +2867,11 @@ var Transactor = class {
2856
2867
  this.transactions.push(t);
2857
2868
  this.onTransactionPushed(t);
2858
2869
  }
2859
- // Subscribe to a normalized record for every handler invocation (send/receive,
2860
- // bubble, request/response, input). Returns an unsubscribe function. Records
2870
+ // Subscribe to a normalized record for every handler invocation. `kind` is the
2871
+ // handler bucket (`receive` / `intent`), plus `answer` — an observation-only kind for
2872
+ // a message the runtime wrote to answer an intent. The BUCKET does not know about
2873
+ // that distinction, and must not: a handler cannot tell an answer from a message its
2874
+ // parent sent. Returns an unsubscribe function. Records
2861
2875
  // carry: kind, name, args, path, pathKeys, targetPath, handler, handlerName,
2862
2876
  // matched, before, after, parent, timestamp. Purely observational.
2863
2877
  observe(cb) {
@@ -2910,23 +2924,14 @@ var Transactor = class {
2910
2924
  this.pushTransaction(t);
2911
2925
  return this._link(t, parent);
2912
2926
  }
2913
- pushInput(path, name, args = [], opts = {}, parent = null) {
2914
- const t = new InputDispatchEvent(path, this, name, args, parent, opts);
2915
- this.pushTransaction(t);
2916
- return this._link(t, parent);
2917
- }
2918
- pushBubble(path, name, args = [], opts = {}, parent = null, targetPath = null) {
2919
- const newOpts = opts.skipSelf ? { ...opts, skipSelf: false } : opts;
2920
- const t = new BubbleEvent(path, this, name, args, parent, newOpts, targetPath);
2921
- this.pushTransaction(t);
2922
- return this._link(t, parent);
2923
- }
2924
- pushRequest(path, name, args = [], opts = {}, parent = null) {
2927
+ // Raise an intent: a job the sender did not address, walked along a route until
2928
+ // something answers. `opts.route` is a list of legs (see DEFAULT_ROUTE) and
2929
+ // `opts.livePath` opts out of pinning the answer's destination keys.
2930
+ pushIntent(path, name, args = [], opts = {}, parent = null) {
2925
2931
  const release = parent ? parent.completion.track() : null;
2926
- const p = this._runRequest(path, name, args, opts, parent, release);
2927
- this._inflight.add(p);
2928
- p.finally(() => this._inflight.delete(p));
2929
- return p;
2932
+ const walk = new IntentWalk(this, path, name, args, opts, parent, release);
2933
+ walk.advance();
2934
+ return walk;
2930
2935
  }
2931
2936
  // Drain queued transactions and await in-flight requests until quiescent. Each
2932
2937
  // awaited request enqueues a ResponseEvent, which may dispatch more work, so we
@@ -2937,57 +2942,6 @@ var Transactor = class {
2937
2942
  if (this._inflight.size) await Promise.allSettled([...this._inflight]);
2938
2943
  }
2939
2944
  }
2940
- async _runRequest(path, name, args = [], opts = {}, parent = null, release = null) {
2941
- let released = false;
2942
- const transfer = (t) => {
2943
- if (release) {
2944
- released = true;
2945
- t.completion.whenSubtreeSettled().then(release);
2946
- }
2947
- };
2948
- try {
2949
- const curRoot = this.state.val;
2950
- const txnPath = path.toTransactionPath();
2951
- const curLeaf = txnPath.lookup(curRoot);
2952
- const found = this.comps.getRequestFor(curLeaf, name);
2953
- const handler = found ?? mkReq404(name);
2954
- if (this._observers.length > 0) {
2955
- const reqPath = txnPath.pinKeys(curRoot);
2956
- this._emit({
2957
- kind: "request",
2958
- name,
2959
- args,
2960
- path: reqPath,
2961
- pathKeys: reqPath.toKeys(),
2962
- targetPath: path,
2963
- handler: found?.fn ?? null,
2964
- handlerName: found?.fn?.name || name,
2965
- matched: found ? "exact" : "none",
2966
- before: curLeaf,
2967
- after: void 0,
2968
- parent,
2969
- timestamp: Date.now()
2970
- });
2971
- }
2972
- const reqCtx = new RequestContext(path, this, parent, curRoot);
2973
- const resHandlerName = opts?.onResName ?? name;
2974
- const resPath = opts?.livePath ? null : txnPath.pinKeys(curRoot);
2975
- const push = (specificName, baseName, singleArg, result, error) => {
2976
- const resArgs = specificName ? [singleArg] : [result, error];
2977
- const t = new ResponseEvent(path, this, specificName ?? baseName, resArgs, parent, resPath);
2978
- transfer(t);
2979
- this.pushTransaction(t);
2980
- };
2981
- try {
2982
- const result = await handler.fn.apply(null, [...args, reqCtx]);
2983
- push(opts?.onOkName, resHandlerName, result, result, null);
2984
- } catch (error) {
2985
- push(opts?.onErrorName, resHandlerName, error, null, error);
2986
- }
2987
- } finally {
2988
- if (release && !released) release();
2989
- }
2990
- }
2991
2945
  get hasPendingTransactions() {
2992
2946
  return this.transactions.length > 0;
2993
2947
  }
@@ -3004,6 +2958,7 @@ var Transactor = class {
3004
2958
  this._emitTransaction(transaction, curState);
3005
2959
  } else console.warn("undefined new state", { curState, transaction });
3006
2960
  } finally {
2961
+ transaction.ensureWalkAdvanced?.();
3007
2962
  transaction._completion?.ensureSelfSettled();
3008
2963
  transaction._completion?.releaseSelf();
3009
2964
  }
@@ -3012,12 +2967,6 @@ var Transactor = class {
3012
2967
  this.transact(new InputEvent(path, event, eventHandler, this, dragInfo));
3013
2968
  }
3014
2969
  };
3015
- function mkReq404(name) {
3016
- const fn = () => {
3017
- throw new Error(`Request not found: ${name}`);
3018
- };
3019
- return { fn };
3020
- }
3021
2970
  function nullHandler() {
3022
2971
  return this;
3023
2972
  }
@@ -3048,6 +2997,15 @@ var Transaction = class {
3048
2997
  }
3049
2998
  afterTransaction() {
3050
2999
  }
3000
+ // Ending a walk and forwarding belong to the two dispatch buckets; the base warns
3001
+ // rather than throwing, so a stray call from the wrong place is a message and not a
3002
+ // crash. `walk` is undefined here, which is how ctx.reply/ctx.fail tell the two apart.
3003
+ stop() {
3004
+ console.warn('ctx.stop() is only meaningful in an "intent" handler - ignored');
3005
+ }
3006
+ forward(_opts) {
3007
+ console.warn('ctx.forward() needs a "receive" or "intent" handler - ignored');
3008
+ }
3051
3009
  buildRootStack(root, comps) {
3052
3010
  return Stack.root(comps, root);
3053
3011
  }
@@ -3109,18 +3067,40 @@ var InputEvent = class extends Transaction {
3109
3067
  this._dispatchPath ??= this.path.compact();
3110
3068
  return this._dispatchPath;
3111
3069
  }
3070
+ // A DOM event is an ADDRESSED message like any other — it reaches the component that
3071
+ // owns the view and stops. It keeps its own class only because it resolves its handler
3072
+ // from the compiled view and runs synchronously, not because it is a different channel.
3112
3073
  get observeKind() {
3113
- return "input";
3074
+ return "receive";
3114
3075
  }
3115
3076
  get observeName() {
3116
3077
  return this.e?.type ?? null;
3117
3078
  }
3079
+ // A view's name is a message. `forward` is where that name can LEAVE the component:
3080
+ // it becomes an intent with the same name and the same arguments, so a view that says
3081
+ // `@on.click="saveDraft .text"` never has to change when an ancestor takes the job over.
3082
+ // (A `$method` handler has a name too, so forwarding one raises an intent under it.)
3083
+ forward(opts) {
3084
+ this._forward = opts ?? {};
3085
+ }
3086
+ afterTransaction() {
3087
+ const f = this._forward;
3088
+ if (f === void 0) return;
3089
+ this._forward = void 0;
3090
+ const { args = this._handlerArgs ?? [], ...rest } = f;
3091
+ const hv = this.handler?.handlerCall?.handlerVal ?? this.handler?.handlerVal;
3092
+ const name = hv?.name;
3093
+ if (name === void 0)
3094
+ return console.warn("ctx.forward() from a handler with no name - ignored");
3095
+ this.transactor.pushIntent(this.dispatchPath, name, args, rest, this);
3096
+ }
3118
3097
  buildRootStack(root, comps) {
3119
3098
  return Stack.root(comps, root, this);
3120
3099
  }
3121
3100
  getHandlerAndArgs(root, _instance, comps) {
3122
3101
  const stack = this.buildStack(root, comps);
3123
3102
  const [handler, args] = this.handler.getHandlerAndArgs(stack, this);
3103
+ this._handlerArgs = [...args];
3124
3104
  const path = this.dispatchPath;
3125
3105
  args.push(new EventContext(path, this.transactor, this));
3126
3106
  return [handler, args];
@@ -3205,39 +3185,190 @@ var NameArgsTransaction = class extends Transaction {
3205
3185
  return [handler, [...this.args, new EventContext(this.path, this.transactor, this)]];
3206
3186
  }
3207
3187
  };
3208
- var ResponseEvent = class extends NameArgsTransaction {
3209
- handlerProp = "response";
3210
- constructor(path, transactor, name, args, parent, txnPath = null) {
3211
- super(path, transactor, name, args, parent);
3212
- this._txnPath = txnPath;
3213
- }
3214
- getTransactionPath() {
3215
- return this._txnPath ?? super.getTransactionPath();
3216
- }
3217
- };
3218
3188
  var SendEvent = class extends NameArgsTransaction {
3219
3189
  handlerProp = "receive";
3220
- run(rootVal, comps) {
3221
- return this.opts.skipSelf ? rootVal : this.updateRootValue(rootVal, comps);
3190
+ // `receive` is the ADDRESSED bucket, and it is the only one: a view's `@on.click`,
3191
+ // a parent's ctx.send, the host's sendAtRoot and an answer to an intent all arrive
3192
+ // here, and a handler cannot tell them apart. Splitting on where a message came from
3193
+ // would let the view's name through and turn the parent's away, though the two say
3194
+ // the same thing — and neither a test nor a parent could drive the component.
3195
+ get observeKind() {
3196
+ return this._isAnswer ? "answer" : "receive";
3197
+ }
3198
+ // `forward` in a RECEIVE body starts a walk: the message that arrived becomes an
3199
+ // intent, keeping its name and payload. This is what lets a view's name leave the
3200
+ // component without the view changing.
3201
+ forward(opts) {
3202
+ this._forward = opts ?? {};
3222
3203
  }
3223
3204
  afterTransaction() {
3224
- const { path, name, args, opts, targetPath } = this;
3225
- if (opts.bubbles && path.steps.length > 0)
3226
- this.transactor.pushBubble(path.popStep(), name, args, opts, this, targetPath);
3205
+ const f = this._forward;
3206
+ if (f === void 0) return;
3207
+ this._forward = void 0;
3208
+ const { args = this.args, ...rest } = f;
3209
+ this.transactor.pushIntent(this.path, this.name, args, rest, this);
3227
3210
  }
3228
3211
  };
3229
- var BubbleEvent = class extends SendEvent {
3230
- handlerProp = "bubble";
3231
- constructor(path, transactor, name, args, parent, opts, targetPath) {
3232
- super(path, transactor, name, args, parent, opts);
3233
- this.targetPath = targetPath ?? path;
3212
+ var IntentEvent = class extends NameArgsTransaction {
3213
+ handlerProp = "intent";
3214
+ constructor(path, transactor, walk) {
3215
+ super(path, transactor, walk.name, walk.args, walk.parent, {});
3216
+ this.walk = walk;
3217
+ this.targetPath = walk.origin;
3218
+ }
3219
+ // `forward` in an INTENT body amends the hop about to be pushed — it does not push
3220
+ // one itself, because a walk advances on its own.
3221
+ forward(opts) {
3222
+ this.walk.amend(opts, this.path);
3223
+ }
3224
+ stop() {
3225
+ this.walk.finish(null, null);
3234
3226
  }
3235
- stopPropagation() {
3236
- this.opts.bubbles = false;
3227
+ afterTransaction() {
3228
+ this.ensureWalkAdvanced();
3229
+ }
3230
+ // Idempotent, because both afterTransaction and the transactor's `finally` call it —
3231
+ // whichever gets there first advances the walk exactly once.
3232
+ ensureWalkAdvanced() {
3233
+ if (this._advanced) return;
3234
+ this._advanced = true;
3235
+ this.walk.advance();
3237
3236
  }
3238
3237
  };
3239
- var InputDispatchEvent = class extends NameArgsTransaction {
3240
- handlerProp = "input";
3238
+ var PASS = /* @__PURE__ */ Symbol.for("tutuca.intent.pass");
3239
+ var DEFAULT_ROUTE = ["dyn", "lex"];
3240
+ var INTENT_DEPTH = 64;
3241
+ var IntentWalk = class {
3242
+ constructor(transactor, path, name, args, opts, parent, release) {
3243
+ this.transactor = transactor;
3244
+ this.name = name;
3245
+ this.args = args;
3246
+ this.route = opts?.route ?? DEFAULT_ROUTE;
3247
+ this.parent = parent;
3248
+ this.release = release;
3249
+ this.origin = path;
3250
+ this.answerPath = opts?.livePath ? null : path.toTransactionPath().pinKeys(transactor.state.val);
3251
+ this.legIndex = 0;
3252
+ this.dynAt = path;
3253
+ this.hops = 0;
3254
+ this.ended = false;
3255
+ }
3256
+ // Offer the intent to the next hop. Called once at dispatch, and again by every hop
3257
+ // that ran without answering.
3258
+ advance() {
3259
+ if (this.ended) return;
3260
+ if (this.hops++ >= INTENT_DEPTH) return this.exhaust("intentDepth");
3261
+ while (this.legIndex < this.route.length) {
3262
+ const leg = this.route[this.legIndex];
3263
+ if (leg === "dyn") {
3264
+ if (this.dynAt.steps.length === 0) {
3265
+ this.legIndex++;
3266
+ continue;
3267
+ }
3268
+ this.dynAt = this.dynAt.popStep();
3269
+ this.transactor.pushTransaction(new IntentEvent(this.dynAt, this.transactor, this));
3270
+ return;
3271
+ }
3272
+ if (leg === "lex") {
3273
+ this.legIndex++;
3274
+ return this._tryLex();
3275
+ }
3276
+ console.warn("unknown intent route leg", leg, '- expected "dyn" or "lex"');
3277
+ this.legIndex++;
3278
+ }
3279
+ this.exhaust("noHandler");
3280
+ }
3281
+ // The `lex` leg: the handlers registered on the scope chain of the component that
3282
+ // raised the intent, in order.
3283
+ _tryLex() {
3284
+ const root = this.transactor.state.val;
3285
+ const txnPath = this.origin.toTransactionPath();
3286
+ const leaf = txnPath.lookup(root);
3287
+ const chain = this.transactor.comps.getIntentChainFor(leaf, this.name);
3288
+ if (this.transactor._observers.length > 0) {
3289
+ const path = txnPath.pinKeys(root);
3290
+ this.transactor._emit({
3291
+ kind: "intent",
3292
+ name: this.name,
3293
+ args: this.args,
3294
+ path,
3295
+ pathKeys: path.toKeys(),
3296
+ targetPath: this.origin,
3297
+ handler: chain[0]?.fn ?? null,
3298
+ handlerName: chain[0]?.fn?.name || this.name,
3299
+ matched: chain.length > 0 ? "exact" : "none",
3300
+ before: leaf,
3301
+ after: void 0,
3302
+ parent: this.parent,
3303
+ timestamp: Date.now()
3304
+ });
3305
+ }
3306
+ if (chain.length === 0) return this.advance();
3307
+ const p = this._runLex(chain, 0);
3308
+ this.transactor._inflight.add(p);
3309
+ p.finally(() => this.transactor._inflight.delete(p));
3310
+ }
3311
+ async _runLex(chain, i) {
3312
+ if (this.ended) return;
3313
+ if (i >= chain.length) return this.advance();
3314
+ const ctx = new IntentContext(this.origin, this.transactor, this.parent);
3315
+ try {
3316
+ const res = await chain[i].fn.apply(null, [...this.args, ctx]);
3317
+ if (res === PASS) return this._runLex(chain, i + 1);
3318
+ this.answer("Ok", res);
3319
+ } catch (error) {
3320
+ this.answer("Error", error);
3321
+ }
3322
+ }
3323
+ // A hop answered. Ends the walk.
3324
+ answer(suffix, value) {
3325
+ this.finish(`${this.name}${suffix}`, [value]);
3326
+ }
3327
+ // `forward` from an intent body: keep walking, optionally with new arguments or a
3328
+ // narrowed route.
3329
+ amend(opts, from) {
3330
+ if (opts?.args !== void 0) this.args = opts.args;
3331
+ if (opts?.route !== void 0) {
3332
+ this.route = opts.route;
3333
+ this.legIndex = 0;
3334
+ this.dynAt = from ?? this.dynAt;
3335
+ }
3336
+ }
3337
+ // The route ran out. What the sender hears depends only on what it DECLARES, which is
3338
+ // how "a sender expects an answer if and only if it declares an answer arm" becomes
3339
+ // code — nobody writes it down twice, and there is no schema to keep in step.
3340
+ exhaust(reason) {
3341
+ if (this.ended) return;
3342
+ const { name, args } = this;
3343
+ const comp = this.transactor.comps.getCompFor(
3344
+ this.origin.toTransactionPath().lookup(this.transactor.state.val)
3345
+ );
3346
+ const declares = (n) => comp?.receive?.[n] !== void 0;
3347
+ if (declares(`${name}Unhandled`)) {
3348
+ this.finish(`${name}Unhandled`, args);
3349
+ } else if (declares(`${name}Error`)) {
3350
+ this.finish(`${name}Error`, [reason]);
3351
+ } else {
3352
+ if (declares(`${name}Ok`))
3353
+ console.warn(
3354
+ `intent "${name}" was not answered (${reason}) and this component declares only "${name}Ok" - add "${name}Unhandled" or "${name}Error" to handle it`
3355
+ );
3356
+ this.finish(null, null);
3357
+ }
3358
+ }
3359
+ // End the walk. `name === null` means no answer goes out, which is what `stop` and a
3360
+ // fire-and-forget notification both are — the tracked unit is released here instead
3361
+ // of following an answer's subtree.
3362
+ finish(name, args) {
3363
+ if (this.ended) return;
3364
+ this.ended = true;
3365
+ if (name === null) return this.release?.();
3366
+ const path = this.answerPath ?? this.origin;
3367
+ const t = new SendEvent(path, this.transactor, name, args, this.parent, {});
3368
+ t._isAnswer = true;
3369
+ this.transactor.pushTransaction(t);
3370
+ if (this.release) t.completion.whenSubtreeSettled().then(this.release);
3371
+ }
3241
3372
  };
3242
3373
  var Completion = class {
3243
3374
  constructor() {
@@ -3318,23 +3449,21 @@ var Dispatcher = class {
3318
3449
  get at() {
3319
3450
  return new PathChanges(this);
3320
3451
  }
3452
+ // A message is ADDRESSED: it names one component and stops there.
3321
3453
  send(name, args, opts) {
3322
3454
  return this.sendAtPath(this.path, name, args, opts);
3323
3455
  }
3324
- bubble(name, args, opts) {
3325
- return this.send(name, args, { skipSelf: true, bubbles: true, ...opts });
3326
- }
3327
3456
  sendAtPath(path, name, args, opts) {
3328
3457
  return this.transactor.pushSend(path, name, args, opts, this.parent);
3329
3458
  }
3330
- request(name, args, opts) {
3331
- return this.requestAtPath(this.path, name, args, opts);
3332
- }
3333
- requestAtPath(path, name, args, opts) {
3334
- return this.transactor.pushRequest(path, name, args, opts, this.parent);
3459
+ // An intent is ROUTED: it names a job and walks until something answers. The verb
3460
+ // does not decide which scope answers — `opts.route` does, written here at the call
3461
+ // site, which is where the decision actually is.
3462
+ intent(name, args, opts) {
3463
+ return this.intentAtPath(this.path, name, args, opts);
3335
3464
  }
3336
- inputAtPath(path, name, args, opts) {
3337
- return this.transactor.pushInput(path, name, args, opts, this.parent);
3465
+ intentAtPath(path, name, args, opts) {
3466
+ return this.transactor.pushIntent(path, name, args, opts, this.parent);
3338
3467
  }
3339
3468
  lookupTypeFor(name, inst) {
3340
3469
  return this.transactor.comps.getCompFor(inst).scope.lookupComponent(name);
@@ -3347,12 +3476,30 @@ var EventContext = class extends Dispatcher {
3347
3476
  get targetPath() {
3348
3477
  return this.parent.targetPath;
3349
3478
  }
3350
- stopPropagation() {
3351
- return this.parent.stopPropagation();
3479
+ // Answer the intent being handled, with a result or with an error. Either ENDS the
3480
+ // walk. Legal only in an `intent` handler: a message has no sender waiting on it.
3481
+ reply(value) {
3482
+ return this.parent.walk === void 0 ? warnNotIntent("reply") : this.parent.walk.answer("Ok", value);
3483
+ }
3484
+ fail(error) {
3485
+ return this.parent.walk === void 0 ? warnNotIntent("fail") : this.parent.walk.answer("Error", error);
3486
+ }
3487
+ // End the walk answering nothing — "served, and no answer".
3488
+ stop() {
3489
+ return this.parent.stop();
3490
+ }
3491
+ // From an `intent` body: hand the intent to the next hop, optionally amending it.
3492
+ // From a `receive` body: turn the message that arrived into an intent. One word from
3493
+ // both ends of a walk; which one you get depends on which bucket you are in.
3494
+ forward(opts) {
3495
+ return this.parent.forward(opts);
3352
3496
  }
3353
3497
  };
3354
- var RequestContext = class extends Dispatcher {
3498
+ var IntentContext = class extends Dispatcher {
3355
3499
  };
3500
+ function warnNotIntent(verb) {
3501
+ console.warn(`ctx.${verb}() is only meaningful in an "intent" handler - ignored`);
3502
+ }
3356
3503
  var PathChanges = class extends PathBuilder {
3357
3504
  constructor(dispatcher) {
3358
3505
  super();
@@ -3361,8 +3508,8 @@ var PathChanges = class extends PathBuilder {
3361
3508
  send(name, args, opts) {
3362
3509
  return this.dispatcher.sendAtPath(this.buildPath(), name, args, opts);
3363
3510
  }
3364
- bubble(name, args, opts) {
3365
- return this.send(name, args, { skipSelf: true, bubbles: true, ...opts });
3511
+ intent(name, args, opts) {
3512
+ return this.dispatcher.intentAtPath(this.buildPath(), name, args, opts);
3366
3513
  }
3367
3514
  buildPath() {
3368
3515
  return this.dispatcher.path.concat(this.pathChanges);
@@ -3755,7 +3902,7 @@ import {
3755
3902
  } from "immutable";
3756
3903
 
3757
3904
  // src/on.js
3758
- var OP_KINDS = ["send", "bubble", "request", "input"];
3905
+ var OP_KINDS = ["send", "intent"];
3759
3906
  function phaseOps(phase) {
3760
3907
  const ops = [];
3761
3908
  for (const type of OP_KINDS) for (const a of phase[type] ?? []) ops.push({ type, ...a });
@@ -3765,11 +3912,6 @@ function phaseOps(phase) {
3765
3912
  function resolveArgs(args, self) {
3766
3913
  return typeof args === "function" ? args(self) ?? [] : args ?? [];
3767
3914
  }
3768
- function phaseHasBubble(phase) {
3769
- if (!phase) return false;
3770
- if (phase.bubble?.length) return true;
3771
- return (phase.do ?? []).some((op) => op.type === "bubble");
3772
- }
3773
3915
  function dispatchPhase(dispatcher, targetPath, phase, self) {
3774
3916
  if (!phase) return;
3775
3917
  for (const op of phaseOps(phase)) {
@@ -3778,18 +3920,8 @@ function dispatchPhase(dispatcher, targetPath, phase, self) {
3778
3920
  case "send":
3779
3921
  dispatcher.sendAtPath(targetPath, op.name, args, op.opts);
3780
3922
  break;
3781
- case "bubble":
3782
- dispatcher.sendAtPath(targetPath, op.name, args, {
3783
- skipSelf: true,
3784
- bubbles: true,
3785
- ...op.opts
3786
- });
3787
- break;
3788
- case "request":
3789
- dispatcher.requestAtPath(targetPath, op.name, args, op.opts);
3790
- break;
3791
- case "input":
3792
- dispatcher.inputAtPath(targetPath, op.name, args, op.opts);
3923
+ case "intent":
3924
+ dispatcher.intentAtPath(targetPath, op.name, args, op.opts);
3793
3925
  break;
3794
3926
  }
3795
3927
  }
@@ -4220,6 +4352,7 @@ export {
4220
4352
  OrderedMap3 as OMap,
4221
4353
  OrderedMap2 as OrderedMap,
4222
4354
  OrderedSet,
4355
+ PASS,
4223
4356
  PairSorting,
4224
4357
  ParseContext,
4225
4358
  Range,
@@ -4268,7 +4401,6 @@ export {
4268
4401
  mergeDeep,
4269
4402
  mergeDeepWith,
4270
4403
  mergeWith,
4271
- phaseHasBubble,
4272
4404
  phaseOps,
4273
4405
  remove,
4274
4406
  removeIn,