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
@@ -34169,6 +34169,7 @@ var import_client_dynamodb2 = __toESM(require_dist_cjs19());
34169
34169
  var import_debug5 = __toESM(require_src());
34170
34170
 
34171
34171
  // src/adapter/aws-cache-store.ts
34172
+ import { AsyncLocalStorage } from "node:async_hooks";
34172
34173
  var import_client_dynamodb = __toESM(require_dist_cjs19());
34173
34174
 
34174
34175
  // src/adapter/cache-utils.ts
@@ -34240,6 +34241,7 @@ function markerOf(item) {
34240
34241
  }
34241
34242
  var BATCH_GET_MAX_KEYS = 100;
34242
34243
  var DEFAULT_TAG_REFRESH_MS = 1e3;
34244
+ var TAG_REFRESH_GRACE_MS = 1e3;
34243
34245
  function hashedTag(tag) {
34244
34246
  return `#${sha256Hex(tag)}`;
34245
34247
  }
@@ -34453,10 +34455,64 @@ var TrackedTagMarkers = class {
34453
34455
  * result rather than dropped.
34454
34456
  */
34455
34457
  pendingRows = /* @__PURE__ */ new Map();
34458
+ /**
34459
+ * Log rows for tags not tracked and not being read, merged by tag, oldest
34460
+ * first: what the log has said about them since {@link completeSince}. Taken
34461
+ * into a tag's marker when it is read or written.
34462
+ */
34463
+ logOnly = /* @__PURE__ */ new Map();
34464
+ /** See {@link completeSince}. */
34465
+ floor;
34466
+ /**
34467
+ * The future `expiredAt`s of markers dropped since, as `[from, to]`, in
34468
+ * order: once `from` is past, {@link completeSince} moves to `to`, and not
34469
+ * before, since only then does it expire anything. `from` and `to` are the
34470
+ * same until spans are merged for space ({@link laterFloor}).
34471
+ */
34472
+ laterFloors = [];
34473
+ /** The in-memory entry stores judged by {@link completeSince}. */
34474
+ entryStores = [];
34475
+ /** How many times {@link forget} ran: a read started before one is made again. */
34476
+ forgotten = 0;
34456
34477
  /** Tracked tags whose read failed, for the next refresh to read again. */
34457
34478
  unread = /* @__PURE__ */ new Set();
34479
+ /**
34480
+ * Set inside a page render {@link catchUp} let go, for that render alone.
34481
+ * See {@link behindInRender}.
34482
+ */
34483
+ caughtUpRender = new AsyncLocalStorage();
34484
+ /** When `sync` last scanned the entry stores ({@link oldestEntry}). */
34485
+ lastEntryScan = -Infinity;
34486
+ /**
34487
+ * No `logOnly` row is known by it or earlier, so a {@link pruneLogged}
34488
+ * against it or earlier drops nothing: a marker is only ever known by later,
34489
+ * as its `expiredAt` passes.
34490
+ */
34491
+ prunedAgainst = -Infinity;
34492
+ /** When the last refresh started, or was skipped. */
34458
34493
  lastRefresh = -Infinity;
34494
+ /**
34495
+ * The refresh in flight. One left open across a freeze comes back after the
34496
+ * thaw with what it read before, so {@link refresh} waits for it and then
34497
+ * for a fresh one, rather than the runtime waiting for it before the freeze:
34498
+ * that would bill every Lambda invocation for the query's tail.
34499
+ */
34459
34500
  refreshing;
34501
+ /**
34502
+ * When {@link refreshing} last showed the process running: a timer ticks
34503
+ * while it is in flight, and a tick that comes late, or none at all, means
34504
+ * the process was paused - frozen - since it started.
34505
+ */
34506
+ lastTick = -Infinity;
34507
+ /** Whether a tick came late since {@link refreshing} started. */
34508
+ pausedWhileRefreshing = false;
34509
+ ticker;
34510
+ canFreeze;
34511
+ /**
34512
+ * When the last refresh settled, either way, unless it spanned a pause: what
34513
+ * it knows is from then. See {@link behind}.
34514
+ */
34515
+ settledAt;
34460
34516
  /**
34461
34517
  * Where the next log query starts (`clock()`): the last successful one's
34462
34518
  * start, less {@link REVALIDATION_LOG_LOOKBACK_MS}.
@@ -34478,20 +34534,68 @@ var TrackedTagMarkers = class {
34478
34534
  this.random = options.random ?? Math.random;
34479
34535
  this.debug = options.debug ?? (() => {
34480
34536
  });
34537
+ this.canFreeze = options.canFreeze ?? false;
34481
34538
  const at = this.clock();
34482
34539
  this.cursor = at - REVALIDATION_LOG_LOOKBACK_MS;
34483
34540
  this.lastLogRead = at;
34541
+ this.settledAt = at;
34542
+ this.floor = markerClock();
34484
34543
  }
34485
34544
  /** `tag`'s marker as this instance knows it, if it tracks `tag`. */
34486
34545
  get(tag) {
34487
34546
  return this.tags.get(tag);
34488
34547
  }
34548
+ /**
34549
+ * Since when (`markerClock()`) this instance knows of every revalidation of
34550
+ * every tag, tracked or not: tracked ones from their markers and the log,
34551
+ * the others from the log alone (`logOnly`). So an entry created at or after
34552
+ * it can be judged by {@link state} and {@link expiration} without
34553
+ * {@link ensure} reading anything - which matters inside a `'use cache'`
34554
+ * lookup, where Next.js waits for the answer and a read pushes the entry out
34555
+ * of the static stage of a staged render.
34556
+ *
34557
+ * Starts at construction: the log is followed from before it. Moves to now
34558
+ * when the tracked markers are forgotten, and past the revalidations of
34559
+ * whatever else it lets go of - a marker evicted for space, a log row
34560
+ * pruned - which are no longer known: to its `expiredAt` only once that is
34561
+ * past, since a profile's `expire` can be a year out, and moving there at
34562
+ * once would turn the read-free path off for the instance's life.
34563
+ *
34564
+ * A row is pruned only once it is no later than every entry held, so that
34565
+ * costs those entries nothing. An entry stored afterwards with an older
34566
+ * `timestamp` - Next.js stamps the start of its generation, which can
34567
+ * outlast a lookback - is older than this too, and is read first.
34568
+ *
34569
+ * It does not cover a revalidation older than the log's first lookback
34570
+ * whose `expiredAt` is still to come, which would expire an entry created
34571
+ * after it. So the `'use cache'` handler reads an entry's tags in the
34572
+ * background as it stores it.
34573
+ */
34574
+ get completeSince() {
34575
+ const at = markerClock();
34576
+ while (this.laterFloors.length > 0 && this.laterFloors[0][0] <= at) {
34577
+ this.floor = Math.max(this.floor, this.laterFloors.shift()[1]);
34578
+ }
34579
+ return this.floor;
34580
+ }
34581
+ /**
34582
+ * Register an in-memory store whose entries are judged by
34583
+ * {@link completeSince}, by the `timestamp` of the oldest entry it holds:
34584
+ * `logOnly` keeps the rows that could still apply to one of them, and no
34585
+ * others. With none registered it keeps nothing. Asked with `scan` false,
34586
+ * `oldestTimestamp` may answer early, but never late, without a scan.
34587
+ */
34588
+ judgeEntriesOf(oldestTimestamp, holdsAny = () => oldestTimestamp(false) < Infinity) {
34589
+ this.entryStores.push({ oldestTimestamp, holdsAny });
34590
+ }
34489
34591
  /**
34490
34592
  * Track `tag` with `marker`: what this instance just wrote, which it knows
34491
34593
  * without reading it back.
34492
34594
  */
34493
34595
  set(tag, marker) {
34494
- this.remember(tag, marker);
34596
+ const logged = this.logOnly.get(tag);
34597
+ this.logOnly.delete(tag);
34598
+ this.remember(tag, mergeMarkers(marker, logged));
34495
34599
  this.known(tag, this.clock());
34496
34600
  }
34497
34601
  /**
@@ -34544,27 +34648,103 @@ var TrackedTagMarkers = class {
34544
34648
  const unread = [];
34545
34649
  for (const tag of new Set(tags)) {
34546
34650
  const inFlight = this.reading.get(tag);
34547
- if (inFlight) {
34548
- waits.push(inFlight);
34549
- } else if (this.tags.has(tag)) {
34651
+ if (this.tags.has(tag)) {
34550
34652
  this.touch(tag);
34653
+ } else if (inFlight) {
34654
+ waits.push(inFlight);
34551
34655
  } else {
34552
34656
  unread.push(tag);
34553
34657
  }
34554
34658
  }
34555
34659
  if (unread.length > 0) {
34556
- const read = this.readInto(unread).then(() => {
34557
- for (const tag of unread) {
34558
- this.reading.delete(tag);
34559
- }
34560
- });
34561
- for (const tag of unread) {
34562
- this.reading.set(tag, read);
34563
- }
34564
- waits.push(read);
34660
+ waits.push(this.startRead(unread));
34565
34661
  }
34566
34662
  await Promise.all(waits);
34567
34663
  }
34664
+ /**
34665
+ * {@link readInto} `tags`, as the read of each that whoever needs one of
34666
+ * them next waits on ({@link reading}). Never rejects: a failed read leaves
34667
+ * the tags tracked.
34668
+ */
34669
+ startRead(tags) {
34670
+ const read = this.readInto(tags).then(() => {
34671
+ for (const tag of tags) {
34672
+ this.reading.delete(tag);
34673
+ }
34674
+ });
34675
+ for (const tag of tags) {
34676
+ this.reading.set(tag, read);
34677
+ }
34678
+ return read;
34679
+ }
34680
+ /**
34681
+ * Whether the last refresh settled more than `refreshIntervalMs` plus
34682
+ * {@link TAG_REFRESH_GRACE_MS} ago - one that spanned a freeze counts from
34683
+ * when it started: after the instance sat idle or frozen - or always, with
34684
+ * a `refreshIntervalMs` of `0`. Answering from what it knows then could
34685
+ * serve an entry revalidated elsewhere since, however long ago that was, so
34686
+ * `refreshTags` waits for the refresh, and the runtime waits for it before
34687
+ * a page render starts ({@link catchUp}).
34688
+ *
34689
+ * From when it settled, not when it started, so a slow query the runtime
34690
+ * waited for before the render does not leave the instance behind inside
34691
+ * it. A failed one counts too, so one request does not wait for two: the
34692
+ * same "assume the entry is valid" a failed read answers with, until the
34693
+ * next refresh, at most the interval and the grace later.
34694
+ */
34695
+ get behind() {
34696
+ return this.log !== void 0 && // `0` asks before every check, so every check waits for the answer.
34697
+ (this.refreshIntervalMs === 0 || this.overdue(this.settledAt));
34698
+ }
34699
+ /**
34700
+ * {@link behind}, as `refreshTags` asks it: never inside a page's render
34701
+ * {@link catchUp} caught up, since the runtime settled it for that request
34702
+ * before the render started, and a wait inside a `'use cache'` lookup is
34703
+ * what it settled it to avoid - a render crossing the line partway through
34704
+ * would otherwise wait after all. So a revalidation elsewhere can go unseen
34705
+ * for a render's duration on top of the usual window.
34706
+ *
34707
+ * A route handler, which no catch-up precedes, waits at the plain line.
34708
+ */
34709
+ get behindInRender() {
34710
+ return this.refreshIntervalMs !== 0 && this.caughtUpRender.getStore() ? false : this.behind;
34711
+ }
34712
+ /**
34713
+ * Whether a registered store ({@link judgeEntriesOf}) holds any entry: one
34714
+ * a refresh could expire. With none there is nothing to wait for even when
34715
+ * the instance is {@link behind}, since whatever is stored from here on is
34716
+ * newer than what the log would say - which covers the first `'use cache'`
34717
+ * call of a process, which creates the handler inside the render.
34718
+ */
34719
+ get holdsEntries() {
34720
+ return this.entryStores.some((store) => store.holdsAny());
34721
+ }
34722
+ /**
34723
+ * {@link refresh}, waited for when the instance is {@link behind}. The
34724
+ * runtime calls it before handing a page request to Next.js, outside the
34725
+ * staged render whose static stage a wait inside a `'use cache'` lookup
34726
+ * would cut short. Only for the `'use cache'` handler's entries
34727
+ * ({@link judgeEntriesOf}): the other caches wait for the refresh where they
34728
+ * read anyway. Not with a `refreshIntervalMs` of `0` either, where
34729
+ * `refreshTags` asks again regardless. Then `render`, marked as caught up
34730
+ * for ({@link behindInRender}). Rejects only as `render` does.
34731
+ */
34732
+ async catchUp(render) {
34733
+ const log = (error2) => console.error("Error refreshing cache tags:", error2);
34734
+ if (this.refreshIntervalMs > 0 && this.behind) {
34735
+ if (!this.holdsEntries) {
34736
+ return render();
34737
+ }
34738
+ await this.refresh().catch(log);
34739
+ } else if (this.refreshIntervalMs > 0) {
34740
+ void this.refresh().catch(log);
34741
+ }
34742
+ return this.caughtUpRender.run(true, render);
34743
+ }
34744
+ /** Whether `at` is more than the interval and the grace ago. */
34745
+ overdue(at) {
34746
+ return this.clock() - at > this.refreshIntervalMs + TAG_REFRESH_GRACE_MS;
34747
+ }
34568
34748
  /**
34569
34749
  * Catch up on revalidations other instances ran, at most once per
34570
34750
  * `refreshIntervalMs`. See {@link TrackedTagMarkers} for the cost.
@@ -34575,6 +34755,9 @@ var TrackedTagMarkers = class {
34575
34755
  return;
34576
34756
  }
34577
34757
  if (this.refreshing) {
34758
+ if (this.behind && this.pausedSinceRefreshStarted()) {
34759
+ return this.refreshing.then(() => this.refresh());
34760
+ }
34578
34761
  return this.refreshing;
34579
34762
  }
34580
34763
  const at = this.clock();
@@ -34582,14 +34765,47 @@ var TrackedTagMarkers = class {
34582
34765
  return;
34583
34766
  }
34584
34767
  this.lastRefresh = at;
34585
- if (this.tags.size === 0) {
34768
+ if (this.tags.size === 0 && this.entryStores.length === 0) {
34586
34769
  return;
34587
34770
  }
34771
+ this.startTicking();
34588
34772
  this.refreshing = this.sync(at, log).finally(() => {
34773
+ this.settledAt = Math.max(
34774
+ this.settledAt,
34775
+ this.pausedSinceRefreshStarted() ? this.lastRefresh : this.clock()
34776
+ );
34777
+ this.stopTicking();
34589
34778
  this.refreshing = void 0;
34590
34779
  });
34591
34780
  return this.refreshing;
34592
34781
  }
34782
+ startTicking() {
34783
+ if (!this.canFreeze) {
34784
+ return;
34785
+ }
34786
+ this.lastTick = this.clock();
34787
+ this.pausedWhileRefreshing = false;
34788
+ this.ticker = setInterval(() => {
34789
+ const at = this.clock();
34790
+ if (at - this.lastTick > REFRESH_PAUSE_MS) {
34791
+ this.pausedWhileRefreshing = true;
34792
+ }
34793
+ this.lastTick = at;
34794
+ }, REFRESH_TICK_MS);
34795
+ this.ticker.unref?.();
34796
+ }
34797
+ stopTicking() {
34798
+ clearInterval(this.ticker);
34799
+ this.ticker = void 0;
34800
+ }
34801
+ /**
34802
+ * Whether the process was paused since {@link refreshing} started: a tick
34803
+ * came late, or the next one is overdue (after a thaw, a request can run
34804
+ * before the timer does).
34805
+ */
34806
+ pausedSinceRefreshStarted() {
34807
+ return this.canFreeze && (this.pausedWhileRefreshing || this.clock() - this.lastTick > REFRESH_PAUSE_MS);
34808
+ }
34593
34809
  /**
34594
34810
  * Record a revalidation of `tags`: here at once, and in the revalidation
34595
34811
  * table for every other instance - the marker row plus a log row per tag.
@@ -34622,7 +34838,7 @@ var TrackedTagMarkers = class {
34622
34838
  const at = markerClock();
34623
34839
  let state2 = "fresh";
34624
34840
  for (const tag of tags) {
34625
- const marker = this.get(tag);
34841
+ const marker = this.knownMarker(tag);
34626
34842
  if (!marker) {
34627
34843
  continue;
34628
34844
  }
@@ -34646,7 +34862,7 @@ var TrackedTagMarkers = class {
34646
34862
  const at = markerClock();
34647
34863
  let latest = 0;
34648
34864
  for (const tag of tags) {
34649
- const marker = this.get(tag);
34865
+ const marker = this.knownMarker(tag);
34650
34866
  if (!marker) {
34651
34867
  continue;
34652
34868
  }
@@ -34733,13 +34949,14 @@ var TrackedTagMarkers = class {
34733
34949
  );
34734
34950
  return;
34735
34951
  }
34736
- const [result] = await Promise.all([
34737
- log.query(this.cursor).catch((error2) => {
34738
- console.error("Error reading tag revalidation log:", error2);
34739
- return void 0;
34740
- }),
34741
- this.readInto(this.due(at))
34742
- ]);
34952
+ const due = this.due(at);
34953
+ if (due.length > 0) {
34954
+ void this.startRead(due);
34955
+ }
34956
+ const result = await log.query(this.cursor).catch((error2) => {
34957
+ console.error("Error reading tag revalidation log:", error2);
34958
+ return void 0;
34959
+ });
34743
34960
  if (!result) {
34744
34961
  return;
34745
34962
  }
@@ -34750,6 +34967,11 @@ var TrackedTagMarkers = class {
34750
34967
  );
34751
34968
  return;
34752
34969
  }
34970
+ const scan = at - this.lastEntryScan > ENTRY_SCAN_INTERVAL_MS;
34971
+ if (scan) {
34972
+ this.lastEntryScan = at;
34973
+ }
34974
+ const oldest = this.oldestEntry(scan);
34753
34975
  let applied = 0;
34754
34976
  for (const row of result.rows) {
34755
34977
  if (this.applied.has(row.sk)) {
@@ -34764,11 +34986,20 @@ var TrackedTagMarkers = class {
34764
34986
  mergeMarkers(this.pendingRows.get(row.tag), row.marker)
34765
34987
  );
34766
34988
  } else {
34989
+ this.rememberLogged(
34990
+ row.tag,
34991
+ mergeMarkers(this.logOnly.get(row.tag), row.marker),
34992
+ oldest
34993
+ );
34767
34994
  continue;
34768
34995
  }
34769
34996
  this.applied.set(row.sk, row.at);
34770
34997
  applied++;
34771
34998
  }
34999
+ if (oldest > this.prunedAgainst) {
35000
+ this.pruneLogged(oldest);
35001
+ }
35002
+ this.prunedAgainst = oldest;
34772
35003
  this.lastLogRead = at;
34773
35004
  this.advance(at - REVALIDATION_LOG_LOOKBACK_MS);
34774
35005
  this.debug(
@@ -34784,14 +35015,16 @@ var TrackedTagMarkers = class {
34784
35015
  const due = [];
34785
35016
  for (const tag of this.unread) {
34786
35017
  if (due.length >= BATCH_GET_MAX_KEYS) return due;
34787
- due.push(tag);
35018
+ if (!this.reading.has(tag)) {
35019
+ due.push(tag);
35020
+ }
34788
35021
  }
34789
35022
  const jitter = this.resyncIntervalMs * TAG_RESYNC_JITTER;
34790
35023
  for (const [tag, dueAt] of this.dueAt) {
34791
35024
  if (due.length >= BATCH_GET_MAX_KEYS || dueAt - jitter > at) {
34792
35025
  break;
34793
35026
  }
34794
- if (dueAt <= at && !this.unread.has(tag)) {
35027
+ if (dueAt <= at && !this.unread.has(tag) && !this.reading.has(tag)) {
34795
35028
  due.push(tag);
34796
35029
  }
34797
35030
  }
@@ -34804,10 +35037,19 @@ var TrackedTagMarkers = class {
34804
35037
  */
34805
35038
  forget(at, why) {
34806
35039
  this.debug(`forgetting ${this.tags.size} tracked tags: ${why}`);
35040
+ this.forgotten++;
35041
+ for (const marker of this.tags.values()) {
35042
+ this.raiseFloor(marker);
35043
+ }
35044
+ for (const marker of this.logOnly.values()) {
35045
+ this.raiseFloor(marker);
35046
+ }
34807
35047
  this.tags.clear();
34808
35048
  this.dueAt.clear();
34809
35049
  this.unread.clear();
34810
35050
  this.applied.clear();
35051
+ this.logOnly.clear();
35052
+ this.floor = Math.max(this.floor, markerClock());
34811
35053
  this.cursor = at - REVALIDATION_LOG_LOOKBACK_MS;
34812
35054
  this.lastLogRead = at;
34813
35055
  }
@@ -34833,13 +35075,17 @@ var TrackedTagMarkers = class {
34833
35075
  * always answered with, rather than a read on every request while DynamoDB
34834
35076
  * is unavailable.
34835
35077
  */
34836
- async readInto(tags) {
35078
+ async readInto(tags, again = false) {
34837
35079
  if (tags.length === 0) {
34838
35080
  return true;
34839
35081
  }
34840
35082
  const at = this.clock();
35083
+ const forgotten = this.forgotten;
34841
35084
  try {
34842
35085
  const read = await this.markers.read(tags);
35086
+ if (this.forgotten !== forgotten && !again) {
35087
+ return await this.readInto(tags, true);
35088
+ }
34843
35089
  for (const tag of tags) {
34844
35090
  this.unread.delete(tag);
34845
35091
  this.remember(
@@ -34855,6 +35101,9 @@ var TrackedTagMarkers = class {
34855
35101
  return true;
34856
35102
  } catch (error2) {
34857
35103
  console.error("Error reading tag markers:", error2);
35104
+ if (this.forgotten !== forgotten && !again) {
35105
+ return this.readInto(tags, true);
35106
+ }
34858
35107
  this.trackUnread(tags);
34859
35108
  for (const tag of tags) {
34860
35109
  this.unread.add(tag);
@@ -34868,8 +35117,119 @@ var TrackedTagMarkers = class {
34868
35117
  }
34869
35118
  takePendingRows(tag) {
34870
35119
  const pending = this.pendingRows.get(tag);
35120
+ const logged = this.logOnly.get(tag);
34871
35121
  this.pendingRows.delete(tag);
34872
- return pending;
35122
+ this.logOnly.delete(tag);
35123
+ return logged ? mergeMarkers(logged, pending) : pending;
35124
+ }
35125
+ /**
35126
+ * What this instance knows of `tag`: tracked, or from the log alone -
35127
+ * including rows that arrived while its first read is in flight.
35128
+ */
35129
+ knownMarker(tag) {
35130
+ const tracked = this.tags.get(tag);
35131
+ if (tracked) {
35132
+ return tracked;
35133
+ }
35134
+ const logged = this.logOnly.get(tag);
35135
+ const pending = this.pendingRows.get(tag);
35136
+ return logged || pending ? mergeMarkers(logged, pending) : void 0;
35137
+ }
35138
+ /**
35139
+ * Keep `tag`'s log rows in `logOnly`, unless they cannot apply to an entry
35140
+ * held, `oldest` or newer (see {@link pruneLogged}), as bounded as the
35141
+ * tracked tags. Either way a row let go of takes what it knew with it, so
35142
+ * `completeSince` moves past it.
35143
+ */
35144
+ rememberLogged(tag, marker, oldest) {
35145
+ this.logOnly.delete(tag);
35146
+ if (knownBy(marker, markerClock()) <= oldest) {
35147
+ this.raiseFloor(marker);
35148
+ return;
35149
+ }
35150
+ this.logOnly.set(tag, marker);
35151
+ for (const [first, dropped] of this.logOnly) {
35152
+ if (this.logOnly.size <= this.maxTrackedTags) {
35153
+ break;
35154
+ }
35155
+ this.logOnly.delete(first);
35156
+ this.raiseFloor(dropped);
35157
+ }
35158
+ }
35159
+ /**
35160
+ * Drop the `logOnly` rows that cannot apply to an entry held, `oldest` or
35161
+ * newer: every revalidation they name so far is no later than it. That
35162
+ * moves `completeSince` no further than `oldest`, which costs the entries
35163
+ * held nothing, and a profile's `expiredAt` still to come waits in
35164
+ * `laterFloors`. Without this, a deployment revalidating many distinct tags
35165
+ * filled `logOnly` with tags no entry here carries, and the evictions past
35166
+ * its bound kept moving `completeSince` up to the present.
35167
+ */
35168
+ pruneLogged(oldest) {
35169
+ const at = markerClock();
35170
+ for (const [tag, marker] of this.logOnly) {
35171
+ if (knownBy(marker, at) <= oldest) {
35172
+ this.logOnly.delete(tag);
35173
+ this.raiseFloor(marker);
35174
+ }
35175
+ }
35176
+ }
35177
+ /**
35178
+ * The oldest entry timestamp of any registered store, `Infinity` with none:
35179
+ * early, never late, unless `scan`.
35180
+ */
35181
+ oldestEntry(scan) {
35182
+ let oldest = Infinity;
35183
+ for (const { oldestTimestamp } of this.entryStores) {
35184
+ oldest = Math.min(oldest, oldestTimestamp(scan));
35185
+ }
35186
+ return oldest;
35187
+ }
35188
+ /**
35189
+ * Move `completeSince` past `marker`'s revalidations: what is no longer
35190
+ * known about a tag once its marker is dropped. A future `expiredAt` waits
35191
+ * in `laterFloors` until it is past.
35192
+ */
35193
+ raiseFloor(marker) {
35194
+ const at = markerClock();
35195
+ this.floor = Math.max(this.floor, knownBy(marker, at));
35196
+ const { expiredAt } = marker;
35197
+ if (expiredAt !== void 0 && expiredAt > Math.max(at, this.floor)) {
35198
+ this.laterFloor(expiredAt);
35199
+ }
35200
+ }
35201
+ /**
35202
+ * Add `time` to `laterFloors`, in order. Past the tracked-tag bound the two
35203
+ * spans closest together are merged: from the first's start,
35204
+ * `completeSince` is the second's end, early - which only costs reads, and
35205
+ * for as short a time as any merge allows. Never later than its time, which
35206
+ * would let an entry expire unseen.
35207
+ */
35208
+ laterFloor(time2) {
35209
+ const floors = this.laterFloors;
35210
+ let low = 0;
35211
+ let high = floors.length;
35212
+ while (low < high) {
35213
+ const mid = Math.floor((low + high) / 2);
35214
+ if (floors[mid][0] < time2) {
35215
+ low = mid + 1;
35216
+ } else {
35217
+ high = mid;
35218
+ }
35219
+ }
35220
+ if (low > 0 && floors[low - 1][1] >= time2 || floors[low]?.[0] === time2) {
35221
+ return;
35222
+ }
35223
+ floors.splice(low, 0, [time2, time2]);
35224
+ if (floors.length > this.maxTrackedTags) {
35225
+ let merge2 = 0;
35226
+ for (let i5 = 1; i5 < floors.length - 1; i5++) {
35227
+ if (floors[i5 + 1][1] - floors[i5][0] < floors[merge2 + 1][1] - floors[merge2][0]) {
35228
+ merge2 = i5;
35229
+ }
35230
+ }
35231
+ floors.splice(merge2, 2, [floors[merge2][0], floors[merge2 + 1][1]]);
35232
+ }
34873
35233
  }
34874
35234
  /** Record that `tag`'s marker was known from the table at `at`. */
34875
35235
  known(tag, at) {
@@ -34886,6 +35246,7 @@ var TrackedTagMarkers = class {
34886
35246
  if (this.tags.size <= this.maxTrackedTags) {
34887
35247
  break;
34888
35248
  }
35249
+ this.raiseFloor(this.tags.get(oldest));
34889
35250
  this.tags.delete(oldest);
34890
35251
  this.dueAt.delete(oldest);
34891
35252
  this.unread.delete(oldest);
@@ -34898,7 +35259,18 @@ var TrackedTagMarkers = class {
34898
35259
  }
34899
35260
  }
34900
35261
  };
35262
+ function knownBy(marker, at) {
35263
+ const { revalidatedAt, staleAt, expiredAt } = marker;
35264
+ return Math.max(
35265
+ revalidatedAt ?? -Infinity,
35266
+ staleAt ?? -Infinity,
35267
+ expiredAt !== void 0 && expiredAt <= at ? expiredAt : -Infinity
35268
+ );
35269
+ }
34901
35270
  var MAX_LOG_PUT_MS = REVALIDATION_LOG_LOOKBACK_MS / 2;
35271
+ var ENTRY_SCAN_INTERVAL_MS = 6e4;
35272
+ var REFRESH_TICK_MS = 250;
35273
+ var REFRESH_PAUSE_MS = 1e3;
34902
35274
  function mergeMarkers(local, remote) {
34903
35275
  if (!local) {
34904
35276
  return remote ?? {};
@@ -34918,6 +35290,11 @@ function maxDefined(a5, b5) {
34918
35290
  return Math.max(a5, b5);
34919
35291
  }
34920
35292
 
35293
+ // src/runtime/tag-manifest.ts
35294
+ var TAG_MANIFEST_SYMBOL = /* @__PURE__ */ Symbol.for(
35295
+ "cdk-nextjs.use-cache.tag-manifest"
35296
+ );
35297
+
34921
35298
  // src/adapter/use-cache-common.ts
34922
35299
  function isBuildPhase() {
34923
35300
  return process.env.NEXT_PHASE === "phase-production-build";
@@ -34999,6 +35376,12 @@ var EntryLru = class {
34999
35376
  maxBytes;
35000
35377
  entries = /* @__PURE__ */ new Map();
35001
35378
  bytes = 0;
35379
+ /**
35380
+ * No later than the oldest entry's `timestamp`: the last scan's answer, less
35381
+ * whatever was set since. Entries dropped since only make it early. See
35382
+ * {@link oldestBound}.
35383
+ */
35384
+ lowest = Infinity;
35002
35385
  get(key) {
35003
35386
  const entry = this.entries.get(key);
35004
35387
  if (entry) {
@@ -35015,6 +35398,7 @@ var EntryLru = class {
35015
35398
  }
35016
35399
  this.entries.set(key, entry);
35017
35400
  this.bytes += size;
35401
+ this.lowest = Math.min(this.lowest, entry.timestamp);
35018
35402
  for (const [oldestKey, oldest] of this.entries) {
35019
35403
  if (this.bytes <= this.maxBytes) {
35020
35404
  break;
@@ -35028,7 +35412,58 @@ var EntryLru = class {
35028
35412
  if (existing) {
35029
35413
  this.entries.delete(key);
35030
35414
  this.bytes -= sizeOf2(key, existing);
35415
+ if (this.entries.size === 0) {
35416
+ this.lowest = Infinity;
35417
+ }
35418
+ }
35419
+ }
35420
+ /**
35421
+ * The `timestamp` of the oldest entry held, `Infinity` with none, dropping
35422
+ * on the way the entries past `revalidate` at `at` (`now()`): a handler
35423
+ * that drops them when next read would otherwise hold them until then, and
35424
+ * they would count here meanwhile. A scan, so asked once in a while: in
35425
+ * between, {@link oldestBound} answers.
35426
+ */
35427
+ oldestTimestamp(at = -Infinity) {
35428
+ let oldest = Infinity;
35429
+ for (const [key, entry] of this.entries) {
35430
+ if (isPastRevalidate(entry, at)) {
35431
+ this.delete(key);
35432
+ } else {
35433
+ oldest = Math.min(oldest, entry.timestamp);
35434
+ }
35435
+ }
35436
+ this.lowest = oldest;
35437
+ return oldest;
35438
+ }
35439
+ /**
35440
+ * No later than {@link oldestTimestamp}, without its scan: as of the last
35441
+ * one, less the entries set since. `Infinity` with none held. Drops the
35442
+ * entries past `revalidate` at `at` from the least recently used end, up to
35443
+ * the first that isn't, so {@link holdsLive} doesn't walk past them on every
35444
+ * request until the next scan.
35445
+ */
35446
+ oldestBound(at = -Infinity) {
35447
+ for (const [key, entry] of this.entries) {
35448
+ if (!isPastRevalidate(entry, at)) {
35449
+ break;
35450
+ }
35451
+ this.delete(key);
35452
+ }
35453
+ return this.entries.size === 0 ? Infinity : this.lowest;
35454
+ }
35455
+ /**
35456
+ * Whether an entry not past `revalidate` at `at` is held. Usually the first
35457
+ * entry answers; the expired ones it passes are dropped by the scan
35458
+ * {@link oldestTimestamp} makes, not here, so asking changes nothing.
35459
+ */
35460
+ holdsLive(at) {
35461
+ for (const entry of this.entries.values()) {
35462
+ if (!isPastRevalidate(entry, at)) {
35463
+ return true;
35464
+ }
35031
35465
  }
35466
+ return false;
35032
35467
  }
35033
35468
  get size() {
35034
35469
  return this.entries.size;
@@ -35037,14 +35472,39 @@ var EntryLru = class {
35037
35472
  function sizeOf2(key, entry) {
35038
35473
  return entry.value.byteLength + key.length;
35039
35474
  }
35475
+ function isPastRevalidate(stored, at) {
35476
+ return at > stored.timestamp + stored.revalidate * 1e3;
35477
+ }
35040
35478
  var DEFAULT_MEMORY_BYTES = 50 * 1024 * 1024;
35041
- function tagMethods(tags) {
35479
+ var TAG_TABLE_REQUEST_TIMEOUT_MS = 3e3;
35480
+ async function awaitIf(wait, work, failure) {
35481
+ const logged = work.catch((error2) => {
35482
+ console.error(failure, error2);
35483
+ });
35484
+ if (wait) {
35485
+ await logged;
35486
+ }
35487
+ }
35488
+ function tagMethods(tags, options = {}) {
35042
35489
  return {
35043
- refreshTags: () => tags.refresh(),
35044
- async getExpiration(implicitTags) {
35045
- await tags.ensure(implicitTags);
35046
- return tags.expiration(implicitTags);
35047
- },
35490
+ // Started, not awaited, while the instance keeps up, unless `blocking`.
35491
+ // Next.js awaits `refreshTags` inside the first `'use cache'` lookup of a
35492
+ // request (`use-cache-wrapper`), and a staged render ends its static stage
35493
+ // on a timer: a log `Query` there pushed the entry out of the static
35494
+ // stage, so a cached navigation stored the page segment without it. What
35495
+ // the query finds applies from the next request. Awaited once the instance
35496
+ // is `behind`, which the runtime settles before a page render starts
35497
+ // (`catchUp`): the first request after an idle or frozen spell would
35498
+ // otherwise serve whatever was revalidated elsewhere meanwhile. A page's
35499
+ // render just after `catchUp` gets a grace more (`behindInRender`), so a
35500
+ // request that came in just short of the line doesn't cross it inside
35501
+ // the render. Not even then while the instance holds no entry the query
35502
+ // could expire (`holdsEntries`), with a `refreshIntervalMs` of `0` too.
35503
+ refreshTags: () => awaitIf(
35504
+ options.blocking === true || tags.behindInRender && tags.holdsEntries,
35505
+ tags.refresh(),
35506
+ "Error refreshing cache tags:"
35507
+ ),
35048
35508
  async updateTags(revalidatedTags, durations) {
35049
35509
  await tags.update(revalidatedTags, durations);
35050
35510
  }
@@ -35061,7 +35521,6 @@ function lazyHandler(create) {
35061
35521
  updateTags: (tags, durations) => instance().updateTags(tags, durations)
35062
35522
  };
35063
35523
  }
35064
- var TAG_MANIFEST_SYMBOL = /* @__PURE__ */ Symbol.for("cdk-nextjs.use-cache.tag-manifest");
35065
35524
  function sharedTagManifest() {
35066
35525
  const global = globalThis;
35067
35526
  if (!global[TAG_MANIFEST_SYMBOL]) {
@@ -35071,12 +35530,24 @@ function sharedTagManifest() {
35071
35530
  DEFAULT_TAG_REFRESH_MS
35072
35531
  );
35073
35532
  if (config.tableName && !isBuildPhase()) {
35074
- const client = new import_client_dynamodb2.DynamoDBClient({ region: config.region });
35533
+ const client = new import_client_dynamodb2.DynamoDBClient({
35534
+ region: config.region,
35535
+ // Without `throwOnRequestTimeout` the timeout only logs a warning. It
35536
+ // ends once the response headers arrive: `socketTimeout`, an
35537
+ // inactivity timer kept until the request closes, covers the body.
35538
+ requestHandler: {
35539
+ requestTimeout: TAG_TABLE_REQUEST_TIMEOUT_MS,
35540
+ throwOnRequestTimeout: true,
35541
+ socketTimeout: TAG_TABLE_REQUEST_TIMEOUT_MS
35542
+ }
35543
+ });
35075
35544
  global[TAG_MANIFEST_SYMBOL] = new TrackedTagMarkers({
35076
35545
  markers: new TagMarkerTable(client, config.tableName, config.buildId),
35077
35546
  log: new RevalidationLog(client, config.tableName, config.buildId),
35078
35547
  refreshIntervalMs,
35079
- debug: (0, import_debug5.default)("cdk-nextjs:cache-handler:tags")
35548
+ debug: (0, import_debug5.default)("cdk-nextjs:cache-handler:tags"),
35549
+ // Set in every Lambda runtime; a container is never frozen.
35550
+ canFreeze: process.env.AWS_LAMBDA_FUNCTION_NAME !== void 0
35080
35551
  });
35081
35552
  } else {
35082
35553
  if (!isBuildPhase()) {
@@ -35100,21 +35571,29 @@ function createDefaultUseCacheHandler(options = {}) {
35100
35571
  );
35101
35572
  const pending = new PendingSets();
35102
35573
  const debug = (0, import_debug6.default)("cdk-nextjs:cache-handler:use-cache:default");
35574
+ tags.judgeEntriesOf(
35575
+ (scan) => scan ? memory.oldestTimestamp(markerClock()) : memory.oldestBound(markerClock()),
35576
+ () => memory.holdsLive(markerClock())
35577
+ );
35578
+ const ensureTags = (needed, tagList) => awaitIf(needed, tags.ensure(tagList), "Error reading cache tags:");
35103
35579
  return {
35104
- async get(cacheKey) {
35580
+ async get(cacheKey, softTags = []) {
35105
35581
  await pending.wait(cacheKey);
35106
35582
  const stored = memory.get(cacheKey);
35107
35583
  if (!stored) {
35108
35584
  debug(`MISS ${cacheKey}`);
35109
35585
  return void 0;
35110
35586
  }
35111
- if (markerClock() > stored.timestamp + stored.revalidate * 1e3) {
35587
+ if (isPastRevalidate(stored, markerClock())) {
35112
35588
  debug(`EXPIRED ${cacheKey}`);
35113
35589
  memory.delete(cacheKey);
35114
35590
  return void 0;
35115
35591
  }
35116
- await tags.ensure(stored.tags);
35117
- const state2 = tags.state(stored.tags, stored.timestamp);
35592
+ await ensureTags(stored.timestamp < tags.completeSince, [
35593
+ ...stored.tags,
35594
+ ...softTags
35595
+ ]);
35596
+ const state2 = tags.expiration(softTags) >= stored.timestamp ? "expired" : tags.state(stored.tags, stored.timestamp);
35118
35597
  if (state2 === "expired") {
35119
35598
  debug(`EXPIRED BY TAG ${cacheKey}`);
35120
35599
  memory.delete(cacheKey);
@@ -35132,7 +35611,7 @@ function createDefaultUseCacheHandler(options = {}) {
35132
35611
  return;
35133
35612
  }
35134
35613
  memory.set(cacheKey, stored);
35135
- tags.track(stored.tags);
35614
+ void ensureTags(false, stored.tags);
35136
35615
  debug(`SET ${cacheKey}`);
35137
35616
  } catch (error2) {
35138
35617
  debug(`SET FAILED ${cacheKey}`, error2);
@@ -35140,7 +35619,11 @@ function createDefaultUseCacheHandler(options = {}) {
35140
35619
  done();
35141
35620
  }
35142
35621
  },
35143
- ...tagMethods(tags)
35622
+ ...tagMethods(tags),
35623
+ // `Infinity` has Next.js pass the implicit tags to `get` instead, so they
35624
+ // are judged against the entry read: whether answering needs a marker
35625
+ // read depends on that entry's `timestamp`, which this does not know.
35626
+ getExpiration: async () => Infinity
35144
35627
  };
35145
35628
  }
35146
35629
  var use_cache_default_handler_default = lazyHandler(() => createDefaultUseCacheHandler());