cdk-nextjs 0.7.6 → 0.7.7

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 (43) 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/caching-guide.md +11 -1
  6. package/docs/harness-coverage.md +2 -1
  7. package/lib/adapter/aws-cache-store.d.ts +214 -3
  8. package/lib/adapter/aws-cache-store.js +470 -35
  9. package/lib/adapter/aws-cache-store.test.js +675 -3
  10. package/lib/adapter/cache-handler.mjs +421 -30
  11. package/lib/adapter/use-cache-common.d.ts +53 -1
  12. package/lib/adapter/use-cache-common.js +121 -14
  13. package/lib/adapter/use-cache-common.test.js +138 -1
  14. package/lib/adapter/use-cache-default-handler.js +36 -6
  15. package/lib/adapter/use-cache-default-handler.mjs +525 -42
  16. package/lib/adapter/use-cache-handlers.test.js +247 -7
  17. package/lib/adapter/use-cache-remote-handler.js +9 -7
  18. package/lib/adapter/use-cache-remote-handler.mjs +516 -40
  19. package/lib/lambdas/post-deploy/prune-revalidation-table.d.ts +2 -1
  20. package/lib/lambdas/post-deploy/prune-revalidation-table.js +35 -13
  21. package/lib/lambdas/post-deploy/prune-revalidation-table.test.js +19 -1
  22. package/lib/nextjs-api.js +1 -1
  23. package/lib/nextjs-build/nextjs-build.js +1 -1
  24. package/lib/nextjs-cache.js +1 -1
  25. package/lib/nextjs-compute/nextjs-containers.js +1 -1
  26. package/lib/nextjs-compute/nextjs-functions.js +1 -1
  27. package/lib/nextjs-distribution.js +1 -1
  28. package/lib/nextjs-post-deploy.js +1 -1
  29. package/lib/nextjs-static-assets.js +1 -1
  30. package/lib/root-constructs/nextjs-base-construct.js +1 -1
  31. package/lib/root-constructs/nextjs-global-containers.js +1 -1
  32. package/lib/root-constructs/nextjs-global-functions.js +1 -1
  33. package/lib/root-constructs/nextjs-regional-containers.js +1 -1
  34. package/lib/root-constructs/nextjs-regional-functions.js +1 -1
  35. package/lib/runtime/core.js +18 -4
  36. package/lib/runtime/core.test.js +17 -1
  37. package/lib/runtime/lambda.mjs +16 -3
  38. package/lib/runtime/server.mjs +16 -3
  39. package/lib/runtime/tag-manifest.d.ts +15 -0
  40. package/lib/runtime/tag-manifest.js +24 -0
  41. package/lib/runtime/tag-manifest.test.d.ts +1 -0
  42. package/lib/runtime/tag-manifest.test.js +21 -0
  43. package/package.json +1 -1
@@ -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
@@ -46390,6 +46391,7 @@ function markerOf(item) {
46390
46391
  }
46391
46392
  var BATCH_GET_MAX_KEYS = 100;
46392
46393
  var DEFAULT_TAG_REFRESH_MS = 1e3;
46394
+ var TAG_REFRESH_GRACE_MS = 1e3;
46393
46395
  function hashedTag(tag) {
46394
46396
  return `#${sha256Hex(tag)}`;
46395
46397
  }
