@moku-labs/web 2.4.12 → 2.4.13

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.
@@ -1998,6 +1998,12 @@ interface DataState {
1998
1998
  lastWrite: DataWriteSummary | null;
1999
1999
  /** Per-path fetched data, cached after the first `at(path)` (browser). */
2000
2000
  cache: Map<string, unknown>;
2001
+ /**
2002
+ * Per-path fetches still in flight (browser). A second `at(path)` awaits the
2003
+ * same promise instead of starting another request. The entry is removed when
2004
+ * the fetch settles, on success and on failure, so a failure is never cached.
2005
+ */
2006
+ pending: Map<string, Promise<unknown | null>>;
2001
2007
  }
2002
2008
  /**
2003
2009
  * Public API mounted at `app.data` — the agnostic data provider. `write()` is the
package/dist/browser.mjs CHANGED
@@ -2367,6 +2367,30 @@ async function loadJson(pathOrUrl) {
2367
2367
  * the isomorphic `loadJson` (whose Node branch is itself lazy).
2368
2368
  */
2369
2369
  /**
2370
+ * Fetches one path's data, caches it on success, and always clears the path's
2371
+ * in-flight entry. Never rejects: a failed fetch or JSON parse resolves `null`
2372
+ * and caches nothing, so a later `at(path)` retries.
2373
+ *
2374
+ * @param ctx - The data plugin context.
2375
+ * @param path - The page URL path (e.g. `/en/hello/`).
2376
+ * @returns The page's raw data, or `null` on failure.
2377
+ * @example
2378
+ * ```ts
2379
+ * const raw = await fetchAndCache(ctx, "/en/hello/");
2380
+ * ```
2381
+ */
2382
+ async function fetchAndCache(ctx, path) {
2383
+ try {
2384
+ const data = await loadJson(`${ctx.config.baseUrl}${dataSuffix(path)}`);
2385
+ ctx.state.cache.set(path, data);
2386
+ return data;
2387
+ } catch {
2388
+ return null;
2389
+ } finally {
2390
+ ctx.state.pending.delete(path);
2391
+ }
2392
+ }
2393
+ /**
2370
2394
  * Builds the data provider — the agnostic bridge. `write()` is the Node persist
2371
2395
  * side; `at()` is the browser read side; `urlFor`/`fileFor` are the pure
2372
2396
  * convention. No `onStart`/`onStop` (holds no long-lived resource).
@@ -2384,6 +2408,7 @@ function dataApi(ctx) {
2384
2408
  return {
2385
2409
  /**
2386
2410
  * READ (browser) — fetch (and cache) the persisted data for a page path.
2411
+ * Concurrent calls for one path share one in-flight request.
2387
2412
  * Returns the raw JSON as `unknown`, which the route uses directly as `ctx.data`
2388
2413
  * (no route `.parse()`); returns `null` if the fetch or JSON parse fails (so
2389
2414
  * `spa` can fall back to HTML).
@@ -2397,13 +2422,11 @@ function dataApi(ctx) {
2397
2422
  */
2398
2423
  async at(path) {
2399
2424
  if (ctx.state.cache.has(path)) return ctx.state.cache.get(path);
2400
- try {
2401
- const data = await loadJson(`${ctx.config.baseUrl}${dataSuffix(path)}`);
2402
- ctx.state.cache.set(path, data);
2403
- return data;
2404
- } catch {
2405
- return null;
2406
- }
2425
+ const inFlight = ctx.state.pending.get(path);
2426
+ if (inFlight) return inFlight;
2427
+ const request = fetchAndCache(ctx, path);
2428
+ ctx.state.pending.set(path, request);
2429
+ return request;
2407
2430
  },
