cdk-nextjs 0.7.6 → 0.7.8

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.
Files changed (65) hide show
  1. package/.jsii +3 -3
  2. package/.jsii.tabl.json +1 -1
  3. package/README.md +12 -3
  4. package/assets/lambdas/post-deploy/post-deploy.lambda/index.js +31 -13
  5. package/docs/adapter-runtime.md +1 -1
  6. package/docs/caching-guide.md +11 -1
  7. package/docs/harness-coverage.md +154 -73
  8. package/lib/adapter/adapter.mjs +12 -3
  9. package/lib/adapter/aws-cache-store.d.ts +221 -3
  10. package/lib/adapter/aws-cache-store.js +481 -35
  11. package/lib/adapter/aws-cache-store.test.js +706 -3
  12. package/lib/adapter/cache-handler.d.ts +5 -3
  13. package/lib/adapter/cache-handler.js +16 -10
  14. package/lib/adapter/cache-handler.mjs +517 -49
  15. package/lib/adapter/cache-handler.test.js +44 -1
  16. package/lib/adapter/cache-utils.d.ts +22 -0
  17. package/lib/adapter/cache-utils.js +44 -1
  18. package/lib/adapter/function-groups.js +17 -4
  19. package/lib/adapter/function-groups.test.js +33 -1
  20. package/lib/adapter/memory-cache-handler.d.ts +5 -1
  21. package/lib/adapter/memory-cache-handler.js +6 -2
  22. package/lib/adapter/s3-cache-handler.d.ts +12 -1
  23. package/lib/adapter/s3-cache-handler.js +30 -8
  24. package/lib/adapter/s3-cache-handler.test.js +84 -2
  25. package/lib/adapter/use-cache-common.d.ts +66 -1
  26. package/lib/adapter/use-cache-common.js +142 -19
  27. package/lib/adapter/use-cache-common.test.js +174 -1
  28. package/lib/adapter/use-cache-default-handler.js +36 -6
  29. package/lib/adapter/use-cache-default-handler.mjs +553 -47
  30. package/lib/adapter/use-cache-handlers.test.js +247 -7
  31. package/lib/adapter/use-cache-remote-handler.js +9 -7
  32. package/lib/adapter/use-cache-remote-handler.mjs +542 -45
  33. package/lib/lambdas/post-deploy/prune-revalidation-table.d.ts +2 -1
  34. package/lib/lambdas/post-deploy/prune-revalidation-table.js +35 -13
  35. package/lib/lambdas/post-deploy/prune-revalidation-table.test.js +19 -1
  36. package/lib/nextjs-api.js +1 -1
  37. package/lib/nextjs-build/nextjs-build.js +1 -1
  38. package/lib/nextjs-cache.js +1 -1
  39. package/lib/nextjs-compute/nextjs-containers.js +1 -1
  40. package/lib/nextjs-compute/nextjs-functions.js +1 -1
  41. package/lib/nextjs-distribution.js +1 -1
  42. package/lib/nextjs-post-deploy.js +1 -1
  43. package/lib/nextjs-static-assets.js +1 -1
  44. package/lib/root-constructs/nextjs-base-construct.js +1 -1
  45. package/lib/root-constructs/nextjs-global-containers.js +1 -1
  46. package/lib/root-constructs/nextjs-global-functions.js +1 -1
  47. package/lib/root-constructs/nextjs-regional-containers.js +1 -1
  48. package/lib/root-constructs/nextjs-regional-functions.js +1 -1
  49. package/lib/runtime/core.js +20 -4
  50. package/lib/runtime/core.test.js +17 -1
  51. package/lib/runtime/image-utils.d.ts +8 -9
  52. package/lib/runtime/image-utils.js +16 -1
  53. package/lib/runtime/image-utils.test.js +13 -1
  54. package/lib/runtime/image.js +9 -3
  55. package/lib/runtime/lambda.mjs +66 -18
  56. package/lib/runtime/server.mjs +66 -18
  57. package/lib/runtime/tag-manifest.d.ts +40 -0
  58. package/lib/runtime/tag-manifest.js +59 -0
  59. package/lib/runtime/tag-manifest.test.d.ts +1 -0
  60. package/lib/runtime/tag-manifest.test.js +47 -0
  61. package/package.json +3 -3
  62. package/scripts/e2e-deploy.sh +7 -2
  63. package/scripts/e2e-harness/README.md +72 -35
  64. package/scripts/e2e-harness/screen.mjs +148 -7
  65. package/scripts/playwright-install-deps.sh +40 -0
@@ -46265,6 +46265,7 @@ var import_debug6 = __toESM(require_src());
46265
46265
  // src/adapter/aws-cache-store.ts
46266
46266
  var import_client_dynamodb = __toESM(require_dist_cjs20());
46267
46267
  var import_client_s3 = __toESM(require_dist_cjs16());
46268
+ import { AsyncLocalStorage } from "node:async_hooks";
46268
46269
  import { join as join5 } from "node:path";
46269
46270
 
46270
46271
  // src/adapter/cache-utils.ts
@@ -46323,6 +46324,19 @@ function sha256Hex(value) {
46323
46324
  return createHash6("sha256").update(value).digest("hex");
46324
46325
  }
46325
46326
 
46327
+ // src/runtime/tag-manifest.ts
46328
+ var OUTSIDE_REQUEST_SYMBOL = /* @__PURE__ */ Symbol.for(
46329
+ "cdk-nextjs.outside-request-context"
46330
+ );
46331
+ function outsideRequest(fn) {
46332
+ const global = globalThis;
46333
+ const run = global[OUTSIDE_REQUEST_SYMBOL];
46334
+ return run ? run(fn) : fn();
46335
+ }
46336
+ var TAG_MANIFEST_SYMBOL = /* @__PURE__ */ Symbol.for(
46337
+ "cdk-nextjs.use-cache.tag-manifest"
46338
+ );
46339
+
46326
46340
  // src/adapter/aws-cache-store.ts
