jssm 5.138.0 → 5.139.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.
@@ -21327,7 +21327,7 @@ var constants = /*#__PURE__*/Object.freeze({
21327
21327
  * Useful for runtime diagnostics and for embedding in serialized machine
21328
21328
  * snapshots so that deserializers can detect version-skew.
21329
21329
  */
21330
- const version = "5.138.0";
21330
+ const version = "5.139.0";
21331
21331
 
21332
21332
  // whargarbl lots of these return arrays could/should be sets
21333
21333
  const { state_name_chars, state_name_first_chars, action_label_chars } = constants;
@@ -25133,7 +25133,7 @@ function make_hook_proxy(ctx, machine) {
25133
25133
  * @param debug_id - Identifier appended to the synthetic sourceURL.
25134
25134
  * @returns The compiled handler.
25135
25135
  */
25136
- function compile_inline_body(body, debug_id) {
25136
+ function compile_inline_body$1(body, debug_id) {
25137
25137
  const annotated = `//# sourceURL=jssm-hook:${debug_id}\n${body}`;
25138
25138
  const ctor = Function;
25139
25139
  return new ctor('m', annotated);
@@ -25148,7 +25148,7 @@ function compile_inline_body(body, debug_id) {
25148
25148
  * @returns The resolved handler.
25149
25149
  * @throws Error - If no callable of that name is found in either location.
25150
25150
  */
25151
- function resolve_named_handler(name, registry) {
25151
+ function resolve_named_handler$1(name, registry) {
25152
25152
  if (registry !== undefined) {
25153
25153
  const registered = registry.get(name);
25154
25154
  if (registered !== undefined) {
@@ -25208,8 +25208,8 @@ function parse_hook_element(el, debug_id, registry) {
25208
25208
  throw new Error('<jssm-hook>: must specify either handler="name" attribute or an inline body');
25209
25209
  }
25210
25210
  const user_handler = handler_attr !== null
25211
- ? resolve_named_handler(handler_attr, registry)
25212
- : compile_inline_body(body_text, debug_id);
25211
+ ? resolve_named_handler$1(handler_attr, registry)
25212
+ : compile_inline_body$1(body_text, debug_id);
25213
25213
  const kind = normalize_hook_kind(el.getAttribute('kind'));
25214
25214
  // Convert null → undefined so downstream descriptors omit absent keys.
25215
25215
  const from = (_a = el.getAttribute('from')) !== null && _a !== void 0 ? _a : undefined;
@@ -25272,6 +25272,177 @@ function build_hook_descriptor(spec, wrapped) {
25272
25272
  return base;
25273
25273
  }
25274
25274
 
25275
+ /**
25276
+ * Allow-list of event names accepted by `<jssm-on event="...">`. Must stay
25277
+ * in sync with the `JssmEventName` union in `jssm_types.ts` (the library's
25278
+ * `machine.on(...)` event API, added in #638). Validating here gives the
25279
+ * declarative wiring a clear "unknown event name" error at the WC layer
25280
+ * instead of relying on a downstream library throw whose message would
25281
+ * mention `machine.on(...)` rather than the offending tag.
25282
+ */
25283
+ const JSSM_ON_EVENT_NAMES = new Set([
25284
+ 'transition',
25285
+ 'rejection',
25286
+ 'action',
25287
+ 'entry',
25288
+ 'exit',
25289
+ 'terminal',
25290
+ 'complete',
25291
+ 'error',
25292
+ 'data-change',
25293
+ 'override',
25294
+ 'timeout',
25295
+ 'hook-registration',
25296
+ 'hook-removal'
25297
+ ]);
25298
+ /**
25299
+ * Parse a `<jssm-on>` element into a validated {@link ParsedJssmOn}
25300
+ * record. Centralized so the declarative-tag logic is testable without
25301
+ * spinning up the full `<jssm-instance>` lifecycle.
25302
+ *
25303
+ * Validation rules (per #643):
25304
+ * - `event` is required and must be in {@link JSSM_ON_EVENT_NAMES}.
25305
+ * - Either a `handler="name"` attribute or non-empty `textContent`
25306
+ * must be supplied, but not both.
25307
+ * - `state` is only meaningful for `event="entry"` / `event="exit"`;
25308
+ * it's silently ignored on other events.
25309
+ * - `from` / `to` are only meaningful for `event="transition"`; they
25310
+ * are silently ignored on other events. Both → AND (a specific
25311
+ * edge). Neither → unfiltered.
25312
+ *
25313
+ * ```typescript
25314
+ * const el = document.createElement('jssm-on');
25315
+ * el.setAttribute('event', 'entry');
25316
+ * el.setAttribute('state', 'paid');
25317
+ * el.setAttribute('handler', 'onPaid');
25318
+ * parse_jssm_on_element(el);
25319
+ * // => { event: 'entry', handler_name: 'onPaid', inline_body: undefined,
25320
+ * // once: false, name: undefined, filter: { state: 'paid' } }
25321
+ * ```
25322
+ *
25323
+ * @param el - The `<jssm-on>` element to parse.
25324
+ * @returns A validated {@link ParsedJssmOn} record.
25325
+ * @throws If `event` is missing, unknown, both handler forms are
25326
+ * supplied, or neither handler form is supplied.
25327
+ */
25328
+ function parse_jssm_on_element(el) {
25329
+ const event_attr = el.getAttribute('event');
25330
+ if (event_attr === null || event_attr.trim().length === 0) {
25331
+ throw new Error('<jssm-on>: missing required `event` attribute');
25332
+ }
25333
+ const event = event_attr.trim();
25334
+ if (!JSSM_ON_EVENT_NAMES.has(event)) {
25335
+ throw new Error(`<jssm-on>: unknown event "${event}"`);
25336
+ }
25337
+ const handler_attr = el.getAttribute('handler');
25338
+ const handler_name = (handler_attr !== null && handler_attr.trim().length > 0)
25339
+ ? handler_attr.trim()
25340
+ : undefined;
25341
+ // textContent on a connected HTMLElement is always a string, so the
25342
+ // historical `?? ''` fallback never executed. Use a direct cast here
25343
+ // and let test cases that supply a literal `null` (defensive coverage)
25344
+ // hit the `=== null` branch instead — that branch is reachable via
25345
+ // Object.defineProperty in tests, where the `??` form would be a dead
25346
+ // operator.
25347
+ const body_text = el.textContent;
25348
+ const inline_body = (body_text !== null && body_text.trim().length > 0) ? body_text : undefined;
25349
+ if (handler_name !== undefined && inline_body !== undefined) {
25350
+ throw new Error('<jssm-on>: specify handler="name" OR inline body, not both');
25351
+ }
25352
+ if (handler_name === undefined && inline_body === undefined) {
25353
+ throw new Error('<jssm-on>: must specify handler="name" or an inline body');
25354
+ }
25355
+ const once_attr = el.hasAttribute('once');
25356
+ const name_attr = el.getAttribute('name');
25357
+ const name = (name_attr !== null && name_attr.trim().length > 0) ? name_attr.trim() : undefined;
25358
+ // Build the filter, but only honour attributes that apply to this event.
25359
+ // Unknown filter attributes for an event are silently ignored, matching
25360
+ // the documented semantics in the issue.
25361
+ let filter;
25362
+ if (event === 'entry' || event === 'exit') {
25363
+ const state_attr = el.getAttribute('state');
25364
+ if (state_attr !== null && state_attr.length > 0) {
25365
+ filter = { state: state_attr };
25366
+ }
25367
+ }
25368
+ else if (event === 'transition') {
25369
+ const from_attr = el.getAttribute('from');
25370
+ const to_attr = el.getAttribute('to');
25371
+ const candidate = {};
25372
+ if (from_attr !== null && from_attr.length > 0) {
25373
+ candidate.from = from_attr;
25374
+ }
25375
+ if (to_attr !== null && to_attr.length > 0) {
25376
+ candidate.to = to_attr;
25377
+ }
25378
+ if (Object.keys(candidate).length > 0) {
25379
+ filter = candidate;
25380
+ }
25381
+ }
25382
+ return {
25383
+ event,
25384
+ handler_name,
25385
+ inline_body,
25386
+ once: once_attr,
25387
+ name,
25388
+ filter
25389
+ };
25390
+ }
25391
+ /**
25392
+ * Optional global registry that `<jssm-on>` (and, later, `<jssm-hook>`)
25393
+ * consult first when resolving a `handler="name"` attribute. Consumers
25394
+ * register named handlers here in a strict-CSP environment where a stray
25395
+ * `globalThis[name]` isn't acceptable. Falls through to `globalThis[name]`
25396
+ * if the registry has no entry.
25397
+ *
25398
+ * Intentionally a `Map<string, Function>` rather than a class with methods,
25399
+ * so consumers can use any of `.get`, `.set`, `.delete`, `.clear` directly
25400
+ * without a thin wrapper API.
25401
+ */
25402
+ const jssm_handler_registry = new Map();
25403
+ /**
25404
+ * Resolve a named handler from the registry, then from `globalThis`.
25405
+ * Throws if neither lookup finds a function — earlier failure here is
25406
+ * better than a delayed "is not a function" at first event delivery.
25407
+ *
25408
+ * @param name - The handler name as supplied by `handler="..."`.
25409
+ * @returns The resolved function.
25410
+ * @throws If no function is registered under `name`.
25411
+ */
25412
+ function resolve_named_handler(name) {
25413
+ const from_registry = jssm_handler_registry.get(name);
25414
+ if (typeof from_registry === 'function') {
25415
+ return from_registry;
25416
+ }
25417
+ const from_global = globalThis[name];
25418
+ if (typeof from_global === 'function') {
25419
+ return from_global;
25420
+ }
25421
+ throw new Error(`<jssm-on>: handler "${name}" not found in registry or globalThis`);
25422
+ }
25423
+ /**
25424
+ * Compile an inline-body string into a handler function whose single
25425
+ * parameter is `e` (the event detail object). Uses the same dynamic
25426
+ * `Function(...)` constructor that browsers use internally for inline
25427
+ * event-handler attributes such as `<a onclick="...">`; the input here
25428
+ * is consumer-authored markup, never network data, so the surface is
25429
+ * exactly that of an inline event-handler attribute and the same CSP
25430
+ * caveats apply (strict CSP without `'unsafe-eval'` blocks it). A
25431
+ * `//# sourceURL=jssm-on:N` pragma is appended so devtools stack traces
25432
+ * point at a meaningful name.
25433
+ *
25434
+ * @param body - The inline JS body (function body, not full function).
25435
+ * @param source_id - A short identifier for the sourceURL pragma.
25436
+ * @returns The compiled handler.
25437
+ */
25438
+ function compile_inline_body(body, source_id) {
25439
+ const wrapped = `${body}\n//# sourceURL=jssm-on:${source_id}`;
25440
+ // The Function constructor is intentional here — see the docblock above
25441
+ // for the rationale and the CSP caveat. Equivalent to how browsers wire
25442
+ // up inline event handlers; the input is consumer-authored markup.
25443
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
25444
+ return new Function('e', wrapped); // skipcq: JS-0086
25445
+ }
25275
25446
  /**
25276
25447
  * Resolve a `<jssm-instance>`'s FSL source from the three legal channels:
25277
25448
  * the `fsl=""` attribute, a child `<script type="text/fsl">`, and the
@@ -25387,6 +25558,15 @@ class JssmInstance extends i {
25387
25558
  * connection.
25388
25559
  */
25389
25560
  this._machine = undefined;
25561
+ /**
25562
+ * Unsubscribe callbacks for every `machine.on(...)` / `machine.once(...)`
25563
+ * subscription installed from a `<jssm-on>` child during
25564
+ * `connectedCallback`. Walked in `disconnectedCallback` so a removed
25565
+ * `<jssm-instance>` doesn't leave dangling handlers on its (now-orphan)
25566
+ * machine. Array (insertion order) rather than Set so cleanup order is
25567
+ * deterministic and easy to reason about.
25568
+ */
25569
+ this._on_unsubscribes = [];
25390
25570
  /**
25391
25571
  * Per-instance registry of named hook handlers consulted before
25392
25572
  * `globalThis` when resolving `<jssm-hook handler="name">`.
@@ -25493,9 +25673,52 @@ class JssmInstance extends i {
25493
25673
  // and dispatch DOM CustomEvents from this element.
25494
25674
  // #641: <jssm-hook> declarative discovery.
25495
25675
  this._install_declarative_hooks();
25496
- // TODO #643: <jssm-on> discovery happens here.
25497
- this._discover_jssm_actions();
25676
+ // #643: <jssm-on> declarative event observation.
25677
+ this._install_jssm_on_children();
25498
25678
  // TODO #645: <jssm-bind> discovery happens here.
25679
+ // #640: <jssm-action> DOM event → machine action wiring.
25680
+ this._discover_jssm_actions();
25681
+ }
25682
+ /**
25683
+ * Discover direct-child `<jssm-on>` elements and install their
25684
+ * subscriptions on the owned machine. Per #643:
25685
+ *
25686
+ * - Direct children only (`:scope > jssm-on`). Deeper nesting is the
25687
+ * responsibility of a future MutationObserver-driven v2.
25688
+ * - Each `<jssm-on>` is parsed by {@link parse_jssm_on_element}, which
25689
+ * enforces the form / event-name / filter rules.
25690
+ * - Handlers come from {@link resolve_named_handler} (form A) or
25691
+ * {@link compile_inline_body} (form B), and the result is installed
25692
+ * via `machine.on(...)` or `machine.once(...)` depending on the
25693
+ * element's `once` attribute.
25694
+ * - Every returned unsubscribe is tracked in {@link _on_unsubscribes}
25695
+ * so {@link disconnectedCallback} can release them all.
25696
+ *
25697
+ * Called once from `connectedCallback` after the machine has been
25698
+ * constructed. Any error thrown by parsing or resolution propagates
25699
+ * out so it surfaces via jsdom's error event (matching the rest of
25700
+ * `<jssm-instance>`'s "fail loud at connect" policy).
25701
+ */
25702
+ _install_jssm_on_children() {
25703
+ const machine = this._machine;
25704
+ const on_nodes = this.querySelectorAll(':scope > jssm-on');
25705
+ let index = 0;
25706
+ for (const el of Array.from(on_nodes)) {
25707
+ index += 1;
25708
+ const parsed = parse_jssm_on_element(el);
25709
+ const handler = parsed.handler_name !== undefined
25710
+ ? resolve_named_handler(parsed.handler_name)
25711
+ : compile_inline_body(parsed.inline_body, String(index));
25712
+ // Argument shape: machine.on(name, handler) when no filter, or
25713
+ // machine.on(name, filter, handler) when filtered. Same for once.
25714
+ // `as any` collapses the per-event detail typing — the WC is a
25715
+ // schema-erased entry point and the type-safety belongs upstream.
25716
+ const subscribe = parsed.once ? machine.once.bind(machine) : machine.on.bind(machine);
25717
+ const unsubscribe = parsed.filter === undefined
25718
+ ? subscribe(parsed.event, handler)
25719
+ : subscribe(parsed.event, parsed.filter, handler);
25720
+ this._on_unsubscribes.push(unsubscribe);
25721
+ }
25499
25722
  }
25500
25723
  /**
25501
25724
  * Discover every direct-child `<jssm-hook>` element and install each
@@ -25503,17 +25726,6 @@ class JssmInstance extends i {
25503
25726
  * adapter that lets user code write `m.data = ...` and return `false` to
25504
25727
  * cancel — see {@link make_hook_proxy} and the issue (#641) doc-comment
25505
25728
  * for the full contract.
25506
- *
25507
- * Direct children only (the `:scope > jssm-hook` selector) so that nested
25508
- * `<jssm-instance>` elements don't have their child hooks installed on
25509
- * the outer machine.
25510
- *
25511
- * Tracks every installed descriptor in `_installed_hooks` so that
25512
- * `disconnectedCallback` can remove them on detach.
25513
- *
25514
- * @throws Error - On a malformed `<jssm-hook>` (mutual-exclusion violation,
25515
- * unknown kind, unresolved name, or jssm's own missing-key
25516
- * errors from `set_hook`).
25517
25729
  */
25518
25730
  _install_declarative_hooks() {
25519
25731
  const machine = this._machine;
@@ -25523,35 +25735,26 @@ class JssmInstance extends i {
25523
25735
  const spec = parse_hook_element(el, debug_id, this.registry);
25524
25736
  const wrapped = wrap_user_handler(spec, machine);
25525
25737
  const desc = build_hook_descriptor(spec, wrapped);
25526
- // `desc` is shaped from runtime kind discrimination; jssm's typed
25527
- // `HookDescription` is a static discriminated union that TS can't
25528
- // unify with our runtime-built object, hence the cast.
25529
25738
  machine.set_hook(desc);
25530
25739
  this._installed_hooks.push(desc);
25531
25740
  }
25532
25741
  }
25533
25742
  /**
25534
25743
  * Prefix used in synthetic `//# sourceURL=jssm-hook:<prefix><n>` annotations
25535
- * for inline-body hooks compiled by this element. Includes the element's
25536
- * `id` when present so multi-instance pages can tell sources apart in
25537
- * devtools.
25744
+ * for inline-body hooks compiled by this element.
25538
25745
  */
25539
25746
  _hook_id_prefix() {
25540
25747
  const host_id = this.getAttribute('id');
25541
25748
  return host_id !== null && host_id.length > 0 ? `${host_id}-` : '';
25542
25749
  }
25543
25750
  /**
25544
- * Lifecycle hook. Removes every hook this WC installed via
25545
- * `<jssm-hook>` discovery so the underlying machine doesn't leak handlers
25546
- * when the element detaches. Called automatically by the browser; the
25547
- * machine itself is not destroyed (consumers can reuse it).
25548
- *
25549
- * Future tickets #638/#643/#645 will extend this to drop other
25550
- * subscriptions / listeners installed by their respective tags.
25751
+ * Lifecycle hook. Cleans up everything the WC installed at connect: hook
25752
+ * registrations from `<jssm-hook>`, event subscriptions from `<jssm-on>`,
25753
+ * and DOM listeners from `<jssm-action>` / `data-jssm-action`.
25551
25754
  */
25552
25755
  disconnectedCallback() {
25553
25756
  super.disconnectedCallback();
25554
- // TODO #638: unsubscribe from machine.on(...) handlers.
25757
+ // TODO #638: unsubscribe from any direct machine.on(...) handlers added by host.
25555
25758
  // #641: remove installed hooks.
25556
25759
  if (this._machine !== undefined) {
25557
25760
  const machine = this._machine;
@@ -25560,10 +25763,16 @@ class JssmInstance extends i {
25560
25763
  }
25561
25764
  }
25562
25765
  this._installed_hooks = [];
25563
- // TODO #643/#645: remove installed listeners / bindings.
25564
- // Remove every listener installed during `<jssm-action>` / `data-jssm-action`
25565
- // discovery. Using the original handler reference ensures `removeEventListener`
25566
- // actually unbinds — anonymous re-creation here would silently leak.
25766
+ // #643: release every subscription installed from a <jssm-on> child.
25767
+ for (const off of this._on_unsubscribes) {
25768
+ try {
25769
+ off();
25770
+ }
25771
+ catch ( /* swallow — cleanup must not throw past us */_a) { /* swallow — cleanup must not throw past us */ }
25772
+ }
25773
+ this._on_unsubscribes = [];
25774
+ // TODO #645: remove installed bindings.
25775
+ // #640: remove DOM listeners installed via <jssm-action> / data-jssm-action.
25567
25776
  for (const entry of this._action_listeners) {
25568
25777
  entry.target.removeEventListener(entry.event, entry.handler);
25569
25778
  }
@@ -25571,30 +25780,12 @@ class JssmInstance extends i {
25571
25780
  }
25572
25781
  /**
25573
25782
  * Wire DOM events to machine actions, using the two declarative forms from
25574
- * issue #640:
25575
- *
25576
- * 1. Inline attribute form: every descendant of the host carrying a
25577
- * `data-jssm-action="<name>"` attribute receives a listener on the
25578
- * event named by `data-jssm-event` (default `click`).
25579
- * 2. Dedicated tag form: each direct `<jssm-action>` child of the host
25580
- * supplies a CSS `selector` (scoped to the host), an `action`, and an
25581
- * optional `event` (default `click`); every matching descendant
25582
- * receives a listener configured by the tag's attributes.
25583
- *
25584
- * Both forms support optional `from-state` guards (dispatch only when the
25585
- * machine's current state matches), `from-property` data extraction (pass
25586
- * the source element's named property as the action's data argument), and
25587
- * `prevent-default` / `stop-propagation` modifiers.
25588
- *
25589
- * Every installed listener is recorded in {@link _action_listeners} so
25590
- * {@link disconnectedCallback} can detach them cleanly.
25783
+ * issue #640. Both forms support optional `from-state` guards,
25784
+ * `from-property` data extraction, and `prevent-default` /
25785
+ * `stop-propagation` modifiers.
25591
25786
  */
25592
25787
  _discover_jssm_actions() {
25593
25788
  var _a, _b, _c, _d;
25594
- // Inline attribute form: `[data-jssm-action]` descendants. Per the
25595
- // ticket, we scan the host's light DOM (not the shadow tree, which is
25596
- // owned by us) and skip any element living inside a `<jssm-action>` tag
25597
- // — those tags are pure data markup, never the source of an event.
25598
25789
  const inline_targets = Array.from(this.querySelectorAll('[data-jssm-action]')).filter(el => el.closest('jssm-action') === null);
25599
25790
  for (const el of inline_targets) {
25600
25791
  this._install_action_listener({
@@ -25607,16 +25798,11 @@ class JssmInstance extends i {
25607
25798
  stop_propagation: 'jssmStopPropagation' in el.dataset,
25608
25799
  });
25609
25800
  }
25610
- // Dedicated tag form: direct `<jssm-action>` children of the host.
25611
- // `:scope >` keeps a nested `<jssm-instance>`'s actions from being
25612
- // claimed by an outer host.
25613
25801
  const tags = this.querySelectorAll(':scope > jssm-action');
25614
25802
  for (const tag of Array.from(tags)) {
25615
25803
  const selector = tag.getAttribute('selector');
25616
25804
  const action_name = tag.getAttribute('action');
25617
25805
  if (selector === null || action_name === null) {
25618
- // Required attrs missing — skip, but don't throw: a malformed tag
25619
- // shouldn't break the rest of the host's wiring.
25620
25806
  continue;
25621
25807
  }
25622
25808
  const event_name = (_b = tag.getAttribute('event')) !== null && _b !== void 0 ? _b : 'click';
@@ -25640,18 +25826,7 @@ class JssmInstance extends i {
25640
25826
  }
25641
25827
  /**
25642
25828
  * Attach one DOM listener that translates a DOM event into a
25643
- * `machine.action(...)` call, honoring the configured modifiers. The
25644
- * listener is recorded in {@link _action_listeners} so it can be removed
25645
- * on disconnect.
25646
- *
25647
- * @param config - Listener configuration.
25648
- * @param config.source - Element to attach the listener to.
25649
- * @param config.event_name - DOM event to listen for.
25650
- * @param config.action_name - Action to dispatch on the machine.
25651
- * @param config.from_state - If set, only fire when `machine.state() === from_state`.
25652
- * @param config.from_property - If set, pass `source[from_property]` as the action's data argument.
25653
- * @param config.prevent_default - If true, call `e.preventDefault()` before checking the guard.
25654
- * @param config.stop_propagation - If true, call `e.stopPropagation()` before checking the guard.
25829
+ * `machine.action(...)` call, honoring the configured modifiers.
25655
25830
  */
25656
25831
  _install_action_listener(config) {
25657
25832
  const handler = (e) => {
@@ -25661,7 +25836,6 @@ class JssmInstance extends i {
25661
25836
  if (config.stop_propagation) {
25662
25837
  e.stopPropagation();
25663
25838
  }
25664
- // Guard: skip dispatch when the machine isn't in the required state.
25665
25839
  if (config.from_state !== undefined && this.state() !== config.from_state) {
25666
25840
  return;
25667
25841
  }
package/dist/cdn/viz.js CHANGED
@@ -21352,7 +21352,7 @@ var constants = /*#__PURE__*/Object.freeze({
21352
21352
  * Useful for runtime diagnostics and for embedding in serialized machine
21353
21353
  * snapshots so that deserializers can detect version-skew.
21354
21354
  */
21355
- const version = "5.138.0";
21355
+ const version = "5.139.0";
21356
21356
 
21357
21357
  // whargarbl lots of these return arrays could/should be sets
21358
21358
  const { state_name_chars, state_name_first_chars, action_label_chars } = constants;