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