46327
46341
  function resolveAwsCacheConfig() {
46328
46342
  return {
@@ -46390,6 +46404,7 @@ function markerOf(item) {
46390
46404
  }
46391
46405
  var BATCH_GET_MAX_KEYS = 100;
46392
46406
  var DEFAULT_TAG_REFRESH_MS = 1e3;
46407
+ var TAG_REFRESH_GRACE_MS = 1e3;
46393
46408
  function hashedTag(tag) {
46394
46409
  return `#${sha256Hex(tag)}`;
46395
46410
  }
@@ -46603,10 +46618,64 @@ var TrackedTagMarkers = class {
46603
46618
  * result rather than dropped.
46604
46619
  */
46605
46620
  pendingRows = /* @__PURE__ */ new Map();
46621
+ /**
46622
+ * Log rows for tags not tracked and not being read, merged by tag, oldest
46623
+ * first: what the log has said about them since {@link completeSince}. Taken
46624
+ * into a tag's marker when it is read or written.
46625
+ */
46626
+ logOnly = /* @__PURE__ */ new Map();
46627
+ /** See {@link completeSince}. */
46628
+ floor;
46629
+ /**
46630
+ * The future `expiredAt`s of markers dropped since, as `[from, to]`, in
46631
+ * order: once `from` is past, {@link completeSince} moves to `to`, and not
46632
+ * before, since only then does it expire anything. `from` and `to` are the
46633
+ * same until spans are merged for space ({@link laterFloor}).
46634
+ */
46635
+ laterFloors = [];
46636
+ /** The in-memory entry stores judged by {@link completeSince}. */
46637
+ entryStores = [];
46638
+ /** How many times {@link forget} ran: a read started before one is made again. */
46639
+ forgotten = 0;
46606
46640
  /** Tracked tags whose read failed, for the next refresh to read again. */
46607
46641
  unread = /* @__PURE__ */ new Set();
46642
+ /**
46643
+ * Set inside a page render {@link catchUp} let go, for that render alone.
46644
+ * See {@link behindInRender}.
46645
+ */
46646
+ caughtUpRender = new AsyncLocalStorage();
46647
+ /** When `sync` last scanned the entry stores ({@link oldestEntry}). */
46648
+ lastEntryScan = -Infinity;
46649
+ /**
46650
+ * No `logOnly` row is known by it or earlier, so a {@link pruneLogged}
46651
+ * against it or earlier drops nothing: a marker is only ever known by later,
46652
+ * as its `expiredAt` passes.
46653
+ */
46654
+ prunedAgainst = -Infinity;
46655
+ /** When the last refresh started, or was skipped. */
46608
46656
  lastRefresh = -Infinity;
46657
+ /**
46658
+ * The refresh in flight. One left open across a freeze comes back after the
46659
+ * thaw with what it read before, so {@link refresh} waits for it and then
46660
+ * for a fresh one, rather than the runtime waiting for it before the freeze:
46661
+ * that would bill every Lambda invocation for the query's tail.
46662
+ */
46609
46663
  refreshing;
46664
+ /**
46665
+ * When {@link refreshing} last showed the process running: a timer ticks
46666
+ * while it is in flight, and a tick that comes late, or none at all, means
46667
+ * the process was paused - frozen - since it started.
46668
+ */
46669
+ lastTick = -Infinity;
46670
+ /** Whether a tick came late since {@link refreshing} started. */
46671
+ pausedWhileRefreshing = false;
46672
+ ticker;
46673
+ canFreeze;
46674
+ /**
46675
+ * When the last refresh settled, either way, unless it spanned a pause: what
46676
+ * it knows is from then. See {@link behind}.
46677
+ */
46678
+ settledAt;
46610
46679
  /**
46611
46680
  * Where the next log query starts (`clock()`): the last successful one's
46612
46681
  * start, less {@link REVALIDATION_LOG_LOOKBACK_MS}.
@@ -46628,20 +46697,68 @@ var TrackedTagMarkers = class {
46628
46697
  this.random = options.random ?? Math.random;
46629
46698
  this.debug = options.debug ?? (() => {
46630
46699
  });
46700
+ this.canFreeze = options.canFreeze ?? false;
46631
46701
  const at = this.clock();
46632
46702
  this.cursor = at - REVALIDATION_LOG_LOOKBACK_MS;
46633
46703
  this.lastLogRead = at;
46704
+ this.settledAt = at;
46705
+ this.floor = markerClock();
46634
46706
  }
46635
46707
  /** `tag`'s marker as this instance knows it, if it tracks `tag`. */
46636
46708
  get(tag) {
46637
46709
  return this.tags.get(tag);
46638
46710
  }
46711
+ /**
46712
+ * Since when (`markerClock()`) this instance knows of every revalidation of
46713
+ * every tag, tracked or not: tracked ones from their markers and the log,
46714
+ * the others from the log alone (`logOnly`). So an entry created at or after
46715
+ * it can be judged by {@link state} and {@link expiration} without
46716
+ * {@link ensure} reading anything - which matters inside a `'use cache'`
46717
+ * lookup, where Next.js waits for the answer and a read pushes the entry out
46718
+ * of the static stage of a staged render.
46719
+ *
46720
+ * Starts at construction: the log is followed from before it. Moves to now
46721
+ * when the tracked markers are forgotten, and past the revalidations of
46722
+ * whatever else it lets go of - a marker evicted for space, a log row
46723
+ * pruned - which are no longer known: to its `expiredAt` only once that is
46724
+ * past, since a profile's `expire` can be a year out, and moving there at
46725
+ * once would turn the read-free path off for the instance's life.
46726
+ *
46727
+ * A row is pruned only once it is no later than every entry held, so that
46728
+ * costs those entries nothing. An entry stored afterwards with an older
46729
+ * `timestamp` - Next.js stamps the start of its generation, which can
46730
+ * outlast a lookback - is older than this too, and is read first.
46731
+ *
46732
+ * It does not cover a revalidation older than the log's first lookback
46733
+ * whose `expiredAt` is still to come, which would expire an entry created
46734
+ * after it. So the `'use cache'` handler reads an entry's tags in the
46735
+ * background as it stores it.
46736
+ */
46737
+ get completeSince() {
46738
+ const at = markerClock();
46739
+ while (this.laterFloors.length > 0 && this.laterFloors[0][0] <= at) {
46740
+ this.floor = Math.max(this.floor, this.laterFloors.shift()[1]);
46741
+ }
46742
+ return this.floor;
46743
+ }
46744
+ /**
46745
+ * Register an in-memory store whose entries are judged by
46746
+ * {@link completeSince}, by the `timestamp` of the oldest entry it holds:
46747
+ * `logOnly` keeps the rows that could still apply to one of them, and no
46748
+ * others. With none registered it keeps nothing. Asked with `scan` false,
46749
+ * `oldestTimestamp` may answer early, but never late, without a scan.
46750
+ */
46751
+ judgeEntriesOf(oldestTimestamp, holdsAny = () => oldestTimestamp(false) < Infinity) {
46752
+ this.entryStores.push({ oldestTimestamp, holdsAny });
46753
+ }
46639
46754
  /**
46640
46755
  * Track `tag` with `marker`: what this instance just wrote, which it knows
46641
46756
  * without reading it back.
46642
46757
  */
46643
46758
  set(tag, marker) {
46644
- this.remember(tag, marker);
46759
+ const logged = this.logOnly.get(tag);
46760
+ this.logOnly.delete(tag);
46761
+ this.remember(tag, mergeMarkers(marker, logged));
46645
46762
  this.known(tag, this.clock());
46646
46763
  }
46647
46764
  /**
@@ -46694,27 +46811,115 @@ var TrackedTagMarkers = class {
46694
46811
  const unread = [];
46695
46812
  for (const tag of new Set(tags)) {
46696
46813
  const inFlight = this.reading.get(tag);
46697
- if (inFlight) {
46698
- waits.push(inFlight);
46699
- } else if (this.tags.has(tag)) {
46814
+ if (this.tags.has(tag)) {
46700
46815
  this.touch(tag);
46816
+ } else if (inFlight) {
46817
+ waits.push(inFlight);
46701
46818
  } else {
46702
46819
  unread.push(tag);
46703
46820
  }
46704
46821
  }
46705
46822
  if (unread.length > 0) {
46706
- const read = this.readInto(unread).then(() => {
46707
- for (const tag of unread) {
46708
- this.reading.delete(tag);
46709
- }
46710
- });
46711
- for (const tag of unread) {
46712
- this.reading.set(tag, read);
46713
- }
46714
- waits.push(read);
46823
+ waits.push(this.startRead(unread));
46715
46824
  }
46716
46825
  await Promise.all(waits);
46717
46826
  }
46827
+ /**
46828
+ * {@link readInto} `tags`, as the read of each that whoever needs one of
46829
+ * them next waits on ({@link reading}). Never rejects: a failed read leaves
46830
+ * the tags tracked.
46831
+ */
46832
+ startRead(tags) {
46833
+ const read = this.readInto(tags).then(() => {
46834
+ for (const tag of tags) {
46835
+ this.reading.delete(tag);
46836
+ }
46837
+ });
46838
+ for (const tag of tags) {
46839
+ this.reading.set(tag, read);
46840
+ }
46841
+ return read;
46842
+ }
46843
+ /**
46844
+ * Whether the last refresh settled more than `refreshIntervalMs` plus
46845
+ * {@link TAG_REFRESH_GRACE_MS} ago - one that spanned a freeze counts from
46846
+ * when it started: after the instance sat idle or frozen - or always, with
46847
+ * a `refreshIntervalMs` of `0`. Answering from what it knows then could
46848
+ * serve an entry revalidated elsewhere since, however long ago that was, so
46849
+ * `refreshTags` waits for the refresh, and the runtime waits for it before
46850
+ * a page render starts ({@link catchUp}).
46851
+ *
46852
+ * From when it settled, not when it started, so a slow query the runtime
46853
+ * waited for before the render does not leave the instance behind inside
46854
+ * it. A failed one counts too, so one request does not wait for two: the
46855
+ * same "assume the entry is valid" a failed read answers with, until the
46856
+ * next refresh, at most the interval and the grace later.
46857
+ */
46858
+ get behind() {
46859
+ return this.log !== void 0 && // `0` asks before every check, so every check waits for the answer.
46860
+ (this.refreshIntervalMs === 0 || this.overdue(this.settledAt));
46861
+ }
46862
+ /**
46863
+ * {@link behind}, as `refreshTags` asks it: never inside a page's render
46864
+ * {@link catchUp} caught up, since the runtime settled it for that request
46865
+ * before the render started, and a wait inside a `'use cache'` lookup is
46866
+ * what it settled it to avoid - a render crossing the line partway through
46867
+ * would otherwise wait after all. So a revalidation elsewhere can go unseen
46868
+ * for a render's duration on top of the usual window.
46869
+ *
46870
+ * A route handler, which no catch-up precedes, waits at the plain line.
46871
+ */
46872
+ get behindInRender() {
46873
+ return this.refreshIntervalMs !== 0 && this.caughtUpRender.getStore() ? false : this.behind;
46874
+ }
46875
+ /**
46876
+ * Whether a registered store ({@link judgeEntriesOf}) holds any entry: one
46877
+ * a refresh could expire. With none there is nothing to wait for even when
46878
+ * the instance is {@link behind}, since whatever is stored from here on is
46879
+ * newer than what the log would say - which covers the first `'use cache'`
46880
+ * call of a process, which creates the handler inside the render.
46881
+ */
46882
+ get holdsEntries() {
46883
+ return this.entryStores.some((store) => store.holdsAny());
46884
+ }
46885
+ /**
46886
+ * {@link refresh}, waited for when the instance is {@link behind}. The
46887
+ * runtime calls it before handing a page request to Next.js, outside the
46888
+ * staged render whose static stage a wait inside a `'use cache'` lookup
46889
+ * would cut short. Only for the `'use cache'` handler's entries
46890
+ * ({@link judgeEntriesOf}): the other caches wait for the refresh where they
46891
+ * read anyway. Not with a `refreshIntervalMs` of `0` either, where
46892
+ * `refreshTags` asks again regardless. Then `render`, marked as caught up
46893
+ * for ({@link behindInRender}). Rejects only as `render` does.
46894
+ */
46895
+ async catchUp(render) {
46896
+ const log = (error2) => console.error("Error refreshing cache tags:", error2);
46897
+ if (this.refreshIntervalMs > 0 && this.behind) {
46898
+ if (!this.holdsEntries) {
46899
+ return render();
46900
+ }
46901
+ await this.refresh().catch(log);
46902
+ } else if (this.refreshIntervalMs > 0) {
46903
+ void this.refresh().catch(log);
46904
+ }
46905
+ return this.caughtUpRender.run(true, render);
46906
+ }
46907
+ /**
46908
+ * Run `fn` outside the request's context (`outsideRequest`), keeping only
46909
+ * whether the render was {@link catchUp caught up}: a cache handler method
46910
+ * called from inside a staged render, whose clock reads and AWS SDK calls
46911
+ * would otherwise end its static stage.
46912
+ */
46913
+ outsideRender(fn) {
46914
+ const caughtUp = this.caughtUpRender.getStore();
46915
+ return outsideRequest(
46916
+ () => caughtUp ? this.caughtUpRender.run(caughtUp, fn) : fn()
46917
+ );
46918
+ }
46919
+ /** Whether `at` is more than the interval and the grace ago. */
46920
+ overdue(at) {
46921
+ return this.clock() - at > this.refreshIntervalMs + TAG_REFRESH_GRACE_MS;
46922
+ }
46718
46923
  /**
46719
46924
  * Catch up on revalidations other instances ran, at most once per
46720
46925
  * `refreshIntervalMs`. See {@link TrackedTagMarkers} for the cost.
@@ -46725,6 +46930,9 @@ var TrackedTagMarkers = class {
46725
46930
  return;
46726
46931
  }
46727
46932
  if (this.refreshing) {
46933
+ if (this.behind && this.pausedSinceRefreshStarted()) {
46934
+ return this.refreshing.then(() => this.refresh());
46935
+ }
46728
46936
  return this.refreshing;
46729
46937
  }
46730
46938
  const at = this.clock();
@@ -46732,14 +46940,47 @@ var TrackedTagMarkers = class {
46732
46940
  return;
46733
46941
  }
46734
46942
  this.lastRefresh = at;
46735
- if (this.tags.size === 0) {
46943
+ if (this.tags.size === 0 && this.entryStores.length === 0) {
46736
46944
  return;
46737
46945
  }
46946
+ this.startTicking();
46738
46947
  this.refreshing = this.sync(at, log).finally(() => {
46948
+ this.settledAt = Math.max(
46949
+ this.settledAt,
46950
+ this.pausedSinceRefreshStarted() ? this.lastRefresh : this.clock()
46951
+ );
46952
+ this.stopTicking();
46739
46953
  this.refreshing = void 0;
46740
46954
  });
46741
46955
  return this.refreshing;
46742
46956
  }
46957
+ startTicking() {
46958
+ if (!this.canFreeze) {
46959
+ return;
46960
+ }
46961
+ this.lastTick = this.clock();
46962
+ this.pausedWhileRefreshing = false;
46963
+ this.ticker = setInterval(() => {
46964
+ const at = this.clock();
46965
+ if (at - this.lastTick > REFRESH_PAUSE_MS) {
46966
+ this.pausedWhileRefreshing = true;
46967
+ }
46968
+ this.lastTick = at;
46969
+ }, REFRESH_TICK_MS);
46970
+ this.ticker.unref?.();
46971
+ }
46972
+ stopTicking() {
46973
+ clearInterval(this.ticker);
46974
+ this.ticker = void 0;
46975
+ }
46976
+ /**
46977
+ * Whether the process was paused since {@link refreshing} started: a tick
46978
+ * came late, or the next one is overdue (after a thaw, a request can run
46979
+ * before the timer does).
46980
+ */
46981
+ pausedSinceRefreshStarted() {
46982
+ return this.canFreeze && (this.pausedWhileRefreshing || this.clock() - this.lastTick > REFRESH_PAUSE_MS);
46983
+ }
46743
46984
  /**
46744
46985
  * Record a revalidation of `tags`: here at once, and in the revalidation
46745
46986
  * table for every other instance - the marker row plus a log row per tag.
@@ -46772,7 +47013,7 @@ var TrackedTagMarkers = class {
46772
47013
  const at = markerClock();
46773
47014
  let state2 = "fresh";
46774
47015
  for (const tag of tags) {
46775
- const marker = this.get(tag);
47016
+ const marker = this.knownMarker(tag);
46776
47017
  if (!marker) {
46777
47018
  continue;
46778
47019
  }
@@ -46796,7 +47037,7 @@ var TrackedTagMarkers = class {
46796
47037
  const at = markerClock();
46797
47038
  let latest = 0;
46798
47039
  for (const tag of tags) {
46799
- const marker = this.get(tag);
47040
+ const marker = this.knownMarker(tag);
46800
47041
  if (!marker) {
46801
47042
  continue;
46802
47043
  }
@@ -46883,13 +47124,14 @@ var TrackedTagMarkers = class {
46883
47124
  );
46884
47125
  return;
46885
47126
  }
46886
- const [result] = await Promise.all([
46887
- log.query(this.cursor).catch((error2) => {
46888
- console.error("Error reading tag revalidation log:", error2);
46889
- return void 0;
46890
- }),
46891
- this.readInto(this.due(at))
46892
- ]);
47127
+ const due = this.due(at);
47128
+ if (due.length > 0) {
47129
+ void this.startRead(due);
47130
+ }
47131
+ const result = await log.query(this.cursor).catch((error2) => {
47132
+ console.error("Error reading tag revalidation log:", error2);
47133
+ return void 0;
47134
+ });
46893
47135
  if (!result) {
46894
47136
  return;
46895
47137
  }
@@ -46900,6 +47142,11 @@ var TrackedTagMarkers = class {
46900
47142
  );
46901
47143
  return;
46902
47144
  }
47145
+ const scan = at - this.lastEntryScan > ENTRY_SCAN_INTERVAL_MS;
47146
+ if (scan) {
47147
+ this.lastEntryScan = at;
47148
+ }
47149
+ const oldest = this.oldestEntry(scan);
46903
47150
  let applied = 0;
46904
47151
  for (const row of result.rows) {
46905
47152
  if (this.applied.has(row.sk)) {
@@ -46914,11 +47161,20 @@ var TrackedTagMarkers = class {
46914
47161
  mergeMarkers(this.pendingRows.get(row.tag), row.marker)
46915
47162
  );
46916
47163
  } else {
47164
+ this.rememberLogged(
47165
+ row.tag,
47166
+ mergeMarkers(this.logOnly.get(row.tag), row.marker),
47167
+ oldest
47168
+ );
46917
47169
  continue;
46918
47170
  }
46919
47171
  this.applied.set(row.sk, row.at);
46920
47172
  applied++;
46921
47173
  }
47174
+ if (oldest > this.prunedAgainst) {
47175
+ this.pruneLogged(oldest);
47176
+ }
47177
+ this.prunedAgainst = oldest;
46922
47178
  this.lastLogRead = at;
46923
47179
  this.advance(at - REVALIDATION_LOG_LOOKBACK_MS);
46924
47180
  this.debug(
@@ -46934,14 +47190,16 @@ var TrackedTagMarkers = class {
46934
47190
  const due = [];
46935
47191
  for (const tag of this.unread) {
46936
47192
  if (due.length >= BATCH_GET_MAX_KEYS) return due;
46937
- due.push(tag);
47193
+ if (!this.reading.has(tag)) {
47194
+ due.push(tag);
47195
+ }
46938
47196
  }
46939
47197
  const jitter = this.resyncIntervalMs * TAG_RESYNC_JITTER;
46940
47198
  for (const [tag, dueAt] of this.dueAt) {
46941
47199
  if (due.length >= BATCH_GET_MAX_KEYS || dueAt - jitter > at) {
46942
47200
  break;
46943
47201
  }
46944
- if (dueAt <= at && !this.unread.has(tag)) {
47202
+ if (dueAt <= at && !this.unread.has(tag) && !this.reading.has(tag)) {
46945
47203
  due.push(tag);
46946
47204
  }
46947
47205
  }
@@ -46954,10 +47212,19 @@ var TrackedTagMarkers = class {
46954
47212
  */
46955
47213
  forget(at, why) {
46956
47214
  this.debug(`forgetting ${this.tags.size} tracked tags: ${why}`);
47215
+ this.forgotten++;
47216
+ for (const marker of this.tags.values()) {
47217
+ this.raiseFloor(marker);
47218
+ }
47219
+ for (const marker of this.logOnly.values()) {
47220
+ this.raiseFloor(marker);
47221
+ }
46957
47222
  this.tags.clear();
46958
47223
  this.dueAt.clear();
46959
47224
  this.unread.clear();
46960
47225
  this.applied.clear();
47226
+ this.logOnly.clear();
47227
+ this.floor = Math.max(this.floor, markerClock());
46961
47228
  this.cursor = at - REVALIDATION_LOG_LOOKBACK_MS;
46962
47229
  this.lastLogRead = at;
46963
47230
  }
@@ -46983,13 +47250,17 @@ var TrackedTagMarkers = class {
46983
47250
  * always answered with, rather than a read on every request while DynamoDB
46984
47251
  * is unavailable.
46985
47252
  */
46986
- async readInto(tags) {
47253
+ async readInto(tags, again = false) {
46987
47254
  if (tags.length === 0) {
46988
47255
  return true;
46989
47256
  }
46990
47257
  const at = this.clock();
47258
+ const forgotten = this.forgotten;
46991
47259
  try {
46992
47260
  const read = await this.markers.read(tags);
47261
+ if (this.forgotten !== forgotten && !again) {
47262
+ return await this.readInto(tags, true);
47263
+ }
46993
47264
  for (const tag of tags) {
46994
47265
  this.unread.delete(tag);
46995
47266
  this.remember(
@@ -47005,6 +47276,9 @@ var TrackedTagMarkers = class {
47005
47276
  return true;
47006
47277
  } catch (error2) {
47007
47278
  console.error("Error reading tag markers:", error2);
47279
+ if (this.forgotten !== forgotten && !again) {
47280
+ return this.readInto(tags, true);
47281
+ }
47008
47282
  this.trackUnread(tags);
47009
47283
  for (const tag of tags) {
47010
47284
  this.unread.add(tag);
@@ -47018,8 +47292,119 @@ var TrackedTagMarkers = class {
47018
47292
  }
47019
47293
  takePendingRows(tag) {
47020
47294
  const pending = this.pendingRows.get(tag);
47295
+ const logged = this.logOnly.get(tag);
47021
47296
  this.pendingRows.delete(tag);
47022
- return pending;
47297
+ this.logOnly.delete(tag);
47298
+ return logged ? mergeMarkers(logged, pending) : pending;
47299
+ }
47300
+ /**
47301
+ * What this instance knows of `tag`: tracked, or from the log alone -
47302
+ * including rows that arrived while its first read is in flight.
47303
+ */
47304
+ knownMarker(tag) {
47305
+ const tracked = this.tags.get(tag);
47306
+ if (tracked) {
47307
+ return tracked;
47308
+ }
47309
+ const logged = this.logOnly.get(tag);
47310
+ const pending = this.pendingRows.get(tag);
47311
+ return logged || pending ? mergeMarkers(logged, pending) : void 0;
47312
+ }
47313
+ /**
47314
+ * Keep `tag`'s log rows in `logOnly`, unless they cannot apply to an entry
47315
+ * held, `oldest` or newer (see {@link pruneLogged}), as bounded as the
47316
+ * tracked tags. Either way a row let go of takes what it knew with it, so
47317
+ * `completeSince` moves past it.
47318
+ */
47319
+ rememberLogged(tag, marker, oldest) {
47320
+ this.logOnly.delete(tag);
47321
+ if (knownBy(marker, markerClock()) <= oldest) {
47322
+ this.raiseFloor(marker);
47323
+ return;
47324
+ }
47325
+ this.logOnly.set(tag, marker);
47326
+ for (const [first, dropped] of this.logOnly) {
47327
+ if (this.logOnly.size <= this.maxTrackedTags) {
47328
+ break;
47329
+ }
47330
+ this.logOnly.delete(first);
47331
+ this.raiseFloor(dropped);
47332
+ }
47333
+ }
47334
+ /**
47335
+ * Drop the `logOnly` rows that cannot apply to an entry held, `oldest` or
47336
+ * newer: every revalidation they name so far is no later than it. That
47337
+ * moves `completeSince` no further than `oldest`, which costs the entries
47338
+ * held nothing, and a profile's `expiredAt` still to come waits in
47339
+ * `laterFloors`. Without this, a deployment revalidating many distinct tags
47340
+ * filled `logOnly` with tags no entry here carries, and the evictions past
47341
+ * its bound kept moving `completeSince` up to the present.
47342
+ */
47343
+ pruneLogged(oldest) {
47344
+ const at = markerClock();
47345
+ for (const [tag, marker] of this.logOnly) {
47346
+ if (knownBy(marker, at) <= oldest) {
47347
+ this.logOnly.delete(tag);
47348
+ this.raiseFloor(marker);
47349
+ }
47350
+ }
47351
+ }
47352
+ /**
47353
+ * The oldest entry timestamp of any registered store, `Infinity` with none:
47354
+ * early, never late, unless `scan`.
47355
+ */
47356
+ oldestEntry(scan) {
47357
+ let oldest = Infinity;
47358
+ for (const { oldestTimestamp } of this.entryStores) {
47359
+ oldest = Math.min(oldest, oldestTimestamp(scan));
47360
+ }
47361
+ return oldest;
47362
+ }
47363
+ /**
47364
+ * Move `completeSince` past `marker`'s revalidations: what is no longer
47365
+ * known about a tag once its marker is dropped. A future `expiredAt` waits
47366
+ * in `laterFloors` until it is past.
47367
+ */
47368
+ raiseFloor(marker) {
47369
+ const at = markerClock();
47370
+ this.floor = Math.max(this.floor, knownBy(marker, at));
47371
+ const { expiredAt } = marker;
47372
+ if (expiredAt !== void 0 && expiredAt > Math.max(at, this.floor)) {
47373
+ this.laterFloor(expiredAt);
47374
+ }
47375
+ }
47376
+ /**
47377
+ * Add `time` to `laterFloors`, in order. Past the tracked-tag bound the two
47378
+ * spans closest together are merged: from the first's start,
47379
+ * `completeSince` is the second's end, early - which only costs reads, and
47380
+ * for as short a time as any merge allows. Never later than its time, which
47381
+ * would let an entry expire unseen.
47382
+ */
47383
+ laterFloor(time2) {
47384
+ const floors = this.laterFloors;
47385
+ let low = 0;
47386
+ let high = floors.length;
47387
+ while (low < high) {
47388
+ const mid = Math.floor((low + high) / 2);
47389
+ if (floors[mid][0] < time2) {
47390
+ low = mid + 1;
47391
+ } else {
47392
+ high = mid;
47393
+ }
47394
+ }
47395
+ if (low > 0 && floors[low - 1][1] >= time2 || floors[low]?.[0] === time2) {
47396
+ return;
47397
+ }
47398
+ floors.splice(low, 0, [time2, time2]);
47399
+ if (floors.length > this.maxTrackedTags) {
47400
+ let merge2 = 0;
47401
+ for (let i5 = 1; i5 < floors.length - 1; i5++) {
47402
+ if (floors[i5 + 1][1] - floors[i5][0] < floors[merge2 + 1][1] - floors[merge2][0]) {
47403
+ merge2 = i5;
47404
+ }
47405
+ }
47406
+ floors.splice(merge2, 2, [floors[merge2][0], floors[merge2 + 1][1]]);
47407
+ }
47023
47408
  }
47024
47409
  /** Record that `tag`'s marker was known from the table at `at`. */
47025
47410
  known(tag, at) {
@@ -47036,6 +47421,7 @@ var TrackedTagMarkers = class {
47036
47421
  if (this.tags.size <= this.maxTrackedTags) {
47037
47422
  break;
47038
47423
  }
47424
+ this.raiseFloor(this.tags.get(oldest));
47039
47425
  this.tags.delete(oldest);
47040
47426
  this.dueAt.delete(oldest);
47041
47427
  this.unread.delete(oldest);
@@ -47048,7 +47434,18 @@ var TrackedTagMarkers = class {
47048
47434
  }
47049
47435
  }
47050
47436
  };
47437
+ function knownBy(marker, at) {
47438
+ const { revalidatedAt, staleAt, expiredAt } = marker;
47439
+ return Math.max(
47440
+ revalidatedAt ?? -Infinity,
47441
+ staleAt ?? -Infinity,
47442
+ expiredAt !== void 0 && expiredAt <= at ? expiredAt : -Infinity
47443
+ );
47444
+ }
47051
47445
  var MAX_LOG_PUT_MS = REVALIDATION_LOG_LOOKBACK_MS / 2;
47446
+ var ENTRY_SCAN_INTERVAL_MS = 6e4;
47447
+ var REFRESH_TICK_MS = 250;
47448
+ var REFRESH_PAUSE_MS = 1e3;
47052
47449
  function mergeMarkers(local, remote) {
47053
47450
  if (!local) {
47054
47451
  return remote ?? {};
@@ -47191,6 +47588,12 @@ var EntryLru = class {
47191
47588
  maxBytes;
47192
47589
  entries = /* @__PURE__ */ new Map();
47193
47590
  bytes = 0;
47591
+ /**
47592
+ * No later than the oldest entry's `timestamp`: the last scan's answer, less
47593
+ * whatever was set since. Entries dropped since only make it early. See
47594
+ * {@link oldestBound}.
47595
+ */
47596
+ lowest = Infinity;
47194
47597
  get(key) {
47195
47598
  const entry = this.entries.get(key);
47196
47599
  if (entry) {
@@ -47207,6 +47610,7 @@ var EntryLru = class {
47207
47610
  }
47208
47611
  this.entries.set(key, entry);
47209
47612
  this.bytes += size;
47613
+ this.lowest = Math.min(this.lowest, entry.timestamp);
47210
47614
  for (const [oldestKey, oldest] of this.entries) {
47211
47615
  if (this.bytes <= this.maxBytes) {
47212
47616
  break;
@@ -47220,7 +47624,58 @@ var EntryLru = class {
47220
47624
  if (existing) {
47221
47625
  this.entries.delete(key);
47222
47626
  this.bytes -= sizeOf2(key, existing);
47627
+ if (this.entries.size === 0) {
47628
+ this.lowest = Infinity;
47629
+ }
47630
+ }
47631
+ }
47632
+ /**
47633
+ * The `timestamp` of the oldest entry held, `Infinity` with none, dropping
47634
+ * on the way the entries past `revalidate` at `at` (`now()`): a handler
47635
+ * that drops them when next read would otherwise hold them until then, and
47636
+ * they would count here meanwhile. A scan, so asked once in a while: in
47637
+ * between, {@link oldestBound} answers.
47638
+ */
47639
+ oldestTimestamp(at = -Infinity) {
47640
+ let oldest = Infinity;
47641
+ for (const [key, entry] of this.entries) {
47642
+ if (isPastRevalidate(entry, at)) {
47643
+ this.delete(key);
47644
+ } else {
47645
+ oldest = Math.min(oldest, entry.timestamp);
47646
+ }
47647
+ }
47648
+ this.lowest = oldest;
47649
+ return oldest;
47650
+ }
47651
+ /**
47652
+ * No later than {@link oldestTimestamp}, without its scan: as of the last
47653
+ * one, less the entries set since. `Infinity` with none held. Drops the
47654
+ * entries past `revalidate` at `at` from the least recently used end, up to
47655
+ * the first that isn't, so {@link holdsLive} doesn't walk past them on every
47656
+ * request until the next scan.
47657
+ */
47658
+ oldestBound(at = -Infinity) {
47659
+ for (const [key, entry] of this.entries) {
47660
+ if (!isPastRevalidate(entry, at)) {
47661
+ break;
47662
+ }
47663
+ this.delete(key);
47223
47664
  }
47665
+ return this.entries.size === 0 ? Infinity : this.lowest;
47666
+ }
47667
+ /**
47668
+ * Whether an entry not past `revalidate` at `at` is held. Usually the first
47669
+ * entry answers; the expired ones it passes are dropped by the scan
47670
+ * {@link oldestTimestamp} makes, not here, so asking changes nothing.
47671
+ */
47672
+ holdsLive(at) {
47673
+ for (const entry of this.entries.values()) {
47674
+ if (!isPastRevalidate(entry, at)) {
47675
+ return true;
47676
+ }
47677
+ }
47678
+ return false;
47224
47679
  }
47225
47680
  get size() {
47226
47681
  return this.entries.size;
@@ -47229,31 +47684,58 @@ var EntryLru = class {
47229
47684
  function sizeOf2(key, entry) {
47230
47685
  return entry.value.byteLength + key.length;
47231
47686
  }
47687
+ function isPastRevalidate(stored, at) {
47688
+ return at > stored.timestamp + stored.revalidate * 1e3;
47689
+ }
47232
47690
  var DEFAULT_MEMORY_BYTES = 50 * 1024 * 1024;
47233
- function tagMethods(tags) {
47691
+ var TAG_TABLE_REQUEST_TIMEOUT_MS = 3e3;
47692
+ async function awaitIf(wait, work, failure) {
47693
+ const logged = work.catch((error2) => {
47694
+ console.error(failure, error2);
47695
+ });
47696
+ if (wait) {
47697
+ await logged;
47698
+ }
47699
+ }
47700
+ function tagMethods(tags, options = {}) {
47234
47701
  return {
47235
- refreshTags: () => tags.refresh(),
47236
- async getExpiration(implicitTags) {
47237
- await tags.ensure(implicitTags);
47238
- return tags.expiration(implicitTags);
47239
- },
47702
+ // Started, not awaited, while the instance keeps up, unless `blocking`.
47703
+ // Next.js awaits `refreshTags` inside the first `'use cache'` lookup of a
47704
+ // request (`use-cache-wrapper`), and a staged render ends its static stage
47705
+ // on a timer: a log `Query` there pushed the entry out of the static
47706
+ // stage, so a cached navigation stored the page segment without it. What
47707
+ // the query finds applies from the next request. Awaited once the instance
47708
+ // is `behind`, which the runtime settles before a page render starts
47709
+ // (`catchUp`): the first request after an idle or frozen spell would
47710
+ // otherwise serve whatever was revalidated elsewhere meanwhile. A page's
47711
+ // render just after `catchUp` gets a grace more (`behindInRender`), so a
47712
+ // request that came in just short of the line doesn't cross it inside
47713
+ // the render. Not even then while the instance holds no entry the query
47714
+ // could expire (`holdsEntries`), with a `refreshIntervalMs` of `0` too.
47715
+ refreshTags: () => awaitIf(
47716
+ options.blocking === true || tags.behindInRender && tags.holdsEntries,
47717
+ tags.refresh(),
47718
+ "Error refreshing cache tags:"
47719
+ ),
47240
47720
  async updateTags(revalidatedTags, durations) {
47241
47721
  await tags.update(revalidatedTags, durations);
47242
47722
  }
47243
47723
  };
47244
47724
  }
47725
+ function outsideRender(fn) {
47726
+ return outsideRequest(sharedTagManifest).outsideRender(fn);
47727
+ }
47245
47728
  function lazyHandler(create) {
47246
47729
  let handler;
47247
47730
  const instance = () => handler ??= create();
47248
47731
  return {
47249
- get: (cacheKey, softTags) => instance().get(cacheKey, softTags),
47250
- set: (cacheKey, pendingEntry) => instance().set(cacheKey, pendingEntry),
47251
- refreshTags: () => instance().refreshTags(),
47252
- getExpiration: (tags) => instance().getExpiration(tags),
47253
- updateTags: (tags, durations) => instance().updateTags(tags, durations)
47732
+ get: (cacheKey, softTags) => outsideRender(() => instance().get(cacheKey, softTags)),
47733
+ set: (cacheKey, pendingEntry) => outsideRender(() => instance().set(cacheKey, pendingEntry)),
47734
+ refreshTags: () => outsideRender(() => instance().refreshTags()),
47735
+ getExpiration: (tags) => outsideRender(() => instance().getExpiration(tags)),
47736
+ updateTags: (tags, durations) => outsideRender(() => instance().updateTags(tags, durations))
47254
47737
  };
47255
47738
  }
47256
- var TAG_MANIFEST_SYMBOL = /* @__PURE__ */ Symbol.for("cdk-nextjs.use-cache.tag-manifest");
47257
47739
  function sharedTagManifest() {
47258
47740
  const global = globalThis;
47259
47741
  if (!global[TAG_MANIFEST_SYMBOL]) {
@@ -47263,12 +47745,24 @@ function sharedTagManifest() {
47263
47745
  DEFAULT_TAG_REFRESH_MS
47264
47746
  );
47265
47747
  if (config.tableName && !isBuildPhase()) {
47266
- const client = new import_client_dynamodb2.DynamoDBClient({ region: config.region });
47748
+ const client = new import_client_dynamodb2.DynamoDBClient({
47749
+ region: config.region,
47750
+ // Without `throwOnRequestTimeout` the timeout only logs a warning. It
47751
+ // ends once the response headers arrive: `socketTimeout`, an
47752
+ // inactivity timer kept until the request closes, covers the body.
47753
+ requestHandler: {
47754
+ requestTimeout: TAG_TABLE_REQUEST_TIMEOUT_MS,
47755
+ throwOnRequestTimeout: true,
47756
+ socketTimeout: TAG_TABLE_REQUEST_TIMEOUT_MS
47757
+ }
47758
+ });
47267
47759
  global[TAG_MANIFEST_SYMBOL] = new TrackedTagMarkers({
47268
47760
  markers: new TagMarkerTable(client, config.tableName, config.buildId),
47269
47761
  log: new RevalidationLog(client, config.tableName, config.buildId),
47270
47762
  refreshIntervalMs,
47271
- debug: (0, import_debug5.default)("cdk-nextjs:cache-handler:tags")
47763
+ debug: (0, import_debug5.default)("cdk-nextjs:cache-handler:tags"),
47764
+ // Set in every Lambda runtime; a container is never frozen.
47765
+ canFreeze: process.env.AWS_LAMBDA_FUNCTION_NAME !== void 0
47272
47766
  });
47273
47767
  } else {
47274
47768
  if (!isBuildPhase()) {
@@ -47298,9 +47792,6 @@ function bucketFromEnv() {
47298
47792
  }
47299
47793
  return new CacheBucket(new import_client_s32.S3Client({ region }), bucketName);
47300
47794
  }
47301
- function isPastRevalidate(stored, at) {
47302
- return at > stored.timestamp + stored.revalidate * 1e3;
47303
- }
47304
47795
  function isPastExpire(stored, at) {
47305
47796
  return at > stored.timestamp + stored.expire * 1e3;
47306
47797
  }
@@ -47420,7 +47911,13 @@ function createRemoteUseCacheHandler(options = {}) {
47420
47911
  done();
47421
47912
  await writeToS3(cacheKey, stored);
47422
47913
  },
47423
- ...tagMethods(tags)
47914
+ // Waits for the log query on every request: an entry read from S3 may
47915
+ // predate this instance, so what the query says applies to it at once.
47916
+ ...tagMethods(tags, { blocking: true }),
47917
+ async getExpiration(implicitTags) {
47918
+ await tags.ensure(implicitTags);
47919
+ return tags.expiration(implicitTags);
47920
+ }
47424
47921
  };
47425
47922
  }
47426
47923
  var use_cache_remote_handler_default = lazyHandler(() => createRemoteUseCacheHandler());