2408
2431
  /**
2409
2432
  * WRITE (Node) — persist one JSON file per entry, keyed by page path. Called by
@@ -2472,13 +2495,13 @@ const defaultDataConfig = {
2472
2495
  //#region src/plugins/data/state.ts
2473
2496
  /**
2474
2497
  * Creates initial data state: a null `lastWrite` slot (populated by the Node
2475
- * `write()` side) and an empty `cache` (populated lazily by the browser `at(path)`
2476
- * side on first fetch).
2498
+ * `write()` side), an empty `cache` (populated lazily by the browser `at(path)`
2499
+ * side on first fetch) and an empty `pending` map of in-flight fetches.
2477
2500
  *
2478
2501
  * @param _ctx - Minimal context with global and config.
2479
2502
  * @param _ctx.global - Global framework configuration.
2480
2503
  * @param _ctx.config - Resolved plugin configuration.
2481
- * @returns Fresh data state with no recorded write and an empty per-path cache.
2504
+ * @returns Fresh data state with no recorded write and an empty per-path cache and no pending fetch.
2482
2505
  * @example
2483
2506
  * ```ts
2484
2507
  * const state = createDataState({ global: {}, config });
@@ -2487,7 +2510,8 @@ const defaultDataConfig = {
2487
2510
  function createDataState(_ctx) {
2488
2511
  return {
2489
2512
  lastWrite: null,
2490
- cache: /* @__PURE__ */ new Map()
2513
+ cache: /* @__PURE__ */ new Map(),
2514
+ pending: /* @__PURE__ */ new Map()
2491
2515
  };
2492
2516
  }
2493
2517
  //#endregion
package/dist/index.cjs CHANGED
@@ -4625,6 +4625,30 @@ async function loadJson(pathOrUrl) {
4625
4625
  * the isomorphic `loadJson` (whose Node branch is itself lazy).
4626
4626
  */
4627
4627
  /**
4628
+ * Fetches one path's data, caches it on success, and always clears the path's
4629
+ * in-flight entry. Never rejects: a failed fetch or JSON parse resolves `null`
4630
+ * and caches nothing, so a later `at(path)` retries.
4631
+ *
4632
+ * @param ctx - The data plugin context.
4633
+ * @param path - The page URL path (e.g. `/en/hello/`).
4634
+ * @returns The page's raw data, or `null` on failure.
4635
+ * @example
4636
+ * ```ts
4637
+ * const raw = await fetchAndCache(ctx, "/en/hello/");
4638
+ * ```
4639
+ */
4640
+ async function fetchAndCache(ctx, path) {
4641
+ try {
4642
+ const data = await loadJson(`${ctx.config.baseUrl}${require_convention.dataSuffix(path)}`);
4643
+ ctx.state.cache.set(path, data);
4644
+ return data;
4645
+ } catch {
4646
+ return null;
4647
+ } finally {
4648
+ ctx.state.pending.delete(path);
4649
+ }
4650
+ }
4651
+ /**
4628
4652
  * Builds the data provider — the agnostic bridge. `write()` is the Node persist
4629
4653
  * side; `at()` is the browser read side; `urlFor`/`fileFor` are the pure
4630
4654
  * convention. No `onStart`/`onStop` (holds no long-lived resource).
@@ -4642,6 +4666,7 @@ function dataApi(ctx) {
4642
4666
  return {
4643
4667
  /**
4644
4668
  * READ (browser) — fetch (and cache) the persisted data for a page path.
4669
+ * Concurrent calls for one path share one in-flight request.
4645
4670
  * Returns the raw JSON as `unknown`, which the route uses directly as `ctx.data`
4646
4671
  * (no route `.parse()`); returns `null` if the fetch or JSON parse fails (so
4647
4672
  * `spa` can fall back to HTML).
@@ -4655,13 +4680,11 @@ function dataApi(ctx) {
4655
4680
  */
4656
4681
  async at(path) {
4657
4682
  if (ctx.state.cache.has(path)) return ctx.state.cache.get(path);
4658
- try {
4659
- const data = await loadJson(`${ctx.config.baseUrl}${require_convention.dataSuffix(path)}`);
4660
- ctx.state.cache.set(path, data);
4661
- return data;
4662
- } catch {
4663
- return null;
4664
- }
4683
+ const inFlight = ctx.state.pending.get(path);
4684
+ if (inFlight) return inFlight;
4685
+ const request = fetchAndCache(ctx, path);
4686
+ ctx.state.pending.set(path, request);
4687
+ return request;
4665
4688
  },
4666
4689
  /**
4667
4690
  * WRITE (Node) — persist one JSON file per entry, keyed by page path. Called by
@@ -4730,13 +4753,13 @@ const defaultDataConfig = {
4730
4753
  //#region src/plugins/data/state.ts
4731
4754
  /**
4732
4755
  * Creates initial data state: a null `lastWrite` slot (populated by the Node
4733
- * `write()` side) and an empty `cache` (populated lazily by the browser `at(path)`
4734
- * side on first fetch).
4756
+ * `write()` side), an empty `cache` (populated lazily by the browser `at(path)`
4757
+ * side on first fetch) and an empty `pending` map of in-flight fetches.
4735
4758
  *
4736
4759
  * @param _ctx - Minimal context with global and config.
4737
4760
  * @param _ctx.global - Global framework configuration.
4738
4761
  * @param _ctx.config - Resolved plugin configuration.
4739
- * @returns Fresh data state with no recorded write and an empty per-path cache.
4762
+ * @returns Fresh data state with no recorded write and an empty per-path cache and no pending fetch.
4740
4763
  * @example
4741
4764
  * ```ts
4742
4765
  * const state = createDataState({ global: {}, config });
@@ -4745,7 +4768,8 @@ const defaultDataConfig = {
4745
4768
  function createDataState(_ctx) {
4746
4769
  return {
4747
4770
  lastWrite: null,
4748
- cache: /* @__PURE__ */ new Map()
4771
+ cache: /* @__PURE__ */ new Map(),
4772
+ pending: /* @__PURE__ */ new Map()
4749
4773
  };
