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-dev.ext.js
CHANGED
|
@@ -787,8 +787,8 @@ function parseProvide(s, px) {
|
|
|
787
787
|
function parseMacroAttr(s, px) {
|
|
788
788
|
return _parseSingle(s, px, G_ALL);
|
|
789
789
|
}
|
|
790
|
-
function
|
|
791
|
-
return _parseHandler(s, px, "
|
|
790
|
+
function parseReceiveHandler(s, px) {
|
|
791
|
+
return _parseHandler(s, px, "receive", true, true);
|
|
792
792
|
}
|
|
793
793
|
function parseAlterHandler(s, px) {
|
|
794
794
|
const r = _parseHandler(s, px, "alter", false, false);
|
|
@@ -1331,11 +1331,11 @@ var EventHandler = class _EventHandler {
|
|
|
1331
1331
|
return [this.handlerVal.evalAsHandler(stack), argValues];
|
|
1332
1332
|
}
|
|
1333
1333
|
static parse(s, px) {
|
|
1334
|
-
const r =
|
|
1334
|
+
const r = parseReceiveHandler(s, px);
|
|
1335
1335
|
return r === null ? null : new _EventHandler(r.handlerVal, r.args);
|
|
1336
1336
|
}
|
|
1337
1337
|
};
|
|
1338
|
-
var
|
|
1338
|
+
var IntentHandler = class {
|
|
1339
1339
|
constructor(name, fn) {
|
|
1340
1340
|
this.name = name;
|
|
1341
1341
|
this.fn = fn;
|
|
@@ -5031,7 +5031,7 @@ var KNOWN_COMPONENT_SPEC_KEYS = new Set(
|
|
|
5031
5031
|
)
|
|
5032
5032
|
);
|
|
5033
5033
|
var EMPTY_SET = /* @__PURE__ */ new Set();
|
|
5034
|
-
var FRAMEWORK_WELL_KNOWN_EXTRAS = /* @__PURE__ */ new Set(["
|
|
5034
|
+
var FRAMEWORK_WELL_KNOWN_EXTRAS = /* @__PURE__ */ new Set(["intentOverridesField"]);
|
|
5035
5035
|
var KNOWN_DIRECTIVE_NAMES = /* @__PURE__ */ new Set([
|
|
5036
5036
|
"dangerouslysetinnerhtml",
|
|
5037
5037
|
"slot",
|
|
@@ -5056,11 +5056,13 @@ var LOOKUP_TARGET_MALFORMED = "LOOKUP_TARGET_MALFORMED";
|
|
|
5056
5056
|
var RENDER_IT_OUTSIDE_OF_LOOP = "RENDER_IT_OUTSIDE_OF_LOOP";
|
|
5057
5057
|
var UNKNOWN_EVENT_MODIFIER = "UNKNOWN_EVENT_MODIFIER";
|
|
5058
5058
|
var UNKNOWN_HANDLER_ARG_NAME = "UNKNOWN_HANDLER_ARG_NAME";
|
|
5059
|
-
var
|
|
5060
|
-
var
|
|
5061
|
-
var
|
|
5062
|
-
var
|
|
5063
|
-
var
|
|
5059
|
+
var RECEIVE_HANDLER_NOT_IMPLEMENTED = "RECEIVE_HANDLER_NOT_IMPLEMENTED";
|
|
5060
|
+
var RETIRED_HANDLER_BUCKET = "RETIRED_HANDLER_BUCKET";
|
|
5061
|
+
var RETIRED_CTX_VERB = "RETIRED_CTX_VERB";
|
|
5062
|
+
var HANDLER_NAME_COLLISION = "HANDLER_NAME_COLLISION";
|
|
5063
|
+
var RECEIVE_HANDLER_METHOD_NOT_IMPLEMENTED = "RECEIVE_HANDLER_METHOD_NOT_IMPLEMENTED";
|
|
5064
|
+
var RECEIVE_HANDLER_FOR_METHOD = "RECEIVE_HANDLER_FOR_METHOD";
|
|
5065
|
+
var METHOD_FOR_RECEIVE_HANDLER = "METHOD_FOR_RECEIVE_HANDLER";
|
|
5064
5066
|
var FIELD_VAL_NOT_DEFINED = "FIELD_VAL_NOT_DEFINED";
|
|
5065
5067
|
var FIELD_VAL_IS_METHOD = "FIELD_VAL_IS_METHOD";
|
|
5066
5068
|
var METHOD_VAL_NOT_DEFINED = "METHOD_VAL_NOT_DEFINED";
|
|
@@ -5178,6 +5180,9 @@ function checkComponent(Comp, lx = new LintContext(), { wellKnownExtras = EMPTY_
|
|
|
5178
5180
|
checkProvidesAreAddressable(lx, Comp);
|
|
5179
5181
|
checkLookupShapes(lx, Comp);
|
|
5180
5182
|
checkHandlersNotAsync(lx, Comp);
|
|
5183
|
+
checkRetiredBuckets(lx, Comp);
|
|
5184
|
+
checkRetiredCtxVerbs(lx, Comp);
|
|
5185
|
+
checkHandlerNameCollisions(lx, Comp);
|
|
5181
5186
|
checkScopedStyleTopLevel(lx, Comp);
|
|
5182
5187
|
const referencedAlters = /* @__PURE__ */ new Set();
|
|
5183
5188
|
const referencedInputs = /* @__PURE__ */ new Set();
|
|
@@ -5191,7 +5196,6 @@ function checkComponent(Comp, lx = new LintContext(), { wellKnownExtras = EMPTY_
|
|
|
5191
5196
|
);
|
|
5192
5197
|
}
|
|
5193
5198
|
checkUnreferencedAlterHandlers(lx, Comp, referencedAlters);
|
|
5194
|
-
checkUnreferencedInputHandlers(lx, Comp, referencedInputs);
|
|
5195
5199
|
checkUnreferencedDynamics(lx, Comp, referencedDynamics);
|
|
5196
5200
|
return lx;
|
|
5197
5201
|
});
|
|
@@ -5461,7 +5465,7 @@ function mkAttrValEnv(Comp, referencedAlters, referencedDynamics) {
|
|
|
5461
5465
|
return { fields, proto, scope, alter, referencedAlters, dynamicMap, referencedDynamics };
|
|
5462
5466
|
}
|
|
5463
5467
|
function checkEventHandlersHaveImpls(lx, Comp, referencedInputs) {
|
|
5464
|
-
const { input, views, Class } = Comp;
|
|
5468
|
+
const { receive: input, views, Class } = Comp;
|
|
5465
5469
|
const { prototype: proto } = Class;
|
|
5466
5470
|
for (const viewName in views) {
|
|
5467
5471
|
lx.push({ viewName }, () => {
|
|
@@ -5478,13 +5482,13 @@ function checkEventHandlersHaveImpls(lx, Comp, referencedInputs) {
|
|
|
5478
5482
|
if (input[name] === void 0) {
|
|
5479
5483
|
const isMethodFix = protoHasMethod(proto, name);
|
|
5480
5484
|
lx.error(
|
|
5481
|
-
|
|
5485
|
+
RECEIVE_HANDLER_NOT_IMPLEMENTED,
|
|
5482
5486
|
{ name, handler, event, eventName, originAttr },
|
|
5483
5487
|
isMethodFix ? { kind: "add-prefix", from: name, to: `$${name}` } : replaceNameSuggestion(name, Object.keys(input))
|
|
5484
5488
|
);
|
|
5485
5489
|
if (isMethodFix) {
|
|
5486
5490
|
lx.hint(
|
|
5487
|
-
|
|
5491
|
+
METHOD_FOR_RECEIVE_HANDLER,
|
|
5488
5492
|
{ name, handler, event, eventName, originAttr },
|
|
5489
5493
|
{ kind: "add-prefix", from: name, to: `$${name}` }
|
|
5490
5494
|
);
|
|
@@ -5496,13 +5500,13 @@ function checkEventHandlersHaveImpls(lx, Comp, referencedInputs) {
|
|
|
5496
5500
|
if (!protoHasMethod(proto, name)) {
|
|
5497
5501
|
const isInputFix = input[name] !== void 0;
|
|
5498
5502
|
lx.error(
|
|
5499
|
-
|
|
5503
|
+
RECEIVE_HANDLER_METHOD_NOT_IMPLEMENTED,
|
|
5500
5504
|
{ name, handler, event, eventName, originAttr },
|
|
5501
5505
|
isInputFix ? { kind: "drop-prefix", from: `$${name}`, to: name } : replaceNameSuggestion(name, collectProtoMethodNames(proto))
|
|
5502
5506
|
);
|
|
5503
5507
|
if (isInputFix) {
|
|
5504
5508
|
lx.hint(
|
|
5505
|
-
|
|
5509
|
+
RECEIVE_HANDLER_FOR_METHOD,
|
|
5506
5510
|
{ name, handler, event, eventName, originAttr },
|
|
5507
5511
|
{ kind: "drop-prefix", from: `$${name}`, to: name }
|
|
5508
5512
|
);
|
|
@@ -5751,12 +5755,83 @@ function checkConsistentAttrs(lx, Comp, referencedAlters, referencedDynamics) {
|
|
|
5751
5755
|
});
|
|
5752
5756
|
}
|
|
5753
5757
|
}
|
|
5758
|
+
var isPlainObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
5759
|
+
var RETIRED_BUCKETS = {
|
|
5760
|
+
input: {
|
|
5761
|
+
to: "receive",
|
|
5762
|
+
how: "rename the bucket to `receive` - a view's own @on.* name and a parent's ctx.send are the same message now, and nothing can tell them apart"
|
|
5763
|
+
},
|
|
5764
|
+
bubble: {
|
|
5765
|
+
to: "intent",
|
|
5766
|
+
how: "rename the bucket to `intent` - and note the walk changed: a handler that calls ctx.reply() ENDS it, where ctx.stopPropagation() used to be the only way to stop"
|
|
5767
|
+
},
|
|
5768
|
+
response: {
|
|
5769
|
+
to: "receive",
|
|
5770
|
+
how: "split each arm into `receive` arms named after the intent: <name>Ok takes the result alone, <name>Error takes the error alone, and the optional <name>Unhandled means nothing claimed it"
|
|
5771
|
+
}
|
|
5772
|
+
};
|
|
5773
|
+
function checkRetiredBuckets(lx, Comp) {
|
|
5774
|
+
const extra = Comp.extra;
|
|
5775
|
+
if (!extra) return;
|
|
5776
|
+
for (const key of Object.keys(extra)) {
|
|
5777
|
+
if (!Object.hasOwn(RETIRED_BUCKETS, key)) continue;
|
|
5778
|
+
const retired = RETIRED_BUCKETS[key];
|
|
5779
|
+
const names = isPlainObj(extra[key]) ? Object.keys(extra[key]) : [];
|
|
5780
|
+
lx.error(RETIRED_HANDLER_BUCKET, { key, to: retired.to, how: retired.how, names });
|
|
5781
|
+
}
|
|
5782
|
+
}
|
|
5783
|
+
var RETIRED_CTX_VERBS = {
|
|
5784
|
+
bubble: 'ctx.intent(name, args, { route: ["dyn"] }) - the route says it walks the ancestors, where the verb used to',
|
|
5785
|
+
request: 'ctx.intent(name, args, { route: ["lex"] }) - and the outcome comes back as a `receive` arm named <name>Ok / <name>Error / <name>Unhandled, so onOkName and onErrorName are gone',
|
|
5786
|
+
stopPropagation: "ctx.stop()",
|
|
5787
|
+
inputAtPath: "ctx.sendAtPath(path, name, args)",
|
|
5788
|
+
requestAtPath: 'ctx.intentAtPath(path, name, args, { route: ["lex"] })'
|
|
5789
|
+
};
|
|
5790
|
+
var RETIRED_CTX_RE = new RegExp(
|
|
5791
|
+
`\\bctx\\.(${Object.keys(RETIRED_CTX_VERBS).join("|")})\\s*\\(`,
|
|
5792
|
+
"g"
|
|
5793
|
+
);
|
|
5794
|
+
function checkRetiredCtxVerbs(lx, Comp) {
|
|
5795
|
+
for (const bucket of ["receive", "intent", "alter", "methods", "statics"]) {
|
|
5796
|
+
const block = bucket === "methods" || bucket === "statics" ? Comp.spec?.[bucket] : Comp[bucket];
|
|
5797
|
+
if (!isPlainObj(block)) continue;
|
|
5798
|
+
for (const name in block) {
|
|
5799
|
+
const fn = block[name];
|
|
5800
|
+
if (typeof fn !== "function") continue;
|
|
5801
|
+
const src = String(fn);
|
|
5802
|
+
for (const m of src.matchAll(RETIRED_CTX_RE)) {
|
|
5803
|
+
const verb = m[1];
|
|
5804
|
+
lx.error(RETIRED_CTX_VERB, { verb, to: RETIRED_CTX_VERBS[verb], bucket, name });
|
|
5805
|
+
}
|
|
5806
|
+
}
|
|
5807
|
+
}
|
|
5808
|
+
}
|
|
5809
|
+
function checkHandlerNameCollisions(lx, Comp) {
|
|
5810
|
+
const receive = Comp.receive ?? {};
|
|
5811
|
+
const intent = Comp.intent ?? {};
|
|
5812
|
+
for (const name in intent)
|
|
5813
|
+
if (receive[name] !== void 0)
|
|
5814
|
+
lx.error(HANDLER_NAME_COLLISION, {
|
|
5815
|
+
name,
|
|
5816
|
+
problem: `"${name}" is declared in both \`receive\` and \`intent\``,
|
|
5817
|
+
fix: "one is addressed and one is routed; give them different names so a reader can tell which a call site meant"
|
|
5818
|
+
});
|
|
5819
|
+
for (const name in receive)
|
|
5820
|
+
for (const suffix of ["Ok", "Error", "Unhandled"])
|
|
5821
|
+
if (receive[name + suffix] !== void 0 && intent[name] === void 0)
|
|
5822
|
+
lx.warn(HANDLER_NAME_COLLISION, {
|
|
5823
|
+
name,
|
|
5824
|
+
problem: `\`receive\` declares both "${name}" and its answer name "${name + suffix}"`,
|
|
5825
|
+
fix: `an intent named "${name}" would dispatch its answer to "${name + suffix}" - rename one unless that is what you meant`
|
|
5826
|
+
});
|
|
5827
|
+
}
|
|
5754
5828
|
function checkUnknownSpecKeys(lx, Comp, wellKnownExtras) {
|
|
5755
5829
|
const extra = Comp.extra;
|
|
5756
5830
|
if (!extra) return;
|
|
5757
5831
|
let candidates = null;
|
|
5758
5832
|
for (const key of Object.keys(extra)) {
|
|
5759
5833
|
if (FRAMEWORK_WELL_KNOWN_EXTRAS.has(key) || wellKnownExtras.has(key)) continue;
|
|
5834
|
+
if (Object.hasOwn(RETIRED_BUCKETS, key)) continue;
|
|
5760
5835
|
candidates ??= [
|
|
5761
5836
|
...KNOWN_COMPONENT_SPEC_KEYS,
|
|
5762
5837
|
...FRAMEWORK_WELL_KNOWN_EXTRAS,
|
|
@@ -5804,15 +5879,8 @@ function checkUnreferencedAlterHandlers(lx, Comp, referencedAlters) {
|
|
|
5804
5879
|
}
|
|
5805
5880
|
}
|
|
5806
5881
|
}
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
if (!referencedInputs.has(name)) {
|
|
5810
|
-
lx.hint(INPUT_HANDLER_NOT_REFERENCED, { name });
|
|
5811
|
-
}
|
|
5812
|
-
}
|
|
5813
|
-
}
|
|
5814
|
-
var HANDLER_CHANNELS = ["input", "receive", "bubble", "response", "alter"];
|
|
5815
|
-
var ASYNC_HANDLER_HELP = "Move the async work into a request handler and trigger it with ctx.request('name', args), then handle the result in a synchronous response handler. To coordinate other components, keep the handler synchronous and use ctx.send to deliver a message or ctx.bubble to raise an event.";
|
|
5882
|
+
var HANDLER_CHANNELS = ["receive", "intent", "alter"];
|
|
5883
|
+
var ASYNC_HANDLER_HELP = `Move the async work into a scope-registered intent handler and trigger it with ctx.intent("name", args, { route: ["lex"] }), then read the outcome in the synchronous \`receive\` arms named after it: <name>Ok, <name>Error and <name>Unhandled. To coordinate other components, keep the handler synchronous and use ctx.send to address one, or ctx.intent to let a route find one.`;
|
|
5816
5884
|
function checkHandlersNotAsync(lx, Comp) {
|
|
5817
5885
|
for (const channel of HANDLER_CHANNELS) {
|
|
5818
5886
|
const block = Comp[channel];
|
|
@@ -6008,8 +6076,11 @@ var Components = class {
|
|
|
6008
6076
|
getHandlerFor(v, name, key) {
|
|
6009
6077
|
return this.getCompFor(v)?.[key][name] ?? null;
|
|
6010
6078
|
}
|
|
6011
|
-
|
|
6012
|
-
|
|
6079
|
+
// The `lex` leg of an intent's route: the handlers registered on the scope chain of
|
|
6080
|
+
// the component that raised it, innermost first. A list, not a first match — see
|
|
6081
|
+
// ComponentStack.lookupIntentChain.
|
|
6082
|
+
getIntentChainFor(v, name) {
|
|
6083
|
+
return this.getCompFor(v)?.scope.lookupIntentChain(name) ?? [];
|
|
6013
6084
|
}
|
|
6014
6085
|
compileStyles() {
|
|
6015
6086
|
const styles = [];
|
|
@@ -6022,7 +6093,7 @@ var ComponentStack = class _ComponentStack {
|
|
|
6022
6093
|
this.comps = comps;
|
|
6023
6094
|
this.parent = parent;
|
|
6024
6095
|
this.byName = {};
|
|
6025
|
-
this.
|
|
6096
|
+
this.intentsByName = {};
|
|
6026
6097
|
this.macros = {};
|
|
6027
6098
|
}
|
|
6028
6099
|
enter() {
|
|
@@ -6054,11 +6125,21 @@ var ComponentStack = class _ComponentStack {
|
|
|
6054
6125
|
getCompFor(v) {
|
|
6055
6126
|
return this.comps.getCompFor(v);
|
|
6056
6127
|
}
|
|
6057
|
-
|
|
6058
|
-
|
|
6128
|
+
// Handlers that answer an intent's `lex` leg. A name maps to a LIST, because the leg
|
|
6129
|
+
// walks: a handler that returns PASS declines and hands the intent to the next one.
|
|
6130
|
+
// A bare function is accepted as a one-element list.
|
|
6131
|
+
registerIntentHandlers(handlers) {
|
|
6132
|
+
for (const name in handlers) {
|
|
6133
|
+
const fns = Array.isArray(handlers[name]) ? handlers[name] : [handlers[name]];
|
|
6134
|
+
this.intentsByName[name] = fns.map((fn) => new IntentHandler(name, fn));
|
|
6135
|
+
}
|
|
6059
6136
|
}
|
|
6060
|
-
|
|
6061
|
-
|
|
6137
|
+
// Innermost scope first, then outward. Concatenated rather than first-match, so a
|
|
6138
|
+
// declining handler can hand the intent to one further up.
|
|
6139
|
+
lookupIntentChain(name) {
|
|
6140
|
+
const here = this.intentsByName[name] ?? [];
|
|
6141
|
+
const up = this.parent?.lookupIntentChain(name) ?? [];
|
|
6142
|
+
return up.length === 0 ? here : here.concat(up);
|
|
6062
6143
|
}
|
|
6063
6144
|
lookupComponent(name) {
|
|
6064
6145
|
return this.byName[name] ?? this.parent?.lookupComponent(name) ?? null;
|
|
@@ -6089,7 +6170,7 @@ var LookupInfo = class {
|
|
|
6089
6170
|
}
|
|
6090
6171
|
};
|
|
6091
6172
|
var isString = (v) => typeof v === "string";
|
|
6092
|
-
var _rawSpecKeys = "name view style commonStyle globalStyle
|
|
6173
|
+
var _rawSpecKeys = "name view style commonStyle globalStyle receive intent alter views provide lookup fields methods statics";
|
|
6093
6174
|
var KNOWN_SPEC_KEYS = new Set(_rawSpecKeys.split(" "));
|
|
6094
6175
|
var _compId = 0;
|
|
6095
6176
|
var Component = class {
|
|
@@ -6100,10 +6181,8 @@ var Component = class {
|
|
|
6100
6181
|
this.views = { main: new View("main", o.view, o.style) };
|
|
6101
6182
|
this.commonStyle = o.commonStyle ?? "";
|
|
6102
6183
|
this.globalStyle = o.globalStyle ?? "";
|
|
6103
|
-
this.input = o.input ?? {};
|
|
6104
6184
|
this.receive = o.receive ?? {};
|
|
6105
|
-
this.
|
|
6106
|
-
this.response = o.response ?? {};
|
|
6185
|
+
this.intent = o.intent ?? {};
|
|
6107
6186
|
this.alter = o.alter ?? {};
|
|
6108
6187
|
for (const name in o.views ?? {}) {
|
|
6109
6188
|
const v = o.views[name];
|
|
@@ -6165,7 +6244,7 @@ var Component = class {
|
|
|
6165
6244
|
};
|
|
6166
6245
|
|
|
6167
6246
|
// src/on.js
|
|
6168
|
-
var OP_KINDS = ["send", "
|
|
6247
|
+
var OP_KINDS = ["send", "intent"];
|
|
6169
6248
|
function phaseOps(phase) {
|
|
6170
6249
|
const ops = [];
|
|
6171
6250
|
for (const type of OP_KINDS) for (const a of phase[type] ?? []) ops.push({ type, ...a });
|
|
@@ -6175,11 +6254,6 @@ function phaseOps(phase) {
|
|
|
6175
6254
|
function resolveArgs(args, self) {
|
|
6176
6255
|
return typeof args === "function" ? args(self) ?? [] : args ?? [];
|
|
6177
6256
|
}
|
|
6178
|
-
function phaseHasBubble(phase) {
|
|
6179
|
-
if (!phase) return false;
|
|
6180
|
-
if (phase.bubble?.length) return true;
|
|
6181
|
-
return (phase.do ?? []).some((op) => op.type === "bubble");
|
|
6182
|
-
}
|
|
6183
6257
|
function dispatchPhase(dispatcher, targetPath, phase, self) {
|
|
6184
6258
|
if (!phase) return;
|
|
6185
6259
|
for (const op of phaseOps(phase)) {
|
|
@@ -6188,18 +6262,8 @@ function dispatchPhase(dispatcher, targetPath, phase, self) {
|
|
|
6188
6262
|
case "send":
|
|
6189
6263
|
dispatcher.sendAtPath(targetPath, op.name, args, op.opts);
|
|
6190
6264
|
break;
|
|
6191
|
-
case "
|
|
6192
|
-
dispatcher.
|
|
6193
|
-
skipSelf: true,
|
|
6194
|
-
bubbles: true,
|
|
6195
|
-
...op.opts
|
|
6196
|
-
});
|
|
6197
|
-
break;
|
|
6198
|
-
case "request":
|
|
6199
|
-
dispatcher.requestAtPath(targetPath, op.name, args, op.opts);
|
|
6200
|
-
break;
|
|
6201
|
-
case "input":
|
|
6202
|
-
dispatcher.inputAtPath(targetPath, op.name, args, op.opts);
|
|
6265
|
+
case "intent":
|
|
6266
|
+
dispatcher.intentAtPath(targetPath, op.name, args, op.opts);
|
|
6203
6267
|
break;
|
|
6204
6268
|
}
|
|
6205
6269
|
}
|
|
@@ -6371,8 +6435,11 @@ var Transactor = class {
|
|
|
6371
6435
|
this.transactions.push(t);
|
|
6372
6436
|
this.onTransactionPushed(t);
|
|
6373
6437
|
}
|
|
6374
|
-
// Subscribe to a normalized record for every handler invocation
|
|
6375
|
-
//
|
|
6438
|
+
// Subscribe to a normalized record for every handler invocation. `kind` is the
|
|
6439
|
+
// handler bucket (`receive` / `intent`), plus `answer` — an observation-only kind for
|
|
6440
|
+
// a message the runtime wrote to answer an intent. The BUCKET does not know about
|
|
6441
|
+
// that distinction, and must not: a handler cannot tell an answer from a message its
|
|
6442
|
+
// parent sent. Returns an unsubscribe function. Records
|
|
6376
6443
|
// carry: kind, name, args, path, pathKeys, targetPath, handler, handlerName,
|
|
6377
6444
|
// matched, before, after, parent, timestamp. Purely observational.
|
|
6378
6445
|
observe(cb) {
|
|
@@ -6425,23 +6492,14 @@ var Transactor = class {
|
|
|
6425
6492
|
this.pushTransaction(t);
|
|
6426
6493
|
return this._link(t, parent);
|
|
6427
6494
|
}
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
}
|
|
6433
|
-
pushBubble(path, name, args = [], opts = {}, parent = null, targetPath = null) {
|
|
6434
|
-
const newOpts = opts.skipSelf ? { ...opts, skipSelf: false } : opts;
|
|
6435
|
-
const t = new BubbleEvent(path, this, name, args, parent, newOpts, targetPath);
|
|
6436
|
-
this.pushTransaction(t);
|
|
6437
|
-
return this._link(t, parent);
|
|
6438
|
-
}
|
|
6439
|
-
pushRequest(path, name, args = [], opts = {}, parent = null) {
|
|
6495
|
+
// Raise an intent: a job the sender did not address, walked along a route until
|
|
6496
|
+
// something answers. `opts.route` is a list of legs (see DEFAULT_ROUTE) and
|
|
6497
|
+
// `opts.livePath` opts out of pinning the answer's destination keys.
|
|
6498
|
+
pushIntent(path, name, args = [], opts = {}, parent = null) {
|
|
6440
6499
|
const release = parent ? parent.completion.track() : null;
|
|
6441
|
-
const
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
return p;
|
|
6500
|
+
const walk = new IntentWalk(this, path, name, args, opts, parent, release);
|
|
6501
|
+
walk.advance();
|
|
6502
|
+
return walk;
|
|
6445
6503
|
}
|
|
6446
6504
|
// Drain queued transactions and await in-flight requests until quiescent. Each
|
|
6447
6505
|
// awaited request enqueues a ResponseEvent, which may dispatch more work, so we
|
|
@@ -6452,57 +6510,6 @@ var Transactor = class {
|
|
|
6452
6510
|
if (this._inflight.size) await Promise.allSettled([...this._inflight]);
|
|
6453
6511
|
}
|
|
6454
6512
|
}
|
|
6455
|
-
async _runRequest(path, name, args = [], opts = {}, parent = null, release = null) {
|
|
6456
|
-
let released = false;
|
|
6457
|
-
const transfer = (t) => {
|
|
6458
|
-
if (release) {
|
|
6459
|
-
released = true;
|
|
6460
|
-
t.completion.whenSubtreeSettled().then(release);
|
|
6461
|
-
}
|
|
6462
|
-
};
|
|
6463
|
-
try {
|
|
6464
|
-
const curRoot = this.state.val;
|
|
6465
|
-
const txnPath = path.toTransactionPath();
|
|
6466
|
-
const curLeaf = txnPath.lookup(curRoot);
|
|
6467
|
-
const found = this.comps.getRequestFor(curLeaf, name);
|
|
6468
|
-
const handler = found ?? mkReq404(name);
|
|
6469
|
-
if (this._observers.length > 0) {
|
|
6470
|
-
const reqPath = txnPath.pinKeys(curRoot);
|
|
6471
|
-
this._emit({
|
|
6472
|
-
kind: "request",
|
|
6473
|
-
name,
|
|
6474
|
-
args,
|
|
6475
|
-
path: reqPath,
|
|
6476
|
-
pathKeys: reqPath.toKeys(),
|
|
6477
|
-
targetPath: path,
|
|
6478
|
-
handler: found?.fn ?? null,
|
|
6479
|
-
handlerName: found?.fn?.name || name,
|
|
6480
|
-
matched: found ? "exact" : "none",
|
|
6481
|
-
before: curLeaf,
|
|
6482
|
-
after: void 0,
|
|
6483
|
-
parent,
|
|
6484
|
-
timestamp: Date.now()
|
|
6485
|
-
});
|
|
6486
|
-
}
|
|
6487
|
-
const reqCtx = new RequestContext(path, this, parent, curRoot);
|
|
6488
|
-
const resHandlerName = opts?.onResName ?? name;
|
|
6489
|
-
const resPath = opts?.livePath ? null : txnPath.pinKeys(curRoot);
|
|
6490
|
-
const push = (specificName, baseName, singleArg, result, error) => {
|
|
6491
|
-
const resArgs = specificName ? [singleArg] : [result, error];
|
|
6492
|
-
const t = new ResponseEvent(path, this, specificName ?? baseName, resArgs, parent, resPath);
|
|
6493
|
-
transfer(t);
|
|
6494
|
-
this.pushTransaction(t);
|
|
6495
|
-
};
|
|
6496
|
-
try {
|
|
6497
|
-
const result = await handler.fn.apply(null, [...args, reqCtx]);
|
|
6498
|
-
push(opts?.onOkName, resHandlerName, result, result, null);
|
|
6499
|
-
} catch (error) {
|
|
6500
|
-
push(opts?.onErrorName, resHandlerName, error, null, error);
|
|
6501
|
-
}
|
|
6502
|
-
} finally {
|
|
6503
|
-
if (release && !released) release();
|
|
6504
|
-
}
|
|
6505
|
-
}
|
|
6506
6513
|
get hasPendingTransactions() {
|
|
6507
6514
|
return this.transactions.length > 0;
|
|
6508
6515
|
}
|
|
@@ -6519,6 +6526,7 @@ var Transactor = class {
|
|
|
6519
6526
|
this._emitTransaction(transaction, curState);
|
|
6520
6527
|
} else console.warn("undefined new state", { curState, transaction });
|
|
6521
6528
|
} finally {
|
|
6529
|
+
transaction.ensureWalkAdvanced?.();
|
|
6522
6530
|
transaction._completion?.ensureSelfSettled();
|
|
6523
6531
|
transaction._completion?.releaseSelf();
|
|
6524
6532
|
}
|
|
@@ -6527,12 +6535,6 @@ var Transactor = class {
|
|
|
6527
6535
|
this.transact(new InputEvent(path, event, eventHandler, this, dragInfo));
|
|
6528
6536
|
}
|
|
6529
6537
|
};
|
|
6530
|
-
function mkReq404(name) {
|
|
6531
|
-
const fn = () => {
|
|
6532
|
-
throw new Error(`Request not found: ${name}`);
|
|
6533
|
-
};
|
|
6534
|
-
return { fn };
|
|
6535
|
-
}
|
|
6536
6538
|
function nullHandler() {
|
|
6537
6539
|
return this;
|
|
6538
6540
|
}
|
|
@@ -6563,6 +6565,15 @@ var Transaction = class {
|
|
|
6563
6565
|
}
|
|
6564
6566
|
afterTransaction() {
|
|
6565
6567
|
}
|
|
6568
|
+
// Ending a walk and forwarding belong to the two dispatch buckets; the base warns
|
|
6569
|
+
// rather than throwing, so a stray call from the wrong place is a message and not a
|
|
6570
|
+
// crash. `walk` is undefined here, which is how ctx.reply/ctx.fail tell the two apart.
|
|
6571
|
+
stop() {
|
|
6572
|
+
console.warn('ctx.stop() is only meaningful in an "intent" handler - ignored');
|
|
6573
|
+
}
|
|
6574
|
+
forward(_opts) {
|
|
6575
|
+
console.warn('ctx.forward() needs a "receive" or "intent" handler - ignored');
|
|
6576
|
+
}
|
|
6566
6577
|
buildRootStack(root, comps) {
|
|
6567
6578
|
return Stack.root(comps, root);
|
|
6568
6579
|
}
|
|
@@ -6624,18 +6635,40 @@ var InputEvent = class extends Transaction {
|
|
|
6624
6635
|
this._dispatchPath ??= this.path.compact();
|
|
6625
6636
|
return this._dispatchPath;
|
|
6626
6637
|
}
|
|
6638
|
+
// A DOM event is an ADDRESSED message like any other — it reaches the component that
|
|
6639
|
+
// owns the view and stops. It keeps its own class only because it resolves its handler
|
|
6640
|
+
// from the compiled view and runs synchronously, not because it is a different channel.
|
|
6627
6641
|
get observeKind() {
|
|
6628
|
-
return "
|
|
6642
|
+
return "receive";
|
|
6629
6643
|
}
|
|
6630
6644
|
get observeName() {
|
|
6631
6645
|
return this.e?.type ?? null;
|
|
6632
6646
|
}
|
|
6647
|
+
// A view's name is a message. `forward` is where that name can LEAVE the component:
|
|
6648
|
+
// it becomes an intent with the same name and the same arguments, so a view that says
|
|
6649
|
+
// `@on.click="saveDraft .text"` never has to change when an ancestor takes the job over.
|
|
6650
|
+
// (A `$method` handler has a name too, so forwarding one raises an intent under it.)
|
|
6651
|
+
forward(opts) {
|
|
6652
|
+
this._forward = opts ?? {};
|
|
6653
|
+
}
|
|
6654
|
+
afterTransaction() {
|
|
6655
|
+
const f = this._forward;
|
|
6656
|
+
if (f === void 0) return;
|
|
6657
|
+
this._forward = void 0;
|
|
6658
|
+
const { args = this._handlerArgs ?? [], ...rest } = f;
|
|
6659
|
+
const hv = this.handler?.handlerCall?.handlerVal ?? this.handler?.handlerVal;
|
|
6660
|
+
const name = hv?.name;
|
|
6661
|
+
if (name === void 0)
|
|
6662
|
+
return console.warn("ctx.forward() from a handler with no name - ignored");
|
|
6663
|
+
this.transactor.pushIntent(this.dispatchPath, name, args, rest, this);
|
|
6664
|
+
}
|
|
6633
6665
|
buildRootStack(root, comps) {
|
|
6634
6666
|
return Stack.root(comps, root, this);
|
|
6635
6667
|
}
|
|
6636
6668
|
getHandlerAndArgs(root, _instance, comps) {
|
|
6637
6669
|
const stack = this.buildStack(root, comps);
|
|
6638
6670
|
const [handler, args] = this.handler.getHandlerAndArgs(stack, this);
|
|
6671
|
+
this._handlerArgs = [...args];
|
|
6639
6672
|
const path = this.dispatchPath;
|
|
6640
6673
|
args.push(new EventContext(path, this.transactor, this));
|
|
6641
6674
|
return [handler, args];
|
|
@@ -6720,39 +6753,190 @@ var NameArgsTransaction = class extends Transaction {
|
|
|
6720
6753
|
return [handler, [...this.args, new EventContext(this.path, this.transactor, this)]];
|
|
6721
6754
|
}
|
|
6722
6755
|
};
|
|
6723
|
-
var ResponseEvent = class extends NameArgsTransaction {
|
|
6724
|
-
handlerProp = "response";
|
|
6725
|
-
constructor(path, transactor, name, args, parent, txnPath = null) {
|
|
6726
|
-
super(path, transactor, name, args, parent);
|
|
6727
|
-
this._txnPath = txnPath;
|
|
6728
|
-
}
|
|
6729
|
-
getTransactionPath() {
|
|
6730
|
-
return this._txnPath ?? super.getTransactionPath();
|
|
6731
|
-
}
|
|
6732
|
-
};
|
|
6733
6756
|
var SendEvent = class extends NameArgsTransaction {
|
|
6734
6757
|
handlerProp = "receive";
|
|
6735
|
-
|
|
6736
|
-
|
|
6758
|
+
// `receive` is the ADDRESSED bucket, and it is the only one: a view's `@on.click`,
|
|
6759
|
+
// a parent's ctx.send, the host's sendAtRoot and an answer to an intent all arrive
|
|
6760
|
+
// here, and a handler cannot tell them apart. Splitting on where a message came from
|
|
6761
|
+
// would let the view's name through and turn the parent's away, though the two say
|
|
6762
|
+
// the same thing — and neither a test nor a parent could drive the component.
|
|
6763
|
+
get observeKind() {
|
|
6764
|
+
return this._isAnswer ? "answer" : "receive";
|
|
6765
|
+
}
|
|
6766
|
+
// `forward` in a RECEIVE body starts a walk: the message that arrived becomes an
|
|
6767
|
+
// intent, keeping its name and payload. This is what lets a view's name leave the
|
|
6768
|
+
// component without the view changing.
|
|
6769
|
+
forward(opts) {
|
|
6770
|
+
this._forward = opts ?? {};
|
|
6737
6771
|
}
|
|
6738
6772
|
afterTransaction() {
|
|
6739
|
-
const
|
|
6740
|
-
if (
|
|
6741
|
-
|
|
6773
|
+
const f = this._forward;
|
|
6774
|
+
if (f === void 0) return;
|
|
6775
|
+
this._forward = void 0;
|
|
6776
|
+
const { args = this.args, ...rest } = f;
|
|
6777
|
+
this.transactor.pushIntent(this.path, this.name, args, rest, this);
|
|
6742
6778
|
}
|
|
6743
6779
|
};
|
|
6744
|
-
var
|
|
6745
|
-
handlerProp = "
|
|
6746
|
-
constructor(path, transactor,
|
|
6747
|
-
super(path, transactor, name, args, parent,
|
|
6748
|
-
this.
|
|
6780
|
+
var IntentEvent = class extends NameArgsTransaction {
|
|
6781
|
+
handlerProp = "intent";
|
|
6782
|
+
constructor(path, transactor, walk) {
|
|
6783
|
+
super(path, transactor, walk.name, walk.args, walk.parent, {});
|
|
6784
|
+
this.walk = walk;
|
|
6785
|
+
this.targetPath = walk.origin;
|
|
6749
6786
|
}
|
|
6750
|
-
|
|
6751
|
-
|
|
6787
|
+
// `forward` in an INTENT body amends the hop about to be pushed — it does not push
|
|
6788
|
+
// one itself, because a walk advances on its own.
|
|
6789
|
+
forward(opts) {
|
|
6790
|
+
this.walk.amend(opts, this.path);
|
|
6791
|
+
}
|
|
6792
|
+
stop() {
|
|
6793
|
+
this.walk.finish(null, null);
|
|
6794
|
+
}
|
|
6795
|
+
afterTransaction() {
|
|
6796
|
+
this.ensureWalkAdvanced();
|
|
6797
|
+
}
|
|
6798
|
+
// Idempotent, because both afterTransaction and the transactor's `finally` call it —
|
|
6799
|
+
// whichever gets there first advances the walk exactly once.
|
|
6800
|
+
ensureWalkAdvanced() {
|
|
6801
|
+
if (this._advanced) return;
|
|
6802
|
+
this._advanced = true;
|
|
6803
|
+
this.walk.advance();
|
|
6752
6804
|
}
|
|
6753
6805
|
};
|
|
6754
|
-
var
|
|
6755
|
-
|
|
6806
|
+
var PASS = /* @__PURE__ */ Symbol.for("tutuca.intent.pass");
|
|
6807
|
+
var DEFAULT_ROUTE = ["dyn", "lex"];
|
|
6808
|
+
var INTENT_DEPTH = 64;
|
|
6809
|
+
var IntentWalk = class {
|
|
6810
|
+
constructor(transactor, path, name, args, opts, parent, release) {
|
|
6811
|
+
this.transactor = transactor;
|
|
6812
|
+
this.name = name;
|
|
6813
|
+
this.args = args;
|
|
6814
|
+
this.route = opts?.route ?? DEFAULT_ROUTE;
|
|
6815
|
+
this.parent = parent;
|
|
6816
|
+
this.release = release;
|
|
6817
|
+
this.origin = path;
|
|
6818
|
+
this.answerPath = opts?.livePath ? null : path.toTransactionPath().pinKeys(transactor.state.val);
|
|
6819
|
+
this.legIndex = 0;
|
|
6820
|
+
this.dynAt = path;
|
|
6821
|
+
this.hops = 0;
|
|
6822
|
+
this.ended = false;
|
|
6823
|
+
}
|
|
6824
|
+
// Offer the intent to the next hop. Called once at dispatch, and again by every hop
|
|
6825
|
+
// that ran without answering.
|
|
6826
|
+
advance() {
|
|
6827
|
+
if (this.ended) return;
|
|
6828
|
+
if (this.hops++ >= INTENT_DEPTH) return this.exhaust("intentDepth");
|
|
6829
|
+
while (this.legIndex < this.route.length) {
|
|
6830
|
+
const leg = this.route[this.legIndex];
|
|
6831
|
+
if (leg === "dyn") {
|
|
6832
|
+
if (this.dynAt.steps.length === 0) {
|
|
6833
|
+
this.legIndex++;
|
|
6834
|
+
continue;
|
|
6835
|
+
}
|
|
6836
|
+
this.dynAt = this.dynAt.popStep();
|
|
6837
|
+
this.transactor.pushTransaction(new IntentEvent(this.dynAt, this.transactor, this));
|
|
6838
|
+
return;
|
|
6839
|
+
}
|
|
6840
|
+
if (leg === "lex") {
|
|
6841
|
+
this.legIndex++;
|
|
6842
|
+
return this._tryLex();
|
|
6843
|
+
}
|
|
6844
|
+
console.warn("unknown intent route leg", leg, '- expected "dyn" or "lex"');
|
|
6845
|
+
this.legIndex++;
|
|
6846
|
+
}
|
|
6847
|
+
this.exhaust("noHandler");
|
|
6848
|
+
}
|
|
6849
|
+
// The `lex` leg: the handlers registered on the scope chain of the component that
|
|
6850
|
+
// raised the intent, in order.
|
|
6851
|
+
_tryLex() {
|
|
6852
|
+
const root = this.transactor.state.val;
|
|
6853
|
+
const txnPath = this.origin.toTransactionPath();
|
|
6854
|
+
const leaf = txnPath.lookup(root);
|
|
6855
|
+
const chain = this.transactor.comps.getIntentChainFor(leaf, this.name);
|
|
6856
|
+
if (this.transactor._observers.length > 0) {
|
|
6857
|
+
const path = txnPath.pinKeys(root);
|
|
6858
|
+
this.transactor._emit({
|
|
6859
|
+
kind: "intent",
|
|
6860
|
+
name: this.name,
|
|
6861
|
+
args: this.args,
|
|
6862
|
+
path,
|
|
6863
|
+
pathKeys: path.toKeys(),
|
|
6864
|
+
targetPath: this.origin,
|
|
6865
|
+
handler: chain[0]?.fn ?? null,
|
|
6866
|
+
handlerName: chain[0]?.fn?.name || this.name,
|
|
6867
|
+
matched: chain.length > 0 ? "exact" : "none",
|
|
6868
|
+
before: leaf,
|
|
6869
|
+
after: void 0,
|
|
6870
|
+
parent: this.parent,
|
|
6871
|
+
timestamp: Date.now()
|
|
6872
|
+
});
|
|
6873
|
+
}
|
|
6874
|
+
if (chain.length === 0) return this.advance();
|
|
6875
|
+
const p = this._runLex(chain, 0);
|
|
6876
|
+
this.transactor._inflight.add(p);
|
|
6877
|
+
p.finally(() => this.transactor._inflight.delete(p));
|
|
6878
|
+
}
|
|
6879
|
+
async _runLex(chain, i) {
|
|
6880
|
+
if (this.ended) return;
|
|
6881
|
+
if (i >= chain.length) return this.advance();
|
|
6882
|
+
const ctx = new IntentContext(this.origin, this.transactor, this.parent);
|
|
6883
|
+
try {
|
|
6884
|
+
const res = await chain[i].fn.apply(null, [...this.args, ctx]);
|
|
6885
|
+
if (res === PASS) return this._runLex(chain, i + 1);
|
|
6886
|
+
this.answer("Ok", res);
|
|
6887
|
+
} catch (error) {
|
|
6888
|
+
this.answer("Error", error);
|
|
6889
|
+
}
|
|
6890
|
+
}
|
|
6891
|
+
// A hop answered. Ends the walk.
|
|
6892
|
+
answer(suffix, value) {
|
|
6893
|
+
this.finish(`${this.name}${suffix}`, [value]);
|
|
6894
|
+
}
|
|
6895
|
+
// `forward` from an intent body: keep walking, optionally with new arguments or a
|
|
6896
|
+
// narrowed route.
|
|
6897
|
+
amend(opts, from) {
|
|
6898
|
+
if (opts?.args !== void 0) this.args = opts.args;
|
|
6899
|
+
if (opts?.route !== void 0) {
|
|
6900
|
+
this.route = opts.route;
|
|
6901
|
+
this.legIndex = 0;
|
|
6902
|
+
this.dynAt = from ?? this.dynAt;
|
|
6903
|
+
}
|
|
6904
|
+
}
|
|
6905
|
+
// The route ran out. What the sender hears depends only on what it DECLARES, which is
|
|
6906
|
+
// how "a sender expects an answer if and only if it declares an answer arm" becomes
|
|
6907
|
+
// code — nobody writes it down twice, and there is no schema to keep in step.
|
|
6908
|
+
exhaust(reason) {
|
|
6909
|
+
if (this.ended) return;
|
|
6910
|
+
const { name, args } = this;
|
|
6911
|
+
const comp = this.transactor.comps.getCompFor(
|
|
6912
|
+
this.origin.toTransactionPath().lookup(this.transactor.state.val)
|
|
6913
|
+
);
|
|
6914
|
+
const declares = (n) => comp?.receive?.[n] !== void 0;
|
|
6915
|
+
if (declares(`${name}Unhandled`)) {
|
|
6916
|
+
this.finish(`${name}Unhandled`, args);
|
|
6917
|
+
} else if (declares(`${name}Error`)) {
|
|
6918
|
+
this.finish(`${name}Error`, [reason]);
|
|
6919
|
+
} else {
|
|
6920
|
+
if (declares(`${name}Ok`))
|
|
6921
|
+
console.warn(
|
|
6922
|
+
`intent "${name}" was not answered (${reason}) and this component declares only "${name}Ok" - add "${name}Unhandled" or "${name}Error" to handle it`
|
|
6923
|
+
);
|
|
6924
|
+
this.finish(null, null);
|
|
6925
|
+
}
|
|
6926
|
+
}
|
|
6927
|
+
// End the walk. `name === null` means no answer goes out, which is what `stop` and a
|
|
6928
|
+
// fire-and-forget notification both are — the tracked unit is released here instead
|
|
6929
|
+
// of following an answer's subtree.
|
|
6930
|
+
finish(name, args) {
|
|
6931
|
+
if (this.ended) return;
|
|
6932
|
+
this.ended = true;
|
|
6933
|
+
if (name === null) return this.release?.();
|
|
6934
|
+
const path = this.answerPath ?? this.origin;
|
|
6935
|
+
const t = new SendEvent(path, this.transactor, name, args, this.parent, {});
|
|
6936
|
+
t._isAnswer = true;
|
|
6937
|
+
this.transactor.pushTransaction(t);
|
|
6938
|
+
if (this.release) t.completion.whenSubtreeSettled().then(this.release);
|
|
6939
|
+
}
|
|
6756
6940
|
};
|
|
6757
6941
|
var Completion = class {
|
|
6758
6942
|
constructor() {
|
|
@@ -6833,23 +7017,21 @@ var Dispatcher = class {
|
|
|
6833
7017
|
get at() {
|
|
6834
7018
|
return new PathChanges(this);
|
|
6835
7019
|
}
|
|
7020
|
+
// A message is ADDRESSED: it names one component and stops there.
|
|
6836
7021
|
send(name, args, opts) {
|
|
6837
7022
|
return this.sendAtPath(this.path, name, args, opts);
|
|
6838
7023
|
}
|
|
6839
|
-
bubble(name, args, opts) {
|
|
6840
|
-
return this.send(name, args, { skipSelf: true, bubbles: true, ...opts });
|
|
6841
|
-
}
|
|
6842
7024
|
sendAtPath(path, name, args, opts) {
|
|
6843
7025
|
return this.transactor.pushSend(path, name, args, opts, this.parent);
|
|
6844
7026
|
}
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
return this.
|
|
7027
|
+
// An intent is ROUTED: it names a job and walks until something answers. The verb
|
|
7028
|
+
// does not decide which scope answers — `opts.route` does, written here at the call
|
|
7029
|
+
// site, which is where the decision actually is.
|
|
7030
|
+
intent(name, args, opts) {
|
|
7031
|
+
return this.intentAtPath(this.path, name, args, opts);
|
|
6850
7032
|
}
|
|
6851
|
-
|
|
6852
|
-
return this.transactor.
|
|
7033
|
+
intentAtPath(path, name, args, opts) {
|
|
7034
|
+
return this.transactor.pushIntent(path, name, args, opts, this.parent);
|
|
6853
7035
|
}
|
|
6854
7036
|
lookupTypeFor(name, inst) {
|
|
6855
7037
|
return this.transactor.comps.getCompFor(inst).scope.lookupComponent(name);
|
|
@@ -6862,12 +7044,30 @@ var EventContext = class extends Dispatcher {
|
|
|
6862
7044
|
get targetPath() {
|
|
6863
7045
|
return this.parent.targetPath;
|
|
6864
7046
|
}
|
|
6865
|
-
|
|
6866
|
-
|
|
7047
|
+
// Answer the intent being handled, with a result or with an error. Either ENDS the
|
|
7048
|
+
// walk. Legal only in an `intent` handler: a message has no sender waiting on it.
|
|
7049
|
+
reply(value) {
|
|
7050
|
+
return this.parent.walk === void 0 ? warnNotIntent("reply") : this.parent.walk.answer("Ok", value);
|
|
7051
|
+
}
|
|
7052
|
+
fail(error) {
|
|
7053
|
+
return this.parent.walk === void 0 ? warnNotIntent("fail") : this.parent.walk.answer("Error", error);
|
|
7054
|
+
}
|
|
7055
|
+
// End the walk answering nothing — "served, and no answer".
|
|
7056
|
+
stop() {
|
|
7057
|
+
return this.parent.stop();
|
|
7058
|
+
}
|
|
7059
|
+
// From an `intent` body: hand the intent to the next hop, optionally amending it.
|
|
7060
|
+
// From a `receive` body: turn the message that arrived into an intent. One word from
|
|
7061
|
+
// both ends of a walk; which one you get depends on which bucket you are in.
|
|
7062
|
+
forward(opts) {
|
|
7063
|
+
return this.parent.forward(opts);
|
|
6867
7064
|
}
|
|
6868
7065
|
};
|
|
6869
|
-
var
|
|
7066
|
+
var IntentContext = class extends Dispatcher {
|
|
6870
7067
|
};
|
|
7068
|
+
function warnNotIntent(verb) {
|
|
7069
|
+
console.warn(`ctx.${verb}() is only meaningful in an "intent" handler - ignored`);
|
|
7070
|
+
}
|
|
6871
7071
|
var PathChanges = class extends PathBuilder {
|
|
6872
7072
|
constructor(dispatcher) {
|
|
6873
7073
|
super();
|
|
@@ -6876,8 +7076,8 @@ var PathChanges = class extends PathBuilder {
|
|
|
6876
7076
|
send(name, args, opts) {
|
|
6877
7077
|
return this.dispatcher.sendAtPath(this.buildPath(), name, args, opts);
|
|
6878
7078
|
}
|
|
6879
|
-
|
|
6880
|
-
return this.
|
|
7079
|
+
intent(name, args, opts) {
|
|
7080
|
+
return this.dispatcher.intentAtPath(this.buildPath(), name, args, opts);
|
|
6881
7081
|
}
|
|
6882
7082
|
buildPath() {
|
|
6883
7083
|
return this.dispatcher.path.concat(this.pathChanges);
|
|
@@ -7149,29 +7349,24 @@ function captureError(e) {
|
|
|
7149
7349
|
if ("actual" in e) out.actual = e.actual;
|
|
7150
7350
|
return out;
|
|
7151
7351
|
}
|
|
7152
|
-
function buildStack({ components = [], macros = null,
|
|
7352
|
+
function buildStack({ components = [], macros = null, intentHandlers = null } = {}) {
|
|
7153
7353
|
const stack = new ComponentStack();
|
|
7154
7354
|
stack.registerComponents(components);
|
|
7155
7355
|
if (macros) stack.registerMacros(macros);
|
|
7156
|
-
if (
|
|
7356
|
+
if (intentHandlers) stack.registerIntentHandlers(intentHandlers);
|
|
7157
7357
|
return stack;
|
|
7158
7358
|
}
|
|
7159
7359
|
async function driveStack(stack, value, phase, opts = {}) {
|
|
7160
|
-
const warn = opts.warn ?? console.warn;
|
|
7161
7360
|
const transactor = new Transactor(stack.comps, value);
|
|
7162
7361
|
if (opts.onMessage)
|
|
7163
7362
|
transactor.state.onChange(({ val, old, info }) => {
|
|
7164
7363
|
const t = info?.transaction;
|
|
7165
7364
|
opts.onMessage(
|
|
7166
|
-
{ kind: t?.handlerProp ?? "
|
|
7365
|
+
{ kind: t?.handlerProp ?? "receive", name: t?.name, args: t?.args, path: t?.path },
|
|
7167
7366
|
old,
|
|
7168
7367
|
val
|
|
7169
7368
|
);
|
|
7170
7369
|
});
|
|
7171
|
-
if (phaseHasBubble(phase))
|
|
7172
|
-
warn(
|
|
7173
|
-
"drive(): a `bubble` action is a no-op here — drive originates at the root and bubbles travel child→parent, so there is no ancestor to receive it (and the root's own bubble handler is skipped). To exercise a bubble handler, call it directly."
|
|
7174
|
-
);
|
|
7175
7370
|
dispatchPhase(rootDispatcher(transactor), new Path([]), phase, value);
|
|
7176
7371
|
await transactor.settle();
|
|
7177
7372
|
return transactor.state.val;
|
|
@@ -7187,7 +7382,7 @@ async function runTests({
|
|
|
7187
7382
|
name = null,
|
|
7188
7383
|
grep = null,
|
|
7189
7384
|
bail = false,
|
|
7190
|
-
|
|
7385
|
+
intentHandlers = null,
|
|
7191
7386
|
macros = null
|
|
7192
7387
|
} = {}) {
|
|
7193
7388
|
const counts = { pass: 0, fail: 0, skip: 0, total: 0 };
|
|
@@ -7201,7 +7396,7 @@ async function runTests({
|
|
|
7201
7396
|
}
|
|
7202
7397
|
const { describe, test: test2, moduleTests } = makeCollector({ path, components });
|
|
7203
7398
|
let _stack = null;
|
|
7204
|
-
const getStack = () => _stack ??= buildStack({ components, macros,
|
|
7399
|
+
const getStack = () => _stack ??= buildStack({ components, macros, intentHandlers });
|
|
7205
7400
|
const driveLocal = (value, phase, opts = {}) => driveStack(getStack(), value, phase, opts);
|
|
7206
7401
|
await getTests({ describe, test: test2, expect: expect3, drive: driveLocal });
|
|
7207
7402
|
let bailed = false;
|
|
@@ -7267,7 +7462,7 @@ async function runTests({
|
|
|
7267
7462
|
}
|
|
7268
7463
|
|
|
7269
7464
|
// tools/format/console.js
|
|
7270
|
-
var
|
|
7465
|
+
var PASS2 = "color: #0a0; font-weight: bold";
|
|
7271
7466
|
var FAIL = "color: #c00; font-weight: bold";
|
|
7272
7467
|
var SKIP = "color: #888";
|
|
7273
7468
|
var DIM = "color: #888";
|
|
@@ -7282,7 +7477,7 @@ function reportTestNode(node) {
|
|
|
7282
7477
|
}
|
|
7283
7478
|
const dur = node.status === "skip" ? "" : ` (${Math.round(node.durationMs)}ms)`;
|
|
7284
7479
|
if (node.status === "pass") {
|
|
7285
|
-
console.log(`%c✓%c ${node.title}%c${dur}`,
|
|
7480
|
+
console.log(`%c✓%c ${node.title}%c${dur}`, PASS2, RESET, DIM);
|
|
7286
7481
|
} else if (node.status === "skip") {
|
|
7287
7482
|
console.log(`%c○ ${node.title}%c (skipped)`, SKIP, RESET);
|
|
7288
7483
|
} else {
|
|
@@ -7309,7 +7504,7 @@ function reportTestReportToConsole(report) {
|
|
|
7309
7504
|
const summary = `${c.pass} passed, ${c.fail} failed, ${c.skip} skipped (${c.total} total)`;
|
|
7310
7505
|
if (c.fail > 0) console.error(`%c${summary}`, FAIL);
|
|
7311
7506
|
else if (c.total === 0) console.log(`%c${summary}`, DIM);
|
|
7312
|
-
else console.log(`%c${summary}`,
|
|
7507
|
+
else console.log(`%c${summary}`, PASS2);
|
|
7313
7508
|
console.groupEnd();
|
|
7314
7509
|
}
|
|
7315
7510
|
return report;
|
|
@@ -7399,16 +7594,22 @@ function lintIdToMessage(id, info) {
|
|
|
7399
7594
|
}
|
|
7400
7595
|
case "UNKNOWN_HANDLER_ARG_NAME":
|
|
7401
7596
|
return `Unknown handler argument '${info.name}'${fmtOriginSuffix(info)}`;
|
|
7402
|
-
case "
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
case "
|
|
7597
|
+
case "RETIRED_HANDLER_BUCKET": {
|
|
7598
|
+
const which = info.names?.length ? ` (${info.names.join(", ")})` : "";
|
|
7599
|
+
return `\`${info.key}\` is no longer a handler bucket${which} — ${info.how}`;
|
|
7600
|
+
}
|
|
7601
|
+
case "RETIRED_CTX_VERB":
|
|
7602
|
+
return `ctx.${info.verb}() no longer exists — use ${info.to} (in \`${info.bucket}.${info.name}\`)`;
|
|
7603
|
+
case "HANDLER_NAME_COLLISION":
|
|
7604
|
+
return `${info.problem} — ${info.fix}`;
|
|
7605
|
+
case "RECEIVE_HANDLER_NOT_IMPLEMENTED":
|
|
7606
|
+
return `Receive handler '${info.name}' is not implemented${fmtEventSuffix(info)}`;
|
|
7607
|
+
case "RECEIVE_HANDLER_METHOD_NOT_IMPLEMENTED":
|
|
7407
7608
|
return `Method '$${info.name}' is not implemented${fmtEventSuffix(info)}`;
|
|
7408
|
-
case "
|
|
7409
|
-
return `'$${info.name}' is a method reference, but '${info.name}' is defined as
|
|
7410
|
-
case "
|
|
7411
|
-
return `'${info.name}' is
|
|
7609
|
+
case "RECEIVE_HANDLER_FOR_METHOD":
|
|
7610
|
+
return `'$${info.name}' is a method reference, but '${info.name}' is defined as a receive handler${fmtEventSuffix(info)}`;
|
|
7611
|
+
case "METHOD_FOR_RECEIVE_HANDLER":
|
|
7612
|
+
return `'${info.name}' is a receive handler reference, but '${info.name}' is defined as a method${fmtEventSuffix(info)}`;
|
|
7412
7613
|
case "FIELD_NAME_RESERVED_BY_RECORD":
|
|
7413
7614
|
return `Field '${info.name}' collides with the Immutable Record API: '.${info.name}' reads the Record member, not your value — rename the field (its value is only reachable via .get('${info.name}'))`;
|
|
7414
7615
|
case "FIELD_VAL_NOT_DEFINED":
|
|
@@ -8512,10 +8713,9 @@ function getComponentDoc(comp) {
|
|
|
8512
8713
|
name: k,
|
|
8513
8714
|
sig: getSignature(k, methods[k])
|
|
8514
8715
|
}));
|
|
8515
|
-
const
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
}));
|
|
8716
|
+
const sigs = (bucket) => Object.keys(bucket ?? {}).map((k) => ({ name: k, sig: getSignature(k, bucket[k]) }));
|
|
8717
|
+
const receiveHandlers = sigs(comp.receive);
|
|
8718
|
+
const intentHandlers = sigs(comp.intent);
|
|
8519
8719
|
const fieldDocs = [];
|
|
8520
8720
|
for (const fieldName in fields) {
|
|
8521
8721
|
const field = fields[fieldName];
|
|
@@ -8526,7 +8726,13 @@ function getComponentDoc(comp) {
|
|
|
8526
8726
|
methods: getFieldMethods(field)
|
|
8527
8727
|
});
|
|
8528
8728
|
}
|
|
8529
|
-
return {
|
|
8729
|
+
return {
|
|
8730
|
+
name,
|
|
8731
|
+
methods: userMethods,
|
|
8732
|
+
receive: receiveHandlers,
|
|
8733
|
+
intent: intentHandlers,
|
|
8734
|
+
fields: fieldDocs
|
|
8735
|
+
};
|
|
8530
8736
|
}
|
|
8531
8737
|
function docComponents(normalized, { name = null } = {}) {
|
|
8532
8738
|
const comps = normalized.components;
|
|
@@ -8634,13 +8840,9 @@ export {
|
|
|
8634
8840
|
FIELD_VAL_IS_METHOD,
|
|
8635
8841
|
FIELD_VAL_NOT_DEFINED,
|
|
8636
8842
|
GLOBAL_SELECTOR_IN_SCOPED_STYLE,
|
|
8843
|
+
HANDLER_NAME_COLLISION,
|
|
8637
8844
|
IF_NO_BRANCH_SET,
|
|
8638
8845
|
Map3 as IMap,
|
|
8639
|
-
INPUT_HANDLER_FOR_INPUT_HANDLER_METHOD,
|
|
8640
|
-
INPUT_HANDLER_METHOD_FOR_INPUT_HANDLER,
|
|
8641
|
-
INPUT_HANDLER_METHOD_NOT_IMPLEMENTED,
|
|
8642
|
-
INPUT_HANDLER_NOT_IMPLEMENTED,
|
|
8643
|
-
INPUT_HANDLER_NOT_REFERENCED,
|
|
8644
8846
|
Set3 as ISet,
|
|
8645
8847
|
LOOKUP_BAD_SHAPE,
|
|
8646
8848
|
LOOKUP_TARGET_MALFORMED,
|
|
@@ -8653,6 +8855,7 @@ export {
|
|
|
8653
8855
|
List2 as List,
|
|
8654
8856
|
MAYBE_ADD_AT_PREFIX,
|
|
8655
8857
|
MAYBE_DROP_AT_PREFIX,
|
|
8858
|
+
METHOD_FOR_RECEIVE_HANDLER,
|
|
8656
8859
|
METHOD_VAL_IS_FIELD,
|
|
8657
8860
|
METHOD_VAL_NOT_DEFINED,
|
|
8658
8861
|
Map2 as Map,
|
|
@@ -8662,12 +8865,18 @@ export {
|
|
|
8662
8865
|
OrderedMap3 as OMap,
|
|
8663
8866
|
OrderedMap2 as OrderedMap,
|
|
8664
8867
|
OrderedSet,
|
|
8868
|
+
PASS,
|
|
8665
8869
|
PLACEHOLDERLESS_TEMPLATE_STRING,
|
|
8666
8870
|
PROVIDE_NOT_ADDRESSABLE,
|
|
8667
8871
|
PairSorting,
|
|
8668
8872
|
ParseContext,
|
|
8873
|
+
RECEIVE_HANDLER_FOR_METHOD,
|
|
8874
|
+
RECEIVE_HANDLER_METHOD_NOT_IMPLEMENTED,
|
|
8875
|
+
RECEIVE_HANDLER_NOT_IMPLEMENTED,
|
|
8669
8876
|
REDUNDANT_TEMPLATE_STRING,
|
|
8670
8877
|
RENDER_IT_OUTSIDE_OF_LOOP,
|
|
8878
|
+
RETIRED_CTX_VERB,
|
|
8879
|
+
RETIRED_HANDLER_BUCKET,
|
|
8671
8880
|
Range,
|
|
8672
8881
|
Record2 as Record,
|
|
8673
8882
|
RenderBatch,
|
|
@@ -8738,7 +8947,6 @@ export {
|
|
|
8738
8947
|
mergeDeep,
|
|
8739
8948
|
mergeDeepWith,
|
|
8740
8949
|
mergeWith,
|
|
8741
|
-
phaseHasBubble,
|
|
8742
8950
|
phaseOps,
|
|
8743
8951
|
remove,
|
|
8744
8952
|
removeIn,
|