@jsenv/navi 0.30.24 → 0.30.26

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.
@@ -3752,6 +3752,7 @@ ${lines.join("\n")}`);
3752
3752
 
3753
3753
  const actionToRunPrivateProperties =
3754
3754
  getActionPrivateProperties(actionToPrerunOrRun);
3755
+ actionToRunPrivateProperties.answeredAsync = false;
3755
3756
  let performRunResult;
3756
3757
  try {
3757
3758
  performRunResult = actionToRunPrivateProperties.performRun({
@@ -3776,6 +3777,7 @@ ${lines.join("\n")}`);
3776
3777
  activationWeakSet.add(actionToPrerunOrRun);
3777
3778
 
3778
3779
  if (performRunResult && typeof performRunResult.then === "function") {
3780
+ actionToRunPrivateProperties.answeredAsync = true;
3779
3781
  actionPromiseMap.set(actionToPrerunOrRun, performRunResult);
3780
3782
  allThenableArray.push(performRunResult);
3781
3783
  hasAsync = true;
@@ -3805,7 +3807,9 @@ ${lines.join("\n")}`);
3805
3807
 
3806
3808
  // Execute promotions (prerun -> run requested)
3807
3809
  for (const actionToPromote of willPromoteSet) {
3808
- actionToPromote.isPrerunSignal.value = false;
3810
+ writeActionState(() => {
3811
+ actionToPromote.isPrerunSignal.value = false;
3812
+ });
3809
3813
  }
3810
3814
  }
3811
3815
 
@@ -3814,13 +3818,21 @@ ${lines.join("\n")}`);
3814
3818
  // that IS carrying it. `await action.run()` then means the same thing to
3815
3819
  // every caller: the second one waits for the first one's request and reads
3816
3820
  // the same data, instead of being handed `undefined` before the network has