4750
4774
  }
4751
4775
  //#endregion
package/dist/index.d.cts CHANGED
@@ -3467,6 +3467,12 @@ interface DataState {
3467
3467
  lastWrite: DataWriteSummary | null;
3468
3468
  /** Per-path fetched data, cached after the first `at(path)` (browser). */
3469
3469
  cache: Map<string, unknown>;
3470
+ /**
3471
+ * Per-path fetches still in flight (browser). A second `at(path)` awaits the
3472
+ * same promise instead of starting another request. The entry is removed when
3473
+ * the fetch settles, on success and on failure, so a failure is never cached.
3474
+ */
3475
+ pending: Map<string, Promise<unknown | null>>;
3470
3476
  }
3471
3477
  /**
3472
3478
  * Public API mounted at `app.data` — the agnostic data provider. `write()` is the
package/dist/index.d.mts CHANGED
@@ -3465,6 +3465,12 @@ interface DataState {
3465
3465
  lastWrite: DataWriteSummary | null;
3466
3466
  /** Per-path fetched data, cached after the first `at(path)` (browser). */
3467
3467
  cache: Map<string, unknown>;
3468
+ /**
3469
+ * Per-path fetches still in flight (browser). A second `at(path)` awaits the
3470
+ * same promise instead of starting another request. The entry is removed when
3471
+ * the fetch settles, on success and on failure, so a failure is never cached.
3472
+ */
3473
+ pending: Map<string, Promise<unknown | null>>;
3468
3474
  }
3469
3475
  /**
3470
3476
  * Public API mounted at `app.data` — the agnostic data provider. `write()` is the
package/dist/index.mjs CHANGED
@@ -4581,6 +4581,30 @@ async function loadJson(pathOrUrl) {
4581
4581
  * the isomorphic `loadJson` (whose Node branch is itself lazy).
4582
4582
  */
