@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.
@@ -3535,6 +3535,7 @@ const updateActions = ({
3535
3535
 
3536
3536
  const actionToRunPrivateProperties =
3537
3537
  getActionPrivateProperties(actionToPrerunOrRun);
3538
+ actionToRunPrivateProperties.answeredAsync = false;
3538
3539
  let performRunResult;
3539
3540
  try {
3540
3541
  performRunResult = actionToRunPrivateProperties.performRun({
@@ -3559,6 +3560,7 @@ const updateActions = ({
3559
3560
  activationWeakSet.add(actionToPrerunOrRun);
3560
3561
 
3561
3562
  if (performRunResult && typeof performRunResult.then === "function") {
3563
+ actionToRunPrivateProperties.answeredAsync = true;
3562
3564
  actionPromiseMap.set(actionToPrerunOrRun, performRunResult);
3563
3565
  allThenableArray.push(performRunResult);
3564
3566
  hasAsync = true;
@@ -3588,7 +3590,9 @@ const updateActions = ({
3588
3590
 
3589
3591
  // Execute promotions (prerun -> run requested)
3590
3592
  for (const actionToPromote of willPromoteSet) {
3591
- actionToPromote.isPrerunSignal.value = false;
3593
+ writeActionState(() => {
3594
+ actionToPromote.isPrerunSignal.value = false;
3595
+ });
3592
3596
  }
3593
3597
  }
3594
3598
 
@@ -3597,13 +3601,21 @@ const updateActions = ({
3597
3601
  // that IS carrying it. `await action.run()` then means the same thing to
3598
3602
  // every caller: the second one waits for the first one's request and reads
3599
3603
  // the same data, instead of being handed `undefined` before the network has
3600
- // said anything. See the run() JSDoc.
3604
+ // said anything. It answers in the tense of that run: the answer of a run
3605
+ // that answered with a promise comes back as one, so `run().catch()` holds
3606
+ // whatever state the instance is in. See the run() JSDoc.
3601
3607
  {
3602
3608
  for (const actionToJoin of willJoinSet) {
3603
3609
  const runningPromise = actionPromiseMap.get(actionToJoin);
3604
3610
  if (runningPromise) {
3605
3611
  hasAsync = true;
3606
3612
  resultArray.push({ type: "async", promise: runningPromise });
3613
+ } else if (getActionPrivateProperties(actionToJoin).answeredAsync) {
3614
+ hasAsync = true;
3615
+ resultArray.push({
3616
+ type: "async",
3617
+ promise: Promise.resolve(actionToJoin.dataSignal.peek()),
3618
+ });
3607
3619
  } else {
3608
3620
  resultArray.push({
3609
3621
  type: "sync",
@@ -3809,6 +3821,10 @@ const createAction = (callback, rootOptions = {}) => {
3809
3821
  * the one that asked first or the one that arrived while the request was
3810
3822
  * out. Use `rerun()` to force a fresh run ("refresh", "check now", any
3811
3823
  * explicit user intent to go back to the network).
3824
+ *
3825
+ * An asynchronous callback answers with a promise, joined or not:
3826
+ * `ACTION.run().catch(…)` holds in every state. A synchronous one answers
3827
+ * synchronously, and throws its failure.
3812
3828
  */
3813
3829
  const run = (options) => {
3814
3830
  action.debug(`${action}.run(${stringifyForDisplay(options)})`);
@@ -4267,7 +4283,9 @@ const createAction = (callback, rootOptions = {}) => {
4267
4283
  // another run's, or idle.
4268
4284
  return;
4269
4285
  }
4270
- runningStateSignal.value = ABORTED;
4286
+ writeActionState(() => {
4287
+ runningStateSignal.value = ABORTED;
4288
+ });
4271
4289
  internalAbortController.abort(abortReason);
4272
4290
  actionAbortMap.delete(action);
4273
4291
  if (isPrerun && (globalAbortSignal.aborted || abortSignal.aborted)) {
@@ -4291,7 +4309,7 @@ const createAction = (callback, rootOptions = {}) => {
4291
4309
 
4292
4310
  actionAbortMap.set(action, abort);
4293
4311
 
4294
- batch(() => {
4312
+ writeActionState(() => {
4295
4313
  if (valueSignal.peek() === undefined) {
4296
4314
  // An action saying nothing of its own may still have a value the
4297
4315
  // page kept from the previous document (see kept_values.js).
@@ -4370,7 +4388,7 @@ const createAction = (callback, rootOptions = {}) => {
4370
4388
  * before the UI attempts to render the now-missing resource.
4371
4389
  */
4372
4390
 
4373
- batch(() => {
4391
+ writeActionState(() => {
4374
4392
  const value = resultToValue
4375
4393
  ? resultToValue(runResult, action)
4376
4394
  : runResult;
@@ -4399,7 +4417,9 @@ const createAction = (callback, rootOptions = {}) => {
4399
4417
  error === internalAbortSignal.reason) ||
4400
4418
  error.name === "AbortError";
4401
4419
  if (isAbort) {
4402
- runningStateSignal.value = ABORTED;
4420
+ writeActionState(() => {
4421
+ runningStateSignal.value = ABORTED;
4422
+ });
4403
4423
  if (isPrerun && abortSignal.aborted) {
4404
4424
  prerunProtectionRegistry.unprotect(action);
4405
4425
  }
@@ -4410,7 +4430,7 @@ const createAction = (callback, rootOptions = {}) => {
4410
4430
  if (isPrerun) {
4411
4431
  markErrorAsPrerunFailure(error);
4412
4432
  }
4413
- batch(() => {
4433
+ writeActionState(() => {
4414
4434
  errorSignal.value = error;
4415
4435
  runningStateSignal.value = FAILED;
4416
4436
  onError?.(error, { event, action, args });
@@ -4502,7 +4522,7 @@ const createAction = (callback, rootOptions = {}) => {
4502
4522
  }
4503
4523
 
4504
4524
  actionPromiseMap.delete(action);
4505
- batch(() => {
4525
+ writeActionState(() => {
4506
4526
  if (!willRunOrPrerun) {
4507
4527
  errorSignal.value = undefined;
4508
4528
  valueSignal.value = valueInitial;
@@ -4520,6 +4540,9 @@ const createAction = (callback, rootOptions = {}) => {
4520
4540
  provisionalValue,
4521
4541
  inheritData,
4522
4542
  keepsAnswers: keptAnswerRegistry !== null,
4543
+ // Whether the last run answered with a promise, written by
4544
+ // updateActions: the tense a run joining this instance answers in.
4545
+ answeredAsync: false,
4523
4546
 
4524
4547
  performRun,
4525
4548
  performReset,
@@ -5003,6 +5026,35 @@ const isPlainObject$2 = (obj) => {
5003
5026
  );
5004
5027
  };
5005
5028
 
5029
+ // Every state write of a run (start, end, failure, abort, reset), batched. At
5030
+ // the end of the batch preact runs the effects those writes woke up, anywhere
5031
+ // in the app, and rethrows the first one that threw: from there it would fail
5032
+ // this run, or leave it RUNNING, or stop the update half way. That throw is
5033
+ // the effect's, so it is reported on its own, as action_error_report.js
5034
+ // reports an error (thrown from a timer), and the action carries on. What
5035
+ // `write` throws (resultToValue, onComplete…) is the run's, and goes on.
5036
+ const writeActionState = (write) => {
5037
+ let writeThrew = false;
5038
+ let writeError;
5039
+ try {
5040
+ batch(() => {
5041
+ try {
5042
+ write();
5043
+ } catch (e) {
5044
+ writeThrew = true;
5045
+ writeError = e;
5046
+ }
5047
+ });
5048
+ } catch (effectError) {
5049
+ setTimeout(() => {
5050
+ throw effectError;
5051
+ });
5052
+ }
5053
+ if (writeThrew) {
5054
+ throw writeError;
5055
+ }
5056
+ };
5057
+
5006
5058
  /**
5007
5059
  * Reactively runs an action whenever the params derived from signals change.
5008
5060
  *
@@ -27535,7 +27587,11 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
27535
27587
  * (docs/resource.md).
27536
27588
  */
27537
27589
  const upsertAnswer = (...args) => upsertWith(args, true);
27538
- const drop = (...args) => {
27590
+ // Batched like upsertWith: a removal observer may drop rows of another
27591
+ // store (a `.one()` declared with `onDelete: "cascade"`), and an effect
27592
+ // reading both runs once, never between the child leaving and its parents.
27593
+ const drop = (...args) => batch(() => dropNow(...args));
27594
+ const dropNow = (...args) => {
27539
27595
  const removedItemArray = [];
27540
27596
  const triggerRemovedMutations = () => {
27541
27597
  if (removedItemArray.length === 0) {
@@ -27570,6 +27626,7 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
27570
27626
  hasFound = true;
27571
27627
  idToRemoveSet.delete(existingItemId);
27572
27628
  idRemovedArray.push(existingItemId);
27629
+ removedItemArray.push(existingItem);
27573
27630
  } else {
27574
27631
  arrayWithoutDroppedItems.push(existingItem);
27575
27632
  }
@@ -29629,6 +29686,15 @@ const createResource = (
29629
29686
  * The property is reactive: updating the child item anywhere propagates immediately.
29630
29687
  * The child resource exists independently — it is not owned by, nor deleted with, the parent.
29631
29688
  *
29689
+ * The other way round is declared with `onDelete`. Left out, a parent outlives its
29690
+ * child: once the child leaves its store, the property reads an empty object (truthy,
29691
+ * `valueOf()` is `null`, no fields) until the parent is read again — a game whose
29692
+ * organizer is deleted is still a game. `onDelete: "cascade"` is for a parent that is
29693
+ * nothing without its child (an address-book row, the person in it): when the child
29694
+ * leaves its store, the parents pointing at it leave theirs in the same write, and
29695
+ * with them every list holding them. Which one a relation needs is decided in
29696
+ * docs/resource.md, "When the child is deleted".
29697
+ *
29632
29698
  * Use it when the child is a first-class entity with its own store, shared across
29633
29699
  * parents (a user referenced by many games). When the child only exists inside its
29634
29700
  * owner, use `.scopedOne()` instead.
@@ -29648,7 +29714,12 @@ const createResource = (
29648
29714
  *
29649
29715
  * @param {string} propertyName - property holding the child on each parent item
29650
29716
  * @param {Object} childResource - the independent resource created by `resource()`
29651
- * @param {Object} [restCallbacks] - `{ rerunOn, dependencies, GET, PUT, DELETE }`
29717
+ * @param {Object} [restCallbacks] - `{ rerunOn, dependencies, onDelete, GET, PUT, DELETE }`
29718
+ * @param {"cascade"} [restCallbacks.onDelete] - `"cascade"`: a parent leaves its store
29719
+ * when its child leaves the child's (the child's `DELETE`, `DELETE_MANY`, a write
29720
+ * named with verb `DELETE`, a cascade of its own). Not the relation's own `DELETE`,
29721
+ * which unlinks: the property becomes `null` and the parent stays. Throws on a
29722
+ * relation declared on a scoped child.
29652
29723
  * @see docs/resource.md
29653
29724
  */
29654
29725
  stateFacade.one = (
@@ -29657,6 +29728,7 @@ const createResource = (
29657
29728
  {
29658
29729
  rerunOn: oneRerunOn,
29659
29730
  dependencies: oneDependencies,
29731
+ onDelete,
29660
29732
 
29661
29733
  GET,
29662
29734
  PUT,
@@ -29664,16 +29736,54 @@ const createResource = (
29664
29736
  } = {},
29665
29737
  ) => {
29666
29738
  const declarationSite = getDeclarationSite();
29739
+ const relationLabel = `${name}.one("${propertyName}")`;
29667
29740
  if (ownerName) {
29668
29741
  refuseCallbacksNamingScopedItem(
29669
- `${name}.one("${propertyName}")`,
29742
+ relationLabel,
29670
29743
  { GET, PUT, DELETE },
29671
29744
  { name, ownerName, propertyName, declarationSite },
29672
29745
  );
29673
29746
  }
29747
+ if (onDelete !== undefined && onDelete !== "cascade") {
29748
+ throw new TypeError(
29749
+ `${relationLabel}: onDelete must be "cascade" or left out, got ${onDelete}.
29750
+ ${relationLabel} source location: ${declarationSite}`,
29751
+ );
29752
+ }
29753
+ const cascade = onDelete === "cascade";
29754
+ if (cascade && ownerName) {
29755
+ // A scoped item lives in its owner's collection, not in a store this
29756
+ // relation could drop it from.
29757
+ throw new Error(
29758
+ `${relationLabel} cannot declare onDelete: "cascade": a "${name}" item exists only inside its "${ownerName}", and leaves through the "${ownerName}" callbacks.
29759
+ Read the "${ownerName}" again after the "${childResource.name}" is deleted (dependencies).
29760
+ ${relationLabel} source location: ${declarationSite}`,
29761
+ );
29762
+ }
29674
29763
  const childName = `${name}.${propertyName}`;
29675
29764
  const childIdKey = childResource.idKey;
29676
29765
  const childStore = childResource.store;
29766
+ // The id each parent points at, carried by the parent row (copies of a row
29767
+ // keep it, a spread does not): a removal is matched against the parents
29768
+ // the store holds at that moment, under the ids they have then.
29769
+ const childIdSymbol = cascade ? Symbol(`${childName} id`) : undefined;
29770
+ if (cascade) {
29771
+ childStore.observeRemovals((removedChildArray) => {
29772
+ const removedChildIdSet = new Set();
29773
+ for (const removedChild of removedChildArray) {
29774
+ removedChildIdSet.add(removedChild[childIdKey]);
29775
+ }
29776
+ const orphanIdArray = [];
29777
+ for (const parentItem of store.arraySignal.peek()) {
29778
+ if (removedChildIdSet.has(parentItem[childIdSymbol])) {
29779
+ orphanIdArray.push(parentItem[idKey]);
29780
+ }
29781
+ }
29782
+ if (orphanIdArray.length > 0) {
29783
+ store.drop(orphanIdArray);
29784
+ }
29785
+ });
29786
+ }
29677
29787
  addItemSetup((item, answer) => {
29678
29788
  const childItemIdSignal = signal();
29679
29789
  const updateChildItemId = (value, answer) => {
@@ -29728,6 +29838,11 @@ const createResource = (
29728
29838
  get: () => childItemFacadeSignal.value,
29729
29839
  set: updateChildItemId,
29730
29840
  });
29841
+ if (cascade) {
29842
+ Object.defineProperty(item, childIdSymbol, {
29843
+ get: () => childItemIdSignal.peek(),
29844
+ });
29845
+ }
29731
29846
  debug$2(
29732
29847
  `setup ${item}.${propertyName} is one "${childResource.name}" (current value: ${childItemSignal.peek()})`,
29733
29848
  );
@@ -38153,10 +38268,12 @@ const barsOutside = (areaElement) => {
38153
38268
  };
38154
38269
 
38155
38270
  // Room around what a bar's picture holds for what paints past every box — a
38156
- // shadow, the blur of a wall — which no API measures: a filter cuts whatever
38271
+ // shadow, the bar's own filter — which no API measures: a filter cuts whatever
38157
38272
  // falls outside its region.
38158
38273
  const WALL_FILTER_INK_ROOM = 100;
38159
38274
  const SVG_NS = "http://www.w3.org/2000/svg";
38275
+ // A blur() in a computed filter, its radius in px.
38276
+ const BLUR_FUNCTION_REGEXP = /blur\(([^)]*)\)/g;
38160
38277
 
38161
38278
  // The bar's picture dimmed, not a rectangle over it: each wall's colour is
38162
38279
  // flooded atop the bar's own pixels (Porter-Duff source-atop, which no blend
@@ -38184,11 +38301,17 @@ const dimBarPicture = (bar, sources) => {
38184
38301
  const ownFilter = getComputedStyle(bar).filter;
38185
38302
  const chain = ownFilter === "none" ? [] : [ownFilter];
38186
38303
  for (const source of sources) {
38187
- const backdropFilter = getComputedStyle(source)
38188
- .getPropertyValue("--backdrop-filter")
38189
- .trim();
38190
- if (backdropFilter && backdropFilter !== "none") {
38191
- chain.push(backdropFilter);
38304
+ const backdropFilter = readBackdropFilter(source);
38305
+ if (backdropFilter !== "none") {
38306
+ // Blurred alone, the bar would fade to transparent over its edges and
38307
+ // show the live document through them: its blur stays inside its shape.
38308
+ chain.push(
38309
+ backdropFilter.replace(
38310
+ BLUR_FUNCTION_REGEXP,
38311
+ (_, radius) =>
38312
+ `url(#${createShapeBlurFilter(parseFloat(radius), region).id})`,
38313
+ ),
38314
+ );
38192
38315
  }
38193
38316
  chain.push(`url(#${createWallFilter(source, region).id})`);
38194
38317
  }
@@ -38197,7 +38320,59 @@ const dimBarPicture = (bar, sources) => {
38197
38320
  dimmedBars.push(bar);
38198
38321
  };
38199
38322
 
38323
+ // The wall's backdrop-filter as computed, so a blur radius written in em or
38324
+ // rem reads in px, as the wall resolves it where it paints.
38325
+ const readBackdropFilter = (source) => {
38326
+ const sourceStyle = getComputedStyle(source);
38327
+ const backdropFilter = sourceStyle
38328
+ .getPropertyValue("--backdrop-filter")
38329
+ .trim();
38330
+ if (!backdropFilter || backdropFilter === "none") {
38331
+ return "none";
38332
+ }
38333
+ const probe = document.createElement("div");
38334
+ probe.style.fontSize = sourceStyle.fontSize;
38335
+ probe.style.filter = backdropFilter;
38336
+ document.body.append(probe);
38337
+ const filter = getComputedStyle(probe).filter;
38338
+ probe.remove();
38339
+ return filter;
38340
+ };
38341
+
38200
38342
  const createWallFilter = (source, region) => {
38343
+ const filter = createFilter(region);
38344
+ const flood = document.createElementNS(SVG_NS, "feFlood");
38345
+ flood.setAttribute("flood-color", readBackdropColor(source));
38346
+ const composite = document.createElementNS(SVG_NS, "feComposite");
38347
+ composite.setAttribute("in2", "SourceGraphic");
38348
+ composite.setAttribute("operator", "atop");
38349
+ filter.append(flood, composite);
38350
+ return filter;
38351
+ };
38352
+
38353
+ // Colour kept, alpha set to 1.
38354
+ const OPAQUE_MATRIX = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 1";
38355
+
38356
+ // The wall's blur kept inside the bar. At rest the window is blurred as one
38357
+ // image; a CSS blur() on the bar sees only the bar, so its edges go half
38358
+ // transparent and its colour spreads past its box. Made opaque, each pixel is
38359
+ // the mean of the bar's own pixels around it (feColorMatrix works on
38360
+ // unpremultiplied colours), then cut back to the bar's own alpha.
38361
+ const createShapeBlurFilter = (stdDeviation, region) => {
38362
+ const filter = createFilter(region);
38363
+ const blur = document.createElementNS(SVG_NS, "feGaussianBlur");
38364
+ blur.setAttribute("stdDeviation", stdDeviation);
38365
+ const opaque = document.createElementNS(SVG_NS, "feColorMatrix");
38366
+ opaque.setAttribute("type", "matrix");
38367
+ opaque.setAttribute("values", OPAQUE_MATRIX);
38368
+ const clip = document.createElementNS(SVG_NS, "feComposite");
38369
+ clip.setAttribute("in2", "SourceAlpha");
38370
+ clip.setAttribute("operator", "in");
38371
+ filter.append(blur, opaque, clip);
38372
+ return filter;
38373
+ };
38374
+
38375
+ const createFilter = (region) => {
38201
38376
  if (!wallFilterSvg) {
38202
38377
  wallFilterSvg = document.createElementNS(SVG_NS, "svg");
38203
38378
  wallFilterSvg.setAttribute(WALL_FILTERS_ATTRIBUTE, "");
@@ -38207,18 +38382,13 @@ const createWallFilter = (source, region) => {
38207
38382
  wallFilterCount++;
38208
38383
  const filter = document.createElementNS(SVG_NS, "filter");
38209
38384
  filter.id = `navi-transition-wall-${wallFilterCount}`;
38210
- // linearRGB, the default, floods another colour than the wall's.
38385
+ // linearRGB, the default, floods another colour than the wall's and blurs
38386
+ // in another space than its backdrop-filter.
38211
38387
  filter.setAttribute("color-interpolation-filters", "sRGB");
38212
38388
  filter.setAttribute("x", region.x);
38213
38389
  filter.setAttribute("y", region.y);
38214
38390
  filter.setAttribute("width", region.width);
38215
38391
  filter.setAttribute("height", region.height);
38216
- const flood = document.createElementNS(SVG_NS, "feFlood");
38217
- flood.setAttribute("flood-color", readBackdropColor(source));
38218
- const composite = document.createElementNS(SVG_NS, "feComposite");
38219
- composite.setAttribute("in2", "SourceGraphic");
38220
- composite.setAttribute("operator", "atop");
38221
- filter.append(flood, composite);
38222
38392
  wallFilterSvg.appendChild(filter);
38223
38393
  return filter;
38224
38394
  };