3817
- // said anything. See the run() JSDoc.
3821
+ // said anything. It answers in the tense of that run: the answer of a run
3822
+ // that answered with a promise comes back as one, so `run().catch()` holds
3823
+ // whatever state the instance is in. See the run() JSDoc.
3818
3824
  {
3819
3825
  for (const actionToJoin of willJoinSet) {
3820
3826
  const runningPromise = actionPromiseMap.get(actionToJoin);
3821
3827
  if (runningPromise) {
3822
3828
  hasAsync = true;
3823
3829
  resultArray.push({ type: "async", promise: runningPromise });
3830
+ } else if (getActionPrivateProperties(actionToJoin).answeredAsync) {
3831
+ hasAsync = true;
3832
+ resultArray.push({
3833
+ type: "async",
3834
+ promise: Promise.resolve(actionToJoin.dataSignal.peek()),
3835
+ });
3824
3836
  } else {
3825
3837
  resultArray.push({
3826
3838
  type: "sync",
@@ -4065,6 +4077,10 @@ const createAction = (callback, rootOptions = {}) => {
4065
4077
  * the one that asked first or the one that arrived while the request was
4066
4078
  * out. Use `rerun()` to force a fresh run ("refresh", "check now", any
4067
4079
  * explicit user intent to go back to the network).
4080
+ *
4081
+ * An asynchronous callback answers with a promise, joined or not:
4082
+ * `ACTION.run().catch(…)` holds in every state. A synchronous one answers
4083
+ * synchronously, and throws its failure.
4068
4084
  */
4069
4085
  const run = (options) => {
4070
4086
  action.debug(`${action}.run(${stringifyForDisplay(options)})`);
@@ -4524,7 +4540,9 @@ const createAction = (callback, rootOptions = {}) => {
4524
4540
  // another run's, or idle.
4525
4541
  return;
4526
4542
  }
4527
- runningStateSignal.value = ABORTED;
4543
+ writeActionState(() => {
4544
+ runningStateSignal.value = ABORTED;
4545
+ });
4528
4546
  internalAbortController.abort(abortReason);
4529
4547
  actionAbortMap.delete(action);
4530
4548
  if (isPrerun && (globalAbortSignal.aborted || abortSignal.aborted)) {
@@ -4551,7 +4569,7 @@ const createAction = (callback, rootOptions = {}) => {
4551
4569
 
4552
4570
  actionAbortMap.set(action, abort);
4553
4571
 
4554
- batch(() => {
4572
+ writeActionState(() => {
4555
4573
  if (valueSignal.peek() === undefined) {
4556
4574
  // An action saying nothing of its own may still have a value the
4557
4575
  // page kept from the previous document (see kept_values.js).
@@ -4630,7 +4648,7 @@ const createAction = (callback, rootOptions = {}) => {
4630
4648
  * before the UI attempts to render the now-missing resource.
4631
4649
  */
4632
4650
 
4633
- batch(() => {
4651
+ writeActionState(() => {
4634
4652
  const value = resultToValue
4635
4653
  ? resultToValue(runResult, action)
4636
4654
  : runResult;
@@ -4662,7 +4680,9 @@ const createAction = (callback, rootOptions = {}) => {
4662
4680
  error === internalAbortSignal.reason) ||
4663
4681
  error.name === "AbortError";
4664
4682
  if (isAbort) {
4665
- runningStateSignal.value = ABORTED;
4683
+ writeActionState(() => {
4684
+ runningStateSignal.value = ABORTED;
4685
+ });
4666
4686
  if (isPrerun && abortSignal.aborted) {
4667
4687
  prerunProtectionRegistry.unprotect(action);
4668
4688
  }
@@ -4676,7 +4696,7 @@ const createAction = (callback, rootOptions = {}) => {
4676
4696
  if (isPrerun) {
4677
4697
  markErrorAsPrerunFailure(error);
4678
4698
  }
4679
- batch(() => {
4699
+ writeActionState(() => {
4680
4700
  errorSignal.value = error;
4681
4701
  runningStateSignal.value = FAILED;
4682
4702
  onError?.(error, { event, action, args });
@@ -4771,7 +4791,7 @@ const createAction = (callback, rootOptions = {}) => {
4771
4791
  }
4772
4792
 
4773
4793
  actionPromiseMap.delete(action);
4774
- batch(() => {
4794
+ writeActionState(() => {
4775
4795
  if (!willRunOrPrerun) {
4776
4796
  errorSignal.value = undefined;
4777
4797
  valueSignal.value = valueInitial;
@@ -4789,6 +4809,9 @@ const createAction = (callback, rootOptions = {}) => {
4789
4809
  provisionalValue,
4790
4810
  inheritData,
4791
4811
  keepsAnswers: keptAnswerRegistry !== null,
4812
+ // Whether the last run answered with a promise, written by
4813
+ // updateActions: the tense a run joining this instance answers in.
4814
+ answeredAsync: false,
4792
4815
 
4793
4816
  performRun,
4794
4817
  performReset,
@@ -5272,6 +5295,35 @@ const isPlainObject$2 = (obj) => {
5272
5295
  );
5273
5296
  };
5274
5297
 
5298
+ // Every state write of a run (start, end, failure, abort, reset), batched. At
5299
+ // the end of the batch preact runs the effects those writes woke up, anywhere
5300
+ // in the app, and rethrows the first one that threw: from there it would fail
5301
+ // this run, or leave it RUNNING, or stop the update half way. That throw is
5302
+ // the effect's, so it is reported on its own, as action_error_report.js
5303
+ // reports an error (thrown from a timer), and the action carries on. What
5304
+ // `write` throws (resultToValue, onComplete…) is the run's, and goes on.
5305
+ const writeActionState = (write) => {
5306
+ let writeThrew = false;
5307
+ let writeError;
5308
+ try {
5309
+ batch(() => {
5310
+ try {
5311
+ write();
5312
+ } catch (e) {
5313
+ writeThrew = true;
5314
+ writeError = e;
5315
+ }
5316
+ });
5317
+ } catch (effectError) {
5318
+ setTimeout(() => {
5319
+ throw effectError;
5320
+ });
5321
+ }
5322
+ if (writeThrew) {
5323
+ throw writeError;
5324
+ }
5325
+ };
5326
+
5275
5327
  /**
5276
5328
  * Reactively runs an action whenever the params derived from signals change.
5277
5329
  *
@@ -28608,7 +28660,11 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
28608
28660
  * (docs/resource.md).
28609
28661
  */
28610
28662
  const upsertAnswer = (...args) => upsertWith(args, true);
28611
- const drop = (...args) => {
28663
+ // Batched like upsertWith: a removal observer may drop rows of another
28664
+ // store (a `.one()` declared with `onDelete: "cascade"`), and an effect
28665
+ // reading both runs once, never between the child leaving and its parents.
28666
+ const drop = (...args) => batch(() => dropNow(...args));
28667
+ const dropNow = (...args) => {
28612
28668
  const removedItemArray = [];
28613
28669
  const triggerRemovedMutations = () => {
28614
28670
  if (removedItemArray.length === 0) {
@@ -28643,6 +28699,7 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
28643
28699
  hasFound = true;
28644
28700
  idToRemoveSet.delete(existingItemId);
28645
28701
  idRemovedArray.push(existingItemId);
28702
+ removedItemArray.push(existingItem);
28646
28703
  } else {
28647
28704
  arrayWithoutDroppedItems.push(existingItem);
28648
28705
  }
@@ -30753,6 +30810,15 @@ const createResource = (
30753
30810
  * The property is reactive: updating the child item anywhere propagates immediately.
30754
30811
  * The child resource exists independently — it is not owned by, nor deleted with, the parent.
30755
30812
  *
30813
+ * The other way round is declared with `onDelete`. Left out, a parent outlives its
30814
+ * child: once the child leaves its store, the property reads an empty object (truthy,
30815
+ * `valueOf()` is `null`, no fields) until the parent is read again — a game whose
30816
+ * organizer is deleted is still a game. `onDelete: "cascade"` is for a parent that is
30817
+ * nothing without its child (an address-book row, the person in it): when the child
30818
+ * leaves its store, the parents pointing at it leave theirs in the same write, and
30819
+ * with them every list holding them. Which one a relation needs is decided in
30820
+ * docs/resource.md, "When the child is deleted".
30821
+ *
30756
30822
  * Use it when the child is a first-class entity with its own store, shared across
30757
30823
  * parents (a user referenced by many games). When the child only exists inside its
30758
30824
  * owner, use `.scopedOne()` instead.
@@ -30772,7 +30838,12 @@ const createResource = (
30772
30838
  *
30773
30839
  * @param {string} propertyName - property holding the child on each parent item
30774
30840
  * @param {Object} childResource - the independent resource created by `resource()`
30775
- * @param {Object} [restCallbacks] - `{ rerunOn, dependencies, GET, PUT, DELETE }`
30841
+ * @param {Object} [restCallbacks] - `{ rerunOn, dependencies, onDelete, GET, PUT, DELETE }`
30842
+ * @param {"cascade"} [restCallbacks.onDelete] - `"cascade"`: a parent leaves its store
30843
+ * when its child leaves the child's (the child's `DELETE`, `DELETE_MANY`, a write
30844
+ * named with verb `DELETE`, a cascade of its own). Not the relation's own `DELETE`,
30845
+ * which unlinks: the property becomes `null` and the parent stays. Throws on a
30846
+ * relation declared on a scoped child.
30776
30847
  * @see docs/resource.md
30777
30848
  */
30778
30849
  stateFacade.one = (
@@ -30781,6 +30852,7 @@ const createResource = (
30781
30852
  {
30782
30853
  rerunOn: oneRerunOn,
30783
30854
  dependencies: oneDependencies,
30855
+ onDelete,
30784
30856
 
30785
30857
  GET,
30786
30858
  PUT,
@@ -30788,16 +30860,54 @@ const createResource = (
30788
30860
  } = {},
30789
30861
  ) => {
30790
30862
  const declarationSite = getDeclarationSite();
30863
+ const relationLabel = `${name}.one("${propertyName}")`;
30791
30864
  if (ownerName) {
30792
30865
  refuseCallbacksNamingScopedItem(
30793
- `${name}.one("${propertyName}")`,
30866
+ relationLabel,
30794
30867
  { GET, PUT, DELETE },
30795
30868
  { name, ownerName, propertyName, declarationSite },
30796
30869
  );
30797
30870
  }
30871
+ if (onDelete !== undefined && onDelete !== "cascade") {
30872
+ throw new TypeError(
30873
+ `${relationLabel}: onDelete must be "cascade" or left out, got ${onDelete}.
30874
+ ${relationLabel} source location: ${declarationSite}`,
30875
+ );
30876
+ }
30877
+ const cascade = onDelete === "cascade";
30878
+ if (cascade && ownerName) {
30879
+ // A scoped item lives in its owner's collection, not in a store this
30880
+ // relation could drop it from.
30881
+ throw new Error(
30882
+ `${relationLabel} cannot declare onDelete: "cascade": a "${name}" item exists only inside its "${ownerName}", and leaves through the "${ownerName}" callbacks.
30883
+ Read the "${ownerName}" again after the "${childResource.name}" is deleted (dependencies).
30884
+ ${relationLabel} source location: ${declarationSite}`,
30885
+ );
30886
+ }
30798
30887
  const childName = `${name}.${propertyName}`;
30799
30888
  const childIdKey = childResource.idKey;
30800
30889
  const childStore = childResource.store;
30890
+ // The id each parent points at, carried by the parent row (copies of a row
30891
+ // keep it, a spread does not): a removal is matched against the parents
30892
+ // the store holds at that moment, under the ids they have then.
30893
+ const childIdSymbol = cascade ? Symbol(`${childName} id`) : undefined;
30894
+ if (cascade) {
30895
+ childStore.observeRemovals((removedChildArray) => {
30896
+ const removedChildIdSet = new Set();
30897
+ for (const removedChild of removedChildArray) {
30898
+ removedChildIdSet.add(removedChild[childIdKey]);
30899
+ }
30900
+ const orphanIdArray = [];
30901
+ for (const parentItem of store.arraySignal.peek()) {
30902
+ if (removedChildIdSet.has(parentItem[childIdSymbol])) {
30903
+ orphanIdArray.push(parentItem[idKey]);
30904
+ }
30905
+ }
30906
+ if (orphanIdArray.length > 0) {
30907
+ store.drop(orphanIdArray);
30908
+ }
30909
+ });
30910
+ }
30801
30911
  addItemSetup((item, answer) => {
30802
30912
  const childItemIdSignal = signal();
30803
30913
  const updateChildItemId = (value, answer) => {
@@ -30852,6 +30962,11 @@ const createResource = (
30852
30962
  get: () => childItemFacadeSignal.value,
30853
30963
  set: updateChildItemId,
30854
30964
  });
30965
+ if (cascade) {
30966
+ Object.defineProperty(item, childIdSymbol, {
30967
+ get: () => childItemIdSignal.peek(),
30968
+ });
30969
+ }
30855
30970
  debug$2(
30856
30971
  `setup ${item}.${propertyName} is one "${childResource.name}" (current value: ${childItemSignal.peek()})`,
30857
30972
  );
@@ -39453,10 +39568,12 @@ const barsOutside = (areaElement) => {
39453
39568
  };
39454
39569
 
39455
39570
  // Room around what a bar's picture holds for what paints past every box — a
39456
- // shadow, the blur of a wall — which no API measures: a filter cuts whatever
39571
+ // shadow, the bar's own filter — which no API measures: a filter cuts whatever
39457
39572
  // falls outside its region.
39458
39573
  const WALL_FILTER_INK_ROOM = 100;
39459
39574
  const SVG_NS = "http://www.w3.org/2000/svg";
39575
+ // A blur() in a computed filter, its radius in px.
39576
+ const BLUR_FUNCTION_REGEXP = /blur\(([^)]*)\)/g;
39460
39577
 
39461
39578
  // The bar's picture dimmed, not a rectangle over it: each wall's colour is
39462
39579
  // flooded atop the bar's own pixels (Porter-Duff source-atop, which no blend
@@ -39484,11 +39601,17 @@ const dimBarPicture = (bar, sources) => {
39484
39601
  const ownFilter = getComputedStyle(bar).filter;
39485
39602
  const chain = ownFilter === "none" ? [] : [ownFilter];
39486
39603
  for (const source of sources) {
39487
- const backdropFilter = getComputedStyle(source)
39488
- .getPropertyValue("--backdrop-filter")
39489
- .trim();
39490
- if (backdropFilter && backdropFilter !== "none") {
39491
- chain.push(backdropFilter);
39604
+ const backdropFilter = readBackdropFilter(source);
39605
+ if (backdropFilter !== "none") {
39606
+ // Blurred alone, the bar would fade to transparent over its edges and
39607
+ // show the live document through them: its blur stays inside its shape.
39608
+ chain.push(
39609
+ backdropFilter.replace(
39610
+ BLUR_FUNCTION_REGEXP,
39611
+ (_, radius) =>
39612
+ `url(#${createShapeBlurFilter(parseFloat(radius), region).id})`,
39613
+ ),
39614
+ );
39492
39615
  }
39493
39616
  chain.push(`url(#${createWallFilter(source, region).id})`);
39494
39617
  }
@@ -39497,7 +39620,59 @@ const dimBarPicture = (bar, sources) => {
39497
39620
  dimmedBars.push(bar);
39498
39621
  };
39499
39622
 
39623
+ // The wall's backdrop-filter as computed, so a blur radius written in em or
39624
+ // rem reads in px, as the wall resolves it where it paints.
39625
+ const readBackdropFilter = (source) => {
39626
+ const sourceStyle = getComputedStyle(source);
39627
+ const backdropFilter = sourceStyle
39628
+ .getPropertyValue("--backdrop-filter")
39629
+ .trim();
39630
+ if (!backdropFilter || backdropFilter === "none") {
39631
+ return "none";
39632
+ }
39633
+ const probe = document.createElement("div");
39634
+ probe.style.fontSize = sourceStyle.fontSize;
39635
+ probe.style.filter = backdropFilter;
39636
+ document.body.append(probe);
39637
+ const filter = getComputedStyle(probe).filter;
39638
+ probe.remove();
39639
+ return filter;
39640
+ };
39641
+
39500
39642
  const createWallFilter = (source, region) => {
39643
+ const filter = createFilter(region);
39644
+ const flood = document.createElementNS(SVG_NS, "feFlood");
39645
+ flood.setAttribute("flood-color", readBackdropColor(source));
39646
+ const composite = document.createElementNS(SVG_NS, "feComposite");
39647
+ composite.setAttribute("in2", "SourceGraphic");
39648
+ composite.setAttribute("operator", "atop");
39649
+ filter.append(flood, composite);
39650
+ return filter;
39651
+ };
39652
+
39653
+ // Colour kept, alpha set to 1.
39654
+ const OPAQUE_MATRIX = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 1";
39655
+
39656
+ // The wall's blur kept inside the bar. At rest the window is blurred as one
39657
+ // image; a CSS blur() on the bar sees only the bar, so its edges go half
39658
+ // transparent and its colour spreads past its box. Made opaque, each pixel is
39659
+ // the mean of the bar's own pixels around it (feColorMatrix works on
39660
+ // unpremultiplied colours), then cut back to the bar's own alpha.
39661
+ const createShapeBlurFilter = (stdDeviation, region) => {
39662
+ const filter = createFilter(region);
39663
+ const blur = document.createElementNS(SVG_NS, "feGaussianBlur");
39664
+ blur.setAttribute("stdDeviation", stdDeviation);
39665
+ const opaque = document.createElementNS(SVG_NS, "feColorMatrix");
39666
+ opaque.setAttribute("type", "matrix");
39667
+ opaque.setAttribute("values", OPAQUE_MATRIX);
39668
+ const clip = document.createElementNS(SVG_NS, "feComposite");
39669
+ clip.setAttribute("in2", "SourceAlpha");
39670
+ clip.setAttribute("operator", "in");
39671
+ filter.append(blur, opaque, clip);
39672
+ return filter;
39673
+ };
39674
+
39675
+ const createFilter = (region) => {
39501
39676
  if (!wallFilterSvg) {
39502
39677
  wallFilterSvg = document.createElementNS(SVG_NS, "svg");
39503
39678
  wallFilterSvg.setAttribute(WALL_FILTERS_ATTRIBUTE, "");
@@ -39507,18 +39682,13 @@ const createWallFilter = (source, region) => {
39507
39682
  wallFilterCount++;
39508
39683
  const filter = document.createElementNS(SVG_NS, "filter");
39509
39684
  filter.id = `navi-transition-wall-${wallFilterCount}`;
39510
- // linearRGB, the default, floods another colour than the wall's.
39685
+ // linearRGB, the default, floods another colour than the wall's and blurs
39686
+ // in another space than its backdrop-filter.
39511
39687
  filter.setAttribute("color-interpolation-filters", "sRGB");
39512
39688
  filter.setAttribute("x", region.x);
39513
39689
  filter.setAttribute("y", region.y);
39514
39690
  filter.setAttribute("width", region.width);
39515
39691
  filter.setAttribute("height", region.height);
39516
- const flood = document.createElementNS(SVG_NS, "feFlood");
39517
- flood.setAttribute("flood-color", readBackdropColor(source));
39518
- const composite = document.createElementNS(SVG_NS, "feComposite");
39519
- composite.setAttribute("in2", "SourceGraphic");
39520
- composite.setAttribute("operator", "atop");
39521
- filter.append(flood, composite);
39522
39692
  wallFilterSvg.appendChild(filter);
39523
39693
  return filter;
39524
39694
  };