4583
4583
  /**
4584
+ * Fetches one path's data, caches it on success, and always clears the path's
4585
+ * in-flight entry. Never rejects: a failed fetch or JSON parse resolves `null`
4586
+ * and caches nothing, so a later `at(path)` retries.
4587
+ *
4588
+ * @param ctx - The data plugin context.
4589
+ * @param path - The page URL path (e.g. `/en/hello/`).
4590
+ * @returns The page's raw data, or `null` on failure.
4591
+ * @example
4592
+ * ```ts
4593
+ * const raw = await fetchAndCache(ctx, "/en/hello/");
4594
+ * ```
4595
+ */
4596
+ async function fetchAndCache(ctx, path) {
4597
+ try {
4598
+ const data = await loadJson(`${ctx.config.baseUrl}${dataSuffix(path)}`);
4599
+ ctx.state.cache.set(path, data);
4600
+ return data;
4601
+ } catch {
4602
+ return null;
4603
+ } finally {
4604
+ ctx.state.pending.delete(path);
4605
+ }
4606
+ }
4607
+ /**
4584
4608
  * Builds the data provider — the agnostic bridge. `write()` is the Node persist
4585
4609
  * side; `at()` is the browser read side; `urlFor`/`fileFor` are the pure
4586
4610
  * convention. No `onStart`/`onStop` (holds no long-lived resource).
@@ -4598,6 +4622,7 @@ function dataApi(ctx) {
4598
4622
  return {
4599
4623
  /**
4600
4624
  * READ (browser) — fetch (and cache) the persisted data for a page path.
4625
+ * Concurrent calls for one path share one in-flight request.
4601
4626
  * Returns the raw JSON as `unknown`, which the route uses directly as `ctx.data`
4602
4627
  * (no route `.parse()`); returns `null` if the fetch or JSON parse fails (so
4603
4628
  * `spa` can fall back to HTML).
@@ -4611,13 +4636,11 @@ function dataApi(ctx) {
4611
4636
  */
4612
4637
  async at(path) {
4613
4638
  if (ctx.state.cache.has(path)) return ctx.state.cache.get(path);
4614
- try {
4615
- const data = await loadJson(`${ctx.config.baseUrl}${dataSuffix(path)}`);
4616
- ctx.state.cache.set(path, data);
4617
- return data;
4618
- } catch {
4619
- return null;
4620
- }
4639
+ const inFlight = ctx.state.pending.get(path);
4640
+ if (inFlight) return inFlight;
4641
+ const request = fetchAndCache(ctx, path);
4642
+ ctx.state.pending.set(path, request);
4643
+ return request;
4621
4644
  },
4622
4645
  /**
4623
4646
  * WRITE (Node) — persist one JSON file per entry, keyed by page path. Called by
@@ -4686,13 +4709,13 @@ const defaultDataConfig = {
4686
4709
  //#region src/plugins/data/state.ts
4687
4710
  /**
4688
4711
  * Creates initial data state: a null `lastWrite` slot (populated by the Node
4689
- * `write()` side) and an empty `cache` (populated lazily by the browser `at(path)`
4690
- * side on first fetch).
4712
+ * `write()` side), an empty `cache` (populated lazily by the browser `at(path)`
4713
+ * side on first fetch) and an empty `pending` map of in-flight fetches.
4691
4714
  *
4692
4715
  * @param _ctx - Minimal context with global and config.
4693
4716
  * @param _ctx.global - Global framework configuration.
4694
4717
  * @param _ctx.config - Resolved plugin configuration.
4695
- * @returns Fresh data state with no recorded write and an empty per-path cache.
4718
+ * @returns Fresh data state with no recorded write and an empty per-path cache and no pending fetch.
4696
4719
  * @example
4697
4720
  * ```ts
4698
4721
  * const state = createDataState({ global: {}, config });
@@ -4701,7 +4724,8 @@ const defaultDataConfig = {
4701
4724
  function createDataState(_ctx) {
4702
4725
  return {
4703
4726
  lastWrite: null,
4704
- cache: /* @__PURE__ */ new Map()
4727
+ cache: /* @__PURE__ */ new Map(),
4728
+ pending: /* @__PURE__ */ new Map()
4705
4729
  };
4706
4730
  }
4707
4731
  //#endregion
package/package.json CHANGED
@@ -139,5 +139,5 @@
139
139
  "typescript-eslint": "8.58.0",
140
140
  "vitest": "4.0.18"
141
141
  },
142
- "version": "2.4.12"
142
+ "version": "2.4.13"
143
143
  }