@@ -46603,10 +46605,64 @@ var TrackedTagMarkers = class {
46603
46605
  * result rather than dropped.
46604
46606
  */
46605
46607
  pendingRows = /* @__PURE__ */ new Map();
46608
+ /**
46609
+ * Log rows for tags not tracked and not being read, merged by tag, oldest
46610
+ * first: what the log has said about them since {@link completeSince}. Taken
46611
+ * into a tag's marker when it is read or written.
46612
+ */
46613
+ logOnly = /* @__PURE__ */ new Map();
46614
+ /** See {@link completeSince}. */
46615
+ floor;
46616
+ /**
46617
+ * The future `expiredAt`s of markers dropped since, as `[from, to]`, in
46618
+ * order: once `from` is past, {@link completeSince} moves to `to`, and not
46619
+ * before, since only then does it expire anything. `from` and `to` are the
46620
+ * same until spans are merged for space ({@link laterFloor}).
46621
+ */
46622
+ laterFloors = [];
46623
+ /** The in-memory entry stores judged by {@link completeSince}. */
46624
+ entryStores = [];
46625
+ /** How many times {@link forget} ran: a read started before one is made again. */
46626
+ forgotten = 0;
46606
46627
  /** Tracked tags whose read failed, for the next refresh to read again. */
46607
46628
  unread = /* @__PURE__ */ new Set();
46629
+ /**
46630
+ * Set inside a page render {@link catchUp} let go, for that render alone.
46631
+ * See {@link behindInRender}.
46632
+ */
46633
+ caughtUpRender = new AsyncLocalStorage();
46634
+ /** When `sync` last scanned the entry stores ({@link oldestEntry}). */
46635
+ lastEntryScan = -Infinity;
46636
+ /**
46637
+ * No `logOnly` row is known by it or earlier, so a {@link pruneLogged}
46638
+ * against it or earlier drops nothing: a marker is only ever known by later,
46639
+ * as its `expiredAt` passes.
46640
+ */
46641
+ prunedAgainst = -Infinity;
46642
+ /** When the last refresh started, or was skipped. */
46608
46643
  lastRefresh = -Infinity;
46644
+ /**
46645
+ * The refresh in flight. One left open across a freeze comes back after the
46646
+ * thaw with what it read before, so {@link refresh} waits for it and then
46647
+ * for a fresh one, rather than the runtime waiting for it before the freeze:
46648
+ * that would bill every Lambda invocation for the query's tail.
46649
+ */
46609
46650
  refreshing;
46651
+ /**
46652
+ * When {@link refreshing} last showed the process running: a timer ticks
46653
+ * while it is in flight, and a tick that comes late, or none at all, means
46654
+ * the process was paused - frozen - since it started.
46655
+ */
46656
+ lastTick = -Infinity;
46657
+ /** Whether a tick came late since {@link refreshing} started. */
46658
+ pausedWhileRefreshing = false;
46659
+ ticker;
46660
+ canFreeze;
46661
+ /**
46662
+ * When the last refresh settled, either way, unless it spanned a pause: what
46663
+ * it knows is from then. See {@link behind}.
46664
+ */
46665
+ settledAt;
46610
46666
  /**
46611
46667
  * Where the next log query starts (`clock()`): the last successful one's
46612
46668
  * start, less {@link REVALIDATION_LOG_LOOKBACK_MS}.
@@ -46628,20 +46684,68 @@ var TrackedTagMarkers = class {
46628
46684
  this.random = options.random ?? Math.random;
46629
46685
  this.debug = options.debug ?? (() => {
46630
46686
  });
46687
+ this.canFreeze = options.canFreeze ?? false;
46631
46688
  const at = this.clock();
46632
46689
  this.cursor = at - REVALIDATION_LOG_LOOKBACK_MS;
46633
46690
  this.lastLogRead = at;
46691
+ this.settledAt = at;
46692
+ this.floor = markerClock();
46634
46693
  }
46635
46694
  /** `tag`'s marker as this instance knows it, if it tracks `tag`. */
46636
46695
  get(tag) {
46637
46696
  return this.tags.get(tag);
46638
46697
  }
46698
+ /**
46699
+ * Since when (`markerClock()`) this instance knows of every revalidation of
46700
+ * every tag, tracked or not: tracked ones from their markers and the log,
46701
+ * the others from the log alone (`logOnly`). So an entry created at or after
46702
+ * it can be judged by {@link state} and {@link expiration} without
46703
+ * {@link ensure} reading anything - which matters inside a `'use cache'`
46704
+ * lookup, where Next.js waits for the answer and a read pushes the entry out
46705
+ * of the static stage of a staged render.
46706
+ *
46707
+ * Starts at construction: the log is followed from before it. Moves to now
46708
+ * when the tracked markers are forgotten, and past the revalidations of
46709
+ * whatever else it lets go of - a marker evicted for space, a log row
46710
+ * pruned - which are no longer known: to its `expiredAt` only once that is
46711
+ * past, since a profile's `expire` can be a year out, and moving there at
46712
+ * once would turn the read-free path off for the instance's life.
46713
+ *
46714
+ * A row is pruned only once it is no later than every entry held, so that
46715
+ * costs those entries nothing. An entry stored afterwards with an older
46716
+ * `timestamp` - Next.js stamps the start of its generation, which can
46717
+ * outlast a lookback - is older than this too, and is read first.
46718
+ *
46719
+ * It does not cover a revalidation older than the log's first lookback
46720
+ * whose `expiredAt` is still to come, which would expire an entry created
46721
+ * after it. So the `'use cache'` handler reads an entry's tags in the
46722
+ * background as it stores it.
46723
+ */
46724
+ get completeSince() {
46725
+ const at = markerClock();
46726
+ while (this.laterFloors.length > 0 && this.laterFloors[0][0] <= at) {
46727
+ this.floor = Math.max(this.floor, this.laterFloors.shift()[1]);
46728
+ }
46729
+ return this.floor;
46730
+ }
46731
+ /**
46732
+ * Register an in-memory store whose entries are judged by
46733
+ * {@link completeSince}, by the `timestamp` of the oldest entry it holds:
46734
+ * `logOnly` keeps the rows that could still apply to one of them, and no
46735
+ * others. With none registered it keeps nothing. Asked with `scan` false,
46736
+ * `oldestTimestamp` may answer early, but never late, without a scan.
46737
+ */
46738
+ judgeEntriesOf(oldestTimestamp, holdsAny = () => oldestTimestamp(false) < Infinity) {
46739
+ this.entryStores.push({ oldestTimestamp, holdsAny });
46740
+ }
46639
46741
  /**
46640
46742
  * Track `tag` with `marker`: what this instance just wrote, which it knows
46641
46743
  * without reading it back.
46642
46744
  */
46643
46745
  set(tag, marker) {
46644
- this.remember(tag, marker);
46746
+ const logged = this.logOnly.get(tag);
46747
+ this.logOnly.delete(tag);
46748
+ this.remember(tag, mergeMarkers(marker, logged));
46645
46749
  this.known(tag, this.clock());
46646
46750
  }
46647
46751
  /**
@@ -46694,27 +46798,103 @@ var TrackedTagMarkers = class {
46694
46798
  const unread = [];
46695
46799
  for (const tag of new Set(tags)) {
46696
46800
  const inFlight = this.reading.get(tag);
46697
- if (inFlight) {
46698
- waits.push(inFlight);
46699
- } else if (this.tags.has(tag)) {
46801
+ if (this.tags.has(tag)) {
46700
46802
  this.touch(tag);
46803
+ } else if (inFlight) {
46804
+ waits.push(inFlight);
46701
46805
  } else {
46702
46806
  unread.push(tag);
46703
46807
  }
46704
46808
  }
46705
46809
  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);
46810
+ waits.push(this.startRead(unread));
46715
46811
  }
46716
46812
  await Promise.all(waits);
46717
46813
  }
46814
+ /**
46815
+ * {@link readInto} `tags`, as the read of each that whoever needs one of
46816
+ * them next waits on ({@link reading}). Never rejects: a failed read leaves
46817
+ * the tags tracked.
46818
+ */
46819
+ startRead(tags) {
46820
+ const read = this.readInto(tags).then(() => {
46821
+ for (const tag of tags) {
46822
+ this.reading.delete(tag);
46823
+ }
46824
+ });
46825
+ for (const tag of tags) {
46826
+ this.reading.set(tag, read);
46827
+ }
46828
+ return read;
46829
+ }
46830
+ /**
46831
+ * Whether the last refresh settled more than `refreshIntervalMs` plus
46832
+ * {@link TAG_REFRESH_GRACE_MS} ago - one that spanned a freeze counts from
46833
+ * when it started: after the instance sat idle or frozen - or always, with
46834
+ * a `refreshIntervalMs` of `0`. Answering from what it knows then could
46835
+ * serve an entry revalidated elsewhere since, however long ago that was, so
46836
+ * `refreshTags` waits for the refresh, and the runtime waits for it before
46837
+ * a page render starts ({@link catchUp}).
46838
+ *
46839
+ * From when it settled, not when it started, so a slow query the runtime
46840
+ * waited for before the render does not leave the instance behind inside
46841
+ * it. A failed one counts too, so one request does not wait for two: the
46842
+ * same "assume the entry is valid" a failed read answers with, until the
46843
+ * next refresh, at most the interval and the grace later.
46844
+ */
46845
+ get behind() {
46846
+ return this.log !== void 0 && // `0` asks before every check, so every check waits for the answer.
46847
+ (this.refreshIntervalMs === 0 || this.overdue(this.settledAt));
46848
+ }
46849
+ /**
46850
+ * {@link behind}, as `refreshTags` asks it: never inside a page's render
46851
+ * {@link catchUp} caught up, since the runtime settled it for that request
46852
+ * before the render started, and a wait inside a `'use cache'` lookup is
46853
+ * what it settled it to avoid - a render crossing the line partway through
46854
+ * would otherwise wait after all. So a revalidation elsewhere can go unseen
46855
+ * for a render's duration on top of the usual window.
46856
+ *
46857
+ * A route handler, which no catch-up precedes, waits at the plain line.
46858
+ */
46859
+ get behindInRender() {
46860
+ return this.refreshIntervalMs !== 0 && this.caughtUpRender.getStore() ? false : this.behind;
46861
+ }
46862
+ /**
46863
+ * Whether a registered store ({@link judgeEntriesOf}) holds any entry: one
46864
+ * a refresh could expire. With none there is nothing to wait for even when
46865
+ * the instance is {@link behind}, since whatever is stored from here on is
46866
+ * newer than what the log would say - which covers the first `'use cache'`
46867
+ * call of a process, which creates the handler inside the render.
46868
+ */
46869
+ get holdsEntries() {
46870
+ return this.entryStores.some((store) => store.holdsAny());
46871
+ }
46872
+ /**
46873
+ * {@link refresh}, waited for when the instance is {@link behind}. The
46874
+ * runtime calls it before handing a page request to Next.js, outside the
46875
+ * staged render whose static stage a wait inside a `'use cache'` lookup
46876
+ * would cut short. Only for the `'use cache'` handler's entries
46877
+ * ({@link judgeEntriesOf}): the other caches wait for the refresh where they
46878
+ * read anyway. Not with a `refreshIntervalMs` of `0` either, where
46879
+ * `refreshTags` asks again regardless. Then `render`, marked as caught up
46880
+ * for ({@link behindInRender}). Rejects only as `render` does.
46881
+ */
46882
+ async catchUp(render) {
46883
+ const log = (error2) => console.error("Error refreshing cache tags:", error2);
46884
+ if (this.refreshIntervalMs > 0 && this.behind) {
46885
+ if (!this.holdsEntries) {
46886
+ return render();
46887
+ }
46888
+ await this.refresh().catch(log);
46889
+ } else if (this.refreshIntervalMs > 0) {
46890
+ void this.refresh().catch(log);
46891
+ }
46892
+ return this.caughtUpRender.run(true, render);
46893
+ }
46894
+ /** Whether `at` is more than the interval and the grace ago. */
46895
+ overdue(at) {
46896
+ return this.clock() - at > this.refreshIntervalMs + TAG_REFRESH_GRACE_MS;
46897
+ }
46718
46898
  /**
46719
46899
  * Catch up on revalidations other instances ran, at most once per
46720
46900
  * `refreshIntervalMs`. See {@link TrackedTagMarkers} for the cost.
@@ -46725,6 +46905,9 @@ var TrackedTagMarkers = class {
46725
46905
  return;
46726
46906
  }
46727
46907
  if (this.refreshing) {
46908
+ if (this.behind && this.pausedSinceRefreshStarted()) {
46909
+ return this.refreshing.then(() => this.refresh());
46910
+ }
46728
46911
  return this.refreshing;
46729
46912
  }
46730
46913
  const at = this.clock();
@@ -46732,14 +46915,47 @@ var TrackedTagMarkers = class {
46732
46915
  return;
46733
46916
  }
46734
46917
  this.lastRefresh = at;
46735
- if (this.tags.size === 0) {
46918
+ if (this.tags.size === 0 && this.entryStores.length === 0) {
46736
46919
  return;
46737
46920
  }
46921
+ this.startTicking();
46738
46922
  this.refreshing = this.sync(at, log).finally(() => {
46923
+ this.settledAt = Math.max(
46924
+ this.settledAt,
46925
+ this.pausedSinceRefreshStarted() ? this.lastRefresh : this.clock()
46926
+ );
46927
+ this.stopTicking();
46739
46928
  this.refreshing = void 0;
46740
46929
  });
46741
46930
  return this.refreshing;
46742
46931
  }
46932
+ startTicking() {
46933
+ if (!this.canFreeze) {
46934
+ return;
46935
+ }
46936
+ this.lastTick = this.clock();
46937
+ this.pausedWhileRefreshing = false;
46938
+ this.ticker = setInterval(() => {
46939
+ const at = this.clock();
46940
+ if (at - this.lastTick > REFRESH_PAUSE_MS) {
46941
+ this.pausedWhileRefreshing = true;
46942
+ }
46943
+ this.lastTick = at;
46944
+ }, REFRESH_TICK_MS);
46945
+ this.ticker.unref?.();
46946
+ }
46947
+ stopTicking() {
46948
+ clearInterval(this.ticker);
46949
+ this.ticker = void 0;
46950
+ }
46951
+ /**
46952
+ * Whether the process was paused since {@link refreshing} started: a tick
46953
+ * came late, or the next one is overdue (after a thaw, a request can run
46954
+ * before the timer does).
46955
+ */
46956
+ pausedSinceRefreshStarted() {
46957
+ return this.canFreeze && (this.pausedWhileRefreshing || this.clock() - this.lastTick > REFRESH_PAUSE_MS);
46958
+ }
46743
46959
  /**
46744
46960
  * Record a revalidation of `tags`: here at once, and in the revalidation
46745
46961
  * table for every other instance - the marker row plus a log row per tag.
@@ -46772,7 +46988,7 @@ var TrackedTagMarkers = class {
46772
46988
  const at = markerClock();
46773
46989
  let state2 = "fresh";
46774
46990
  for (const tag of tags) {
46775
- const marker = this.get(tag);
46991
+ const marker = this.knownMarker(tag);
46776
46992
  if (!marker) {
46777
46993
  continue;
46778
46994
  }
@@ -46796,7 +47012,7 @@ var TrackedTagMarkers = class {
46796
47012
  const at = markerClock();
46797
47013
  let latest = 0;
46798
47014
  for (const tag of tags) {
46799
- const marker = this.get(tag);
47015
+ const marker = this.knownMarker(tag);
46800
47016
  if (!marker) {
46801
47017
  continue;
46802
47018
  }
@@ -46883,13 +47099,14 @@ var TrackedTagMarkers = class {
46883
47099
  );
46884
47100
  return;
46885
47101
  }
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
- ]);
47102
+ const due = this.due(at);
47103
+ if (due.length > 0) {
47104
+ void this.startRead(due);
47105
+ }
47106
+ const result = await log.query(this.cursor).catch((error2) => {
47107
+ console.error("Error reading tag revalidation log:", error2);
47108
+ return void 0;
47109
+ });
46893
47110
  if (!result) {
46894
47111
  return;
46895
47112
  }
@@ -46900,6 +47117,11 @@ var TrackedTagMarkers = class {
46900
47117
  );
46901
47118
  return;
46902
47119
  }
47120
+ const scan = at - this.lastEntryScan > ENTRY_SCAN_INTERVAL_MS;
47121
+ if (scan) {
47122
+ this.lastEntryScan = at;
47123
+ }
47124
+ const oldest = this.oldestEntry(scan);
46903
47125
  let applied = 0;
46904
47126
  for (const row of result.rows) {
46905
47127
  if (this.applied.has(row.sk)) {
@@ -46914,11 +47136,20 @@ var TrackedTagMarkers = class {
46914
47136
  mergeMarkers(this.pendingRows.get(row.tag), row.marker)
46915
47137
  );
46916
47138
  } else {
47139
+ this.rememberLogged(
47140
+ row.tag,
47141
+ mergeMarkers(this.logOnly.get(row.tag), row.marker),
47142
+ oldest
47143
+ );
46917
47144
  continue;
46918
47145
  }
46919
47146
  this.applied.set(row.sk, row.at);
46920
47147
  applied++;
46921
47148
  }
47149
+ if (oldest > this.prunedAgainst) {
47150
+ this.pruneLogged(oldest);
47151
+ }
47152
+ this.prunedAgainst = oldest;
46922
47153
  this.lastLogRead = at;
46923
47154
  this.advance(at - REVALIDATION_LOG_LOOKBACK_MS);
46924
47155
  this.debug(
@@ -46934,14 +47165,16 @@ var TrackedTagMarkers = class {
46934
47165
  const due = [];
46935
47166
  for (const tag of this.unread) {
46936
47167
  if (due.length >= BATCH_GET_MAX_KEYS) return due;
46937
- due.push(tag);
47168
+ if (!this.reading.has(tag)) {
47169
+ due.push(tag);
47170
+ }
46938
47171
  }
46939
47172
  const jitter = this.resyncIntervalMs * TAG_RESYNC_JITTER;
46940
47173
  for (const [tag, dueAt] of this.dueAt) {
46941
47174
  if (due.length >= BATCH_GET_MAX_KEYS || dueAt - jitter > at) {
46942
47175
  break;
46943
47176
  }
46944
- if (dueAt <= at && !this.unread.has(tag)) {
47177
+ if (dueAt <= at && !this.unread.has(tag) && !this.reading.has(tag)) {
46945
47178
  due.push(tag);
46946
47179
  }
46947
47180
  }
@@ -46954,10 +47187,19 @@ var TrackedTagMarkers = class {
46954
47187
  */
46955
47188
  forget(at, why) {
46956
47189
  this.debug(`forgetting ${this.tags.size} tracked tags: ${why}`);
47190
+ this.forgotten++;
47191
+ for (const marker of this.tags.values()) {
47192
+ this.raiseFloor(marker);
47193
+ }
47194
+ for (const marker of this.logOnly.values()) {
47195
+ this.raiseFloor(marker);
47196
+ }
46957
47197
  this.tags.clear();
46958
47198
  this.dueAt.clear();
46959
47199
  this.unread.clear();
46960
47200
  this.applied.clear();
47201
+ this.logOnly.clear();
47202
+ this.floor = Math.max(this.floor, markerClock());
46961
47203
  this.cursor = at - REVALIDATION_LOG_LOOKBACK_MS;
46962
47204
  this.lastLogRead = at;
46963
47205
  }
@@ -46983,13 +47225,17 @@ var TrackedTagMarkers = class {
46983
47225
  * always answered with, rather than a read on every request while DynamoDB
46984
47226
  * is unavailable.
46985
47227
  */
46986
- async readInto(tags) {
47228
+ async readInto(tags, again = false) {
46987
47229
  if (tags.length === 0) {
46988
47230
  return true;
46989
47231
  }
46990
47232
  const at = this.clock();
47233
+ const forgotten = this.forgotten;
46991
47234
  try {
46992
47235
  const read = await this.markers.read(tags);
47236
+ if (this.forgotten !== forgotten && !again) {
47237
+ return await this.readInto(tags, true);
47238
+ }
46993
47239
  for (const tag of tags) {
46994
47240
  this.unread.delete(tag);
46995
47241
  this.remember(
@@ -47005,6 +47251,9 @@ var TrackedTagMarkers = class {
47005
47251
  return true;
47006
47252
  } catch (error2) {
47007
47253
  console.error("Error reading tag markers:", error2);
47254
+ if (this.forgotten !== forgotten && !again) {
47255
+ return this.readInto(tags, true);
47256
+ }
47008
47257
  this.trackUnread(tags);
47009
47258
  for (const tag of tags) {
47010
47259
  this.unread.add(tag);
@@ -47018,8 +47267,119 @@ var TrackedTagMarkers = class {
47018
47267
  }
47019
47268
  takePendingRows(tag) {
47020
47269
  const pending = this.pendingRows.get(tag);
47270
+ const logged = this.logOnly.get(tag);
47021
47271
  this.pendingRows.delete(tag);
47022
- return pending;
47272
+ this.logOnly.delete(tag);
47273
+ return logged ? mergeMarkers(logged, pending) : pending;
47274
+ }
47275
+ /**
47276
+ * What this instance knows of `tag`: tracked, or from the log alone -
47277
+ * including rows that arrived while its first read is in flight.
47278
+ */
47279
+ knownMarker(tag) {
47280
+ const tracked = this.tags.get(tag);
47281
+ if (tracked) {
47282
+ return tracked;
47283
+ }
47284
+ const logged = this.logOnly.get(tag);
47285
+ const pending = this.pendingRows.get(tag);
47286
+ return logged || pending ? mergeMarkers(logged, pending) : void 0;
47287
+ }
47288
+ /**
47289
+ * Keep `tag`'s log rows in `logOnly`, unless they cannot apply to an entry
47290
+ * held, `oldest` or newer (see {@link pruneLogged}), as bounded as the
47291
+ * tracked tags. Either way a row let go of takes what it knew with it, so
47292
+ * `completeSince` moves past it.
47293
+ */
47294
+ rememberLogged(tag, marker, oldest) {
47295
+ this.logOnly.delete(tag);
47296
+ if (knownBy(marker, markerClock()) <= oldest) {
47297
+ this.raiseFloor(marker);
47298
+ return;
47299
+ }
47300
+ this.logOnly.set(tag, marker);
47301
+ for (const [first, dropped] of this.logOnly) {
47302
+ if (this.logOnly.size <= this.maxTrackedTags) {
47303
+ break;
47304
+ }
47305
+ this.logOnly.delete(first);
47306
+ this.raiseFloor(dropped);
47307
+ }
47308
+ }
47309
+ /**
47310
+ * Drop the `logOnly` rows that cannot apply to an entry held, `oldest` or
47311
+ * newer: every revalidation they name so far is no later than it. That
47312
+ * moves `completeSince` no further than `oldest`, which costs the entries
47313
+ * held nothing, and a profile's `expiredAt` still to come waits in
47314
+ * `laterFloors`. Without this, a deployment revalidating many distinct tags
47315
+ * filled `logOnly` with tags no entry here carries, and the evictions past
47316
+ * its bound kept moving `completeSince` up to the present.
47317
+ */
47318
+ pruneLogged(oldest) {
47319
+ const at = markerClock();
47320
+ for (const [tag, marker] of this.logOnly) {
47321
+ if (knownBy(marker, at) <= oldest) {
47322
+ this.logOnly.delete(tag);
47323
+ this.raiseFloor(marker);
47324
+ }
47325
+ }
47326
+ }
47327
+ /**
47328
+ * The oldest entry timestamp of any registered store, `Infinity` with none:
47329
+ * early, never late, unless `scan`.
47330
+ */
47331
+ oldestEntry(scan) {
47332
+ let oldest = Infinity;
47333
+ for (const { oldestTimestamp } of this.entryStores) {
47334
+ oldest = Math.min(oldest, oldestTimestamp(scan));
47335
+ }
47336
+ return oldest;
47337
+ }
47338
+ /**
47339
+ * Move `completeSince` past `marker`'s revalidations: what is no longer
47340
+ * known about a tag once its marker is dropped. A future `expiredAt` waits
47341
+ * in `laterFloors` until it is past.
47342
+ */
47343
+ raiseFloor(marker) {
47344
+ const at = markerClock();
47345
+ this.floor = Math.max(this.floor, knownBy(marker, at));
47346
+ const { expiredAt } = marker;
47347
+ if (expiredAt !== void 0 && expiredAt > Math.max(at, this.floor)) {
47348
+ this.laterFloor(expiredAt);
47349
+ }
47350
+ }
47351
+ /**
47352
+ * Add `time` to `laterFloors`, in order. Past the tracked-tag bound the two
47353
+ * spans closest together are merged: from the first's start,
47354
+ * `completeSince` is the second's end, early - which only costs reads, and
47355
+ * for as short a time as any merge allows. Never later than its time, which
47356
+ * would let an entry expire unseen.
47357
+ */
47358
+ laterFloor(time2) {
47359
+ const floors = this.laterFloors;
47360
+ let low = 0;
47361
+ let high = floors.length;
47362
+ while (low < high) {
47363
+ const mid = Math.floor((low + high) / 2);
47364
+ if (floors[mid][0] < time2) {
47365
+ low = mid + 1;
47366
+ } else {
47367
+ high = mid;
47368
+ }
47369
+ }
47370
+ if (low > 0 && floors[low - 1][1] >= time2 || floors[low]?.[0] === time2) {
47371
+ return;
47372
+ }
47373
+ floors.splice(low, 0, [time2, time2]);
47374
+ if (floors.length > this.maxTrackedTags) {
47375
+ let merge2 = 0;
47376
+ for (let i5 = 1; i5 < floors.length - 1; i5++) {
47377
+ if (floors[i5 + 1][1] - floors[i5][0] < floors[merge2 + 1][1] - floors[merge2][0]) {
47378
+ merge2 = i5;
47379
+ }
47380
+ }
47381
+ floors.splice(merge2, 2, [floors[merge2][0], floors[merge2 + 1][1]]);
47382
+ }
47023
47383
  }
47024
47384
  /** Record that `tag`'s marker was known from the table at `at`. */
47025
47385
  known(tag, at) {
@@ -47036,6 +47396,7 @@ var TrackedTagMarkers = class {
47036
47396
  if (this.tags.size <= this.maxTrackedTags) {
47037
47397
  break;
47038
47398
  }
47399
+ this.raiseFloor(this.tags.get(oldest));
47039
47400
  this.tags.delete(oldest);
47040
47401
  this.dueAt.delete(oldest);
47041
47402
  this.unread.delete(oldest);
@@ -47048,7 +47409,18 @@ var TrackedTagMarkers = class {
47048
47409
  }
47049
47410
  }
47050
47411
  };
47412
+ function knownBy(marker, at) {
47413
+ const { revalidatedAt, staleAt, expiredAt } = marker;
47414
+ return Math.max(
47415
+ revalidatedAt ?? -Infinity,
47416
+ staleAt ?? -Infinity,
47417
+ expiredAt !== void 0 && expiredAt <= at ? expiredAt : -Infinity
47418
+ );
47419
+ }
47051
47420
  var MAX_LOG_PUT_MS = REVALIDATION_LOG_LOOKBACK_MS / 2;
47421
+ var ENTRY_SCAN_INTERVAL_MS = 6e4;
47422
+ var REFRESH_TICK_MS = 250;
47423
+ var REFRESH_PAUSE_MS = 1e3;
47052
47424
  function mergeMarkers(local, remote) {
47053
47425
  if (!local) {
47054
47426
  return remote ?? {};
@@ -47111,6 +47483,13 @@ var CacheBucket = class {
47111
47483
  // src/adapter/use-cache-common.ts
47112
47484
  var import_client_dynamodb2 = __toESM(require_dist_cjs20());
47113
47485
  var import_debug5 = __toESM(require_src());
47486
+
47487
+ // src/runtime/tag-manifest.ts
47488
+ var TAG_MANIFEST_SYMBOL = /* @__PURE__ */ Symbol.for(
47489
+ "cdk-nextjs.use-cache.tag-manifest"
47490
+ );
47491
+
47492
+ // src/adapter/use-cache-common.ts
47114
47493
  function isBuildPhase() {
47115
47494
  return process.env.NEXT_PHASE === "phase-production-build";
47116
47495
  }
@@ -47191,6 +47570,12 @@ var EntryLru = class {
47191
47570
  maxBytes;
47192
47571
  entries = /* @__PURE__ */ new Map();
47193
47572
  bytes = 0;
47573
+ /**
47574
+ * No later than the oldest entry's `timestamp`: the last scan's answer, less
47575
+ * whatever was set since. Entries dropped since only make it early. See
47576
+ * {@link oldestBound}.
47577
+ */
47578
+ lowest = Infinity;
47194
47579
  get(key) {
47195
47580
  const entry = this.entries.get(key);
47196
47581
  if (entry) {
@@ -47207,6 +47592,7 @@ var EntryLru = class {
47207
47592
  }
47208
47593
  this.entries.set(key, entry);
47209
47594
  this.bytes += size;
47595
+ this.lowest = Math.min(this.lowest, entry.timestamp);
47210
47596
  for (const [oldestKey, oldest] of this.entries) {
47211
47597
  if (this.bytes <= this.maxBytes) {
47212
47598
  break;
@@ -47220,8 +47606,59 @@ var EntryLru = class {
47220
47606
  if (existing) {
47221
47607
  this.entries.delete(key);
47222
47608
  this.bytes -= sizeOf2(key, existing);
47609
+ if (this.entries.size === 0) {
47610
+ this.lowest = Infinity;
47611
+ }
47223
47612
  }
47224
47613
  }
47614
+ /**
47615
+ * The `timestamp` of the oldest entry held, `Infinity` with none, dropping
47616
+ * on the way the entries past `revalidate` at `at` (`now()`): a handler
47617
+ * that drops them when next read would otherwise hold them until then, and
47618
+ * they would count here meanwhile. A scan, so asked once in a while: in
47619
+ * between, {@link oldestBound} answers.
47620
+ */
47621
+ oldestTimestamp(at = -Infinity) {
47622
+ let oldest = Infinity;
47623
+ for (const [key, entry] of this.entries) {
47624
+ if (isPastRevalidate(entry, at)) {
47625
+ this.delete(key);
47626
+ } else {
47627
+ oldest = Math.min(oldest, entry.timestamp);
47628
+ }
47629
+ }
47630
+ this.lowest = oldest;
47631
+ return oldest;
47632
+ }
47633
+ /**
47634
+ * No later than {@link oldestTimestamp}, without its scan: as of the last
47635
+ * one, less the entries set since. `Infinity` with none held. Drops the
47636
+ * entries past `revalidate` at `at` from the least recently used end, up to
47637
+ * the first that isn't, so {@link holdsLive} doesn't walk past them on every
47638
+ * request until the next scan.
47639
+ */
47640
+ oldestBound(at = -Infinity) {
47641
+ for (const [key, entry] of this.entries) {
47642
+ if (!isPastRevalidate(entry, at)) {
47643
+ break;
47644
+ }
47645
+ this.delete(key);
47646
+ }
47647
+ return this.entries.size === 0 ? Infinity : this.lowest;
47648
+ }
47649
+ /**
47650
+ * Whether an entry not past `revalidate` at `at` is held. Usually the first
47651
+ * entry answers; the expired ones it passes are dropped by the scan
47652
+ * {@link oldestTimestamp} makes, not here, so asking changes nothing.
47653
+ */
47654
+ holdsLive(at) {
47655
+ for (const entry of this.entries.values()) {
47656
+ if (!isPastRevalidate(entry, at)) {
47657
+ return true;
47658
+ }
47659
+ }
47660
+ return false;
47661
+ }
47225
47662
  get size() {
47226
47663
  return this.entries.size;
47227
47664
  }
@@ -47229,14 +47666,39 @@ var EntryLru = class {
47229
47666
  function sizeOf2(key, entry) {
47230
47667
  return entry.value.byteLength + key.length;
47231
47668
  }
47669
+ function isPastRevalidate(stored, at) {
47670
+ return at > stored.timestamp + stored.revalidate * 1e3;
47671
+ }
47232
47672
  var DEFAULT_MEMORY_BYTES = 50 * 1024 * 1024;
47233
- function tagMethods(tags) {
47673
+ var TAG_TABLE_REQUEST_TIMEOUT_MS = 3e3;
47674
+ async function awaitIf(wait, work, failure) {
47675
+ const logged = work.catch((error2) => {
47676
+ console.error(failure, error2);
47677
+ });
47678
+ if (wait) {
47679
+ await logged;
47680
+ }
47681
+ }
47682
+ function tagMethods(tags, options = {}) {
47234
47683
  return {
47235
- refreshTags: () => tags.refresh(),
47236
- async getExpiration(implicitTags) {
47237
- await tags.ensure(implicitTags);
47238
- return tags.expiration(implicitTags);
47239
- },
47684
+ // Started, not awaited, while the instance keeps up, unless `blocking`.
47685
+ // Next.js awaits `refreshTags` inside the first `'use cache'` lookup of a
47686
+ // request (`use-cache-wrapper`), and a staged render ends its static stage
47687
+ // on a timer: a log `Query` there pushed the entry out of the static
47688
+ // stage, so a cached navigation stored the page segment without it. What
47689
+ // the query finds applies from the next request. Awaited once the instance
47690
+ // is `behind`, which the runtime settles before a page render starts
47691
+ // (`catchUp`): the first request after an idle or frozen spell would
47692
+ // otherwise serve whatever was revalidated elsewhere meanwhile. A page's
47693
+ // render just after `catchUp` gets a grace more (`behindInRender`), so a
47694
+ // request that came in just short of the line doesn't cross it inside
47695
+ // the render. Not even then while the instance holds no entry the query
47696
+ // could expire (`holdsEntries`), with a `refreshIntervalMs` of `0` too.
47697
+ refreshTags: () => awaitIf(
47698
+ options.blocking === true || tags.behindInRender && tags.holdsEntries,
47699
+ tags.refresh(),
47700
+ "Error refreshing cache tags:"
47701
+ ),
47240
47702
  async updateTags(revalidatedTags, durations) {
47241
47703
  await tags.update(revalidatedTags, durations);
47242
47704
  }
@@ -47253,7 +47715,6 @@ function lazyHandler(create) {
47253
47715
  updateTags: (tags, durations) => instance().updateTags(tags, durations)
47254
47716
  };
47255
47717
  }
47256
- var TAG_MANIFEST_SYMBOL = /* @__PURE__ */ Symbol.for("cdk-nextjs.use-cache.tag-manifest");
47257
47718
  function sharedTagManifest() {
47258
47719
  const global = globalThis;
47259
47720
  if (!global[TAG_MANIFEST_SYMBOL]) {
@@ -47263,12 +47724,24 @@ function sharedTagManifest() {
47263
47724
  DEFAULT_TAG_REFRESH_MS
47264
47725
  );
47265
47726
  if (config.tableName && !isBuildPhase()) {
47266
- const client = new import_client_dynamodb2.DynamoDBClient({ region: config.region });
47727
+ const client = new import_client_dynamodb2.DynamoDBClient({
47728
+ region: config.region,
47729
+ // Without `throwOnRequestTimeout` the timeout only logs a warning. It
47730
+ // ends once the response headers arrive: `socketTimeout`, an
47731
+ // inactivity timer kept until the request closes, covers the body.
47732
+ requestHandler: {
47733
+ requestTimeout: TAG_TABLE_REQUEST_TIMEOUT_MS,
47734
+ throwOnRequestTimeout: true,
47735
+ socketTimeout: TAG_TABLE_REQUEST_TIMEOUT_MS
47736
+ }
47737
+ });
47267
47738
  global[TAG_MANIFEST_SYMBOL] = new TrackedTagMarkers({
47268
47739
  markers: new TagMarkerTable(client, config.tableName, config.buildId),
47269
47740
  log: new RevalidationLog(client, config.tableName, config.buildId),
47270
47741
  refreshIntervalMs,
47271
- debug: (0, import_debug5.default)("cdk-nextjs:cache-handler:tags")
47742
+ debug: (0, import_debug5.default)("cdk-nextjs:cache-handler:tags"),
47743
+ // Set in every Lambda runtime; a container is never frozen.
47744
+ canFreeze: process.env.AWS_LAMBDA_FUNCTION_NAME !== void 0
47272
47745
  });
47273
47746
  } else {
47274
47747
  if (!isBuildPhase()) {
@@ -47298,9 +47771,6 @@ function bucketFromEnv() {
47298
47771
  }
47299
47772
  return new CacheBucket(new import_client_s32.S3Client({ region }), bucketName);
47300
47773
  }
47301
- function isPastRevalidate(stored, at) {
47302
- return at > stored.timestamp + stored.revalidate * 1e3;
47303
- }
47304
47774
  function isPastExpire(stored, at) {
47305
47775
  return at > stored.timestamp + stored.expire * 1e3;
47306
47776
  }
@@ -47420,7 +47890,13 @@ function createRemoteUseCacheHandler(options = {}) {
47420
47890
  done();
47421
47891
  await writeToS3(cacheKey, stored);
47422
47892
  },
47423
- ...tagMethods(tags)
47893
+ // Waits for the log query on every request: an entry read from S3 may
47894
+ // predate this instance, so what the query says applies to it at once.
47895
+ ...tagMethods(tags, { blocking: true }),
47896
+ async getExpiration(implicitTags) {
47897
+ await tags.ensure(implicitTags);
47898
+ return tags.expiration(implicitTags);
47899
+ }
47424
47900
  };
47425
47901
  }
47426
47902
  var use_cache_remote_handler_default = lazyHandler(() => createRemoteUseCacheHandler());