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.
- package/README.md +2 -2
- package/dist/tutuca-cli.js +475 -247
- package/dist/tutuca-components.js +13 -19
- package/dist/tutuca-dev.ext.js +410 -202
- package/dist/tutuca-dev.js +410 -202
- package/dist/tutuca-dev.min.js +4 -4
- package/dist/tutuca-extra.ext.js +281 -149
- package/dist/tutuca-extra.js +281 -149
- package/dist/tutuca-extra.min.js +3 -3
- package/dist/tutuca-storybook.js +83 -76
- package/dist/tutuca.ext.js +281 -149
- package/dist/tutuca.js +281 -149
- package/dist/tutuca.min.js +3 -3
- package/package.json +1 -1
- package/skill/tutuca/SKILL.md +4 -4
- package/skill/tutuca/advanced.md +1 -1
- package/skill/tutuca/cli.md +2 -2
- package/skill/tutuca/component-design.md +33 -29
- package/skill/tutuca/core.md +57 -42
- package/skill/tutuca/messages-and-intents.md +398 -0
- package/skill/tutuca/patterns/README.md +1 -1
- package/skill/tutuca/patterns/add-a-story.md +2 -2
- package/skill/tutuca/patterns/coordinate-components.md +39 -18
- package/skill/tutuca/patterns/file-input.md +2 -2
- package/skill/tutuca/patterns/handle-events.md +2 -2
- package/skill/tutuca/semantics.md +41 -35
- package/skill/tutuca/storybook.md +34 -27
- package/skill/tutuca/testing.md +49 -47
- package/skill/tutuca-source/tutuca.ext.js +281 -149
- package/skill/tutuca/request-response.md +0 -341
package/dist/tutuca-extra.ext.js
CHANGED
|
@@ -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
|
|
619
|
-
return _parseHandler(s, px, "
|
|
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 =
|
|
1162
|
+
const r = parseReceiveHandler(s, px);
|
|
1163
1163
|
return r === null ? null : new _EventHandler(r.handlerVal, r.args);
|
|
1164
1164
|
}
|
|
1165
1165
|
};
|
|
1166
|
-
var
|
|
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
|
-
|
|
2538
|
-
|
|
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.
|
|
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
|
-
|
|
2584
|
-
|
|
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
|
-
|
|
2587
|
-
|
|
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
|
|
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.
|
|
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
|
|
2860
|
-
//
|
|
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
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
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
|
|
2927
|
-
|
|
2928
|
-
|
|
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 "
|
|
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
|
-
|
|
3221
|
-
|
|
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
|
|
3225
|
-
if (
|
|
3226
|
-
|
|
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
|
|
3230
|
-
handlerProp = "
|
|
3231
|
-
constructor(path, transactor,
|
|
3232
|
-
super(path, transactor, name, args, parent,
|
|
3233
|
-
this.
|
|
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
|
-
|
|
3236
|
-
this.
|
|
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
|
|
3240
|
-
|
|
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
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
return this.
|
|
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
|
-
|
|
3337
|
-
return this.transactor.
|
|
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
|
-
|
|
3351
|
-
|
|
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
|
|
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
|
-
|
|
3365
|
-
return this.
|
|
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", "
|
|
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 "
|
|
3782
|
-
dispatcher.
|
|
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,
|