lambder 4.3.1 → 4.4.1

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.
@@ -8,36 +8,65 @@ const DEFAULT_IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
8
8
  const DEFAULT_CACHE_CONTROL = "public, max-age=3600";
9
9
  const DEFAULT_MEMORY_CACHE_MAX_BYTES = 32 * 1024 * 1024;
10
10
  const DEFAULT_MEMORY_CACHE_MAX_FILE_BYTES = 2 * 1024 * 1024;
11
+ /**
12
+ * Files from a folder on the Lambda's filesystem, typically the build output
13
+ * bundled into the deployment package. Reads stay under root. The default
14
+ * source of servePublicFiles, over publicPath.
15
+ */
16
+ export class LambderLocalFileSource {
17
+ root;
18
+ constructor({ root }) {
19
+ this.root = root;
20
+ }
21
+ async read(relativePath) {
22
+ const fs = await getFS();
23
+ const path = await getPath();
24
+ if (!fs || !path)
25
+ throw new Error("LambderLocalFileSource requires a Node.js environment.");
26
+ const base = path.resolve(this.root);
27
+ const absolute = path.resolve(base, relativePath);
28
+ if (absolute !== base && !absolute.startsWith(base + path.sep))
29
+ return null;
30
+ const stat = await fs.promises.stat(absolute).catch(() => null);
31
+ if (!stat?.isFile())
32
+ return null;
33
+ return { body: await fs.promises.readFile(absolute) };
34
+ }
35
+ }
36
+ /** Strip the leading slash and reject traversal; null for a path that names no file (empty, or a directory). */
37
+ const toRelativePath = (target) => {
38
+ if (target.split("/").some((segment) => segment === ".."))
39
+ return null;
40
+ const relative = target.startsWith("/") ? target.slice(1) : target;
41
+ if (relative === "" || relative.endsWith("/"))
42
+ return null;
43
+ return relative;
44
+ };
11
45
  /**
12
46
  * Terminal public-file handler registered via lambder.servePublicFiles().
13
47
  * Runs only when no route matched, so it can never shadow routes registered
14
- * after it. Serves real files under publicPath (traversal-safe, mime-typed,
48
+ * after it. Serves files from its source (traversal-safe, mime-typed,
15
49
  * memory-cached, immutable-cache heuristic for content-hashed assets) and
16
- * falls through to the route fallback when the file does not exist.
50
+ * falls through to the route fallback when the source has no such file.
17
51
  */
18
52
  export class LambderPublicFilesHandler {
19
- publicPath;
53
+ source;
20
54
  options;
21
55
  fileCache = new Map();
22
56
  fileCacheBytes = 0;
23
- constructor(publicPath, options) {
24
- this.publicPath = publicPath;
57
+ constructor(source, options) {
58
+ this.source = source;
25
59
  this.options = options;
26
60
  }
27
61
  /** Serve the mapped file, or return null to fall through. */
28
62
  async handle(ctx) {
29
- const fs = await getFS();
30
- const path = await getPath();
31
- if (!fs || !path)
32
- throw new Error("servePublicFiles requires a Node.js environment.");
33
63
  const mappedPath = this.options.path ? this.options.path(ctx) : ctx.path;
34
64
  if (!mappedPath)
35
65
  return null;
36
- const publicRoot = path.resolve(this.publicPath);
37
- const filePath = this.resolveSafe(path, publicRoot, mappedPath);
38
- if (!filePath)
66
+ const relativePath = toRelativePath(mappedPath);
67
+ if (relativePath === null)
39
68
  return null;
40
- const file = await this.readFileCached(fs, filePath);
69
+ const file = await this.readCached(relativePath);
41
70
  if (!file)
42
71
  return null;
43
72
  const compressOption = this.options.compress;
@@ -46,61 +75,52 @@ export class LambderPublicFilesHandler {
46
75
  statusCode: 200,
47
76
  headers: {
48
77
  "Content-Type": file.mimeType,
49
- "Cache-Control": this.cacheControlFor(ctx, filePath),
78
+ "Cache-Control": this.cacheControlFor(ctx, relativePath),
50
79
  },
51
80
  body: file.body,
52
81
  compress,
53
82
  });
54
83
  }
55
- /** Join base+target and require the result to stay under base. */
56
- resolveSafe(path, base, target) {
57
- if (target.split("/").some((segment) => segment === ".."))
58
- return null;
59
- const normalizedTarget = target.startsWith("/") ? target.slice(1) : target;
60
- const absolute = path.resolve(base, normalizedTarget);
61
- if (absolute !== base && !absolute.startsWith(base + path.sep))
62
- return null;
63
- return absolute;
64
- }
65
- /** Read a file, caching small files in memory for warm invocations. */
66
- async readFileCached(fs, filePath) {
67
- const cached = this.fileCache.get(filePath);
84
+ /** Read from the source, caching small files in memory for warm invocations. */
85
+ async readCached(relativePath) {
86
+ const cached = this.fileCache.get(relativePath);
68
87
  if (cached)
69
88
  return cached;
70
- const stat = await fs.promises.stat(filePath).catch(() => null);
71
- if (!stat?.isFile())
89
+ const file = await this.source.read(relativePath);
90
+ if (!file)
72
91
  return null;
73
- const body = await fs.promises.readFile(filePath);
74
- const mimeType = mimeTypeResolver.lookup(filePath) || "application/octet-stream";
75
- const entry = { body, mimeType };
92
+ const entry = {
93
+ body: file.body,
94
+ mimeType: file.mimeType || mimeTypeResolver.lookup(relativePath) || "application/octet-stream",
95
+ };
76
96
  const cacheConfig = this.options.memoryCache;
77
97
  if (cacheConfig !== false) {
78
98
  const maxBytes = cacheConfig?.maxBytes ?? DEFAULT_MEMORY_CACHE_MAX_BYTES;
79
99
  const maxFileBytes = cacheConfig?.maxFileBytes ?? DEFAULT_MEMORY_CACHE_MAX_FILE_BYTES;
80
- if (body.length <= maxFileBytes) {
100
+ if (entry.body.length <= maxFileBytes) {
81
101
  // Evict oldest entries until the new file fits the budget.
82
102
  for (const [key, value] of this.fileCache) {
83
- if (this.fileCacheBytes + body.length <= maxBytes)
103
+ if (this.fileCacheBytes + entry.body.length <= maxBytes)
84
104
  break;
85
105
  this.fileCache.delete(key);
86
106
  this.fileCacheBytes -= value.body.length;
87
107
  }
88
- if (this.fileCacheBytes + body.length <= maxBytes) {
89
- this.fileCache.set(filePath, entry);
90
- this.fileCacheBytes += body.length;
108
+ if (this.fileCacheBytes + entry.body.length <= maxBytes) {
109
+ this.fileCache.set(relativePath, entry);
110
+ this.fileCacheBytes += entry.body.length;
91
111
  }
92
112
  }
93
113
  }
94
114
  return entry;
95
115
  }
96
- cacheControlFor(ctx, filePath) {
116
+ cacheControlFor(ctx, relativePath) {
97
117
  const cacheOption = this.options.cacheControl;
98
118
  if (typeof cacheOption === "function")
99
- return cacheOption(ctx, filePath);
119
+ return cacheOption(ctx, relativePath);
100
120
  const immutablePattern = this.options.immutablePattern === false
101
121
  ? null
102
122
  : (this.options.immutablePattern ?? DEFAULT_IMMUTABLE_PATTERN);
103
- if (immutablePattern && immutablePattern.test(filePath)) {
123
+ if (immutablePattern && immutablePattern.test(relativePath)) {
104
124
  return this.options.immutableCacheControl ?? DEFAULT_IMMUTABLE_CACHE_CONTROL;
105
125
  }
106
126
  return cacheOption ?? DEFAULT_CACHE_CONTROL;
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import Lambder from './core/Lambder.js';
2
2
  export default Lambder;
3
3
  export { initLambder } from './core/Lambder.js';
4
4
  export { default as LambderCaller } from "./client/LambderCaller.js";
5
- export type { LambderApiOutcome, LambderApiFailureReason, LambderCallOptions, LambderIdempotencyKeyScope } from "./client/LambderCaller.js";
5
+ export type { LambderApiOutcome, LambderApiFailureReason, LambderCallOptions, LambderCallerOptions, LambderGuardInputsProvider, LambderProvidedGuardInputs, LambderIdempotencyKeyScope } from "./client/LambderCaller.js";
6
6
  export { LambderApiError, isLambderApiError, refuse, LAMBDER_REFUSAL_CODES } from "./shared/LambderApiError.js";
7
7
  export type { LambderApiErrorOptions, LambderRefusalMessage, LambderRefusalCode, LambderRefuseOptions } from "./shared/LambderApiError.js";
8
8
  export { default as LambderResponseBuilder } from "./core/LambderResponseBuilder.js";
@@ -15,10 +15,13 @@ export { LambderTemplatingEngine } from "./core/LambderTemplatingEngine.js";
15
15
  export type { LambderTemplateData, LambderTemplatingEngineOptions } from "./core/LambderTemplatingEngine.js";
16
16
  export type { LambderResponseOptions, LambderRawResponseInit, } from "./core/LambderResponseBuilder.js";
17
17
  export type { LambderRouteMatcher, LambderCorsConfig, LambderCreateOptions, LambderSessionOptions, ConditionFunction, RouteCondition, PathParamsOf, LambderActionTools, LambderHandler, LambderIndexHtmlOptions, } from "./core/Lambder.js";
18
- export { LambderPublicFilesHandler } from "./core/LambderPublicFiles.js";
19
- export type { LambderPublicFilesOptions } from "./core/LambderPublicFiles.js";
18
+ export { LambderPublicFilesHandler, LambderLocalFileSource } from "./core/LambderPublicFiles.js";
19
+ export type { LambderPublicFilesOptions, LambderPublicFileSource, LambderPublicFile } from "./core/LambderPublicFiles.js";
20
+ export { LambderS3FileSource } from "./stores/LambderS3FileSource.js";
21
+ export type { LambderS3FileSourceOptions } from "./stores/LambderS3FileSource.js";
20
22
  export type { LambderSessionCookieOptions } from "./session/LambderSessionController.js";
21
- export type { LambderSessionContext, LambderCreatedSession, LambderSessionDataRefreshConfig, LambderSessionCompressionConfig } from "./session/LambderSessionManager.js";
23
+ export type { LambderSessionContext, LambderCreatedSession, LambderSessionDataRefreshConfig } from "./session/LambderSessionManager.js";
24
+ export type { LambderCompressionOption, LambderCompressionConfig } from "./stores/LambderDdbCompression.js";
22
25
  export { LambderSessionDataRefreshError, LambderSessionReadError } from "./session/LambderSessionManager.js";
23
26
  export { LambderDdbCache } from "./stores/LambderDdbCache.js";
24
27
  export type { LambderDdbCacheOptions, LambderDdbCacheSetOptions, LambderDdbCacheGetOrSetOptions, } from "./stores/LambderDdbCache.js";
package/dist/index.js CHANGED
@@ -15,7 +15,8 @@ export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtm
15
15
  // Comment-based HTML templating engine (build-pipeline-safe slots and conditionals, standalone)
16
16
  export { LambderTemplatingEngine } from "./core/LambderTemplatingEngine.js";
17
17
  // Public file serving
18
- export { LambderPublicFilesHandler } from "./core/LambderPublicFiles.js";
18
+ export { LambderPublicFilesHandler, LambderLocalFileSource } from "./core/LambderPublicFiles.js";
19
+ export { LambderS3FileSource } from "./stores/LambderS3FileSource.js";
19
20
  export { LambderSessionDataRefreshError, LambderSessionReadError } from "./session/LambderSessionManager.js";
20
21
  // DynamoDB-backed compressed cache (standalone, server-only)
21
22
  export { LambderDdbCache } from "./stores/LambderDdbCache.js";
@@ -49,6 +49,13 @@ export default class LambderSessionController<TSessionData = any> {
49
49
  * session and touches no cookies, so it works on any subject.
50
50
  */
51
51
  deleteSessionAllByKey(sessionKey: string): Promise<void>;
52
+ /**
53
+ * Marks the data of every session of the given sessionKey stale, so each
54
+ * renews via dataRefresh on its next read: the way to apply a change to
55
+ * a subject's roles or permissions immediately, without logging them
56
+ * out. Needs no fetched session; requires dataRefresh.
57
+ */
58
+ expireSessionDataAllByKey(sessionKey: string): Promise<void>;
52
59
  endSession(): Promise<void>;
53
60
  endSessionAll(): Promise<void>;
54
61
  }
@@ -151,6 +151,16 @@ export default class LambderSessionController {
151
151
  await this.lambderSessionManager.deleteSessionAllByKey(sessionKey);
152
152
  }
153
153
  ;
154
+ /**
155
+ * Marks the data of every session of the given sessionKey stale, so each
156
+ * renews via dataRefresh on its next read: the way to apply a change to
157
+ * a subject's roles or permissions immediately, without logging them
158
+ * out. Needs no fetched session; requires dataRefresh.
159
+ */
160
+ async expireSessionDataAllByKey(sessionKey) {
161
+ await this.lambderSessionManager.expireSessionDataAllByKey(sessionKey);
162
+ }
163
+ ;
154
164
  async endSession() {
155
165
  if (!this.ctx.session)
156
166
  throw new Error("Session not found.");
@@ -1,3 +1,4 @@
1
+ import { type LambderCompressionOption } from "../stores/LambderDdbCompression.js";
1
2
  export type LambderSessionContext<SessionData = any> = {
2
3
  [x: string]: any;
3
4
  /**
@@ -53,23 +54,6 @@ export type LambderSessionDataRefreshConfig<SessionData = any> = {
53
54
  */
54
55
  refresh: (session: LambderSessionContext<SessionData>) => Promise<SessionData | null>;
55
56
  };
56
- /**
57
- * Brotli compression of session.data at rest. The option is `true` by
58
- * default, which equals `{ minBytes: 0 }`: every record compressed. A
59
- * compressed record carries the data's JSON as Brotli bytes (`dataBr`)
60
- * beside its byte length (`dataBytes`), the scheme LambderDdbCache and
61
- * LambderDdbIdempotency use. Below minBytes, or with compression off, the
62
- * record keeps a plain `data` attribute. Reads accept both shapes, so the
63
- * setting can be switched on or off on a live table: records written under
64
- * the other setting keep reading, and each is rewritten in the current
65
- * shape on its next write.
66
- */
67
- export type LambderSessionCompressionConfig = {
68
- /** JSON byte length from which data is stored compressed. Default: 0 (always). */
69
- minBytes?: number;
70
- /** Brotli quality (0-11), like LambderDdbCache. Default: 5. */
71
- quality?: number;
72
- };
73
57
  /**
74
58
  * Wraps errors thrown by the dataRefresh callback so they stay
75
59
  * distinguishable from "no session": fetchSessionIfExists() swallows missing
@@ -108,7 +92,7 @@ export default class LambderSessionManager {
108
92
  enableSlidingExpiration?: boolean;
109
93
  slidingWriteIntervalSeconds?: number;
110
94
  dataRefresh?: LambderSessionDataRefreshConfig;
111
- compression?: boolean | LambderSessionCompressionConfig;
95
+ compression?: LambderCompressionOption;
112
96
  });
113
97
  private sessionUserKeyHasher;
114
98
  /**
@@ -124,10 +108,10 @@ export default class LambderSessionManager {
124
108
  * Persists a session record. With compression on, `data` is stored as
125
109
  * Brotli bytes (`dataBr`) beside its JSON byte length (`dataBytes`)
126
110
  * once the JSON reaches minBytes; otherwise it stays a plain attribute.
127
- * See LambderSessionCompressionConfig.
128
111
  */
129
112
  private ddbPutItem;
130
113
  private ddbDeleteItem;
114
+ /** Sort keys of every session under a partition (the callers only need the keys). */
131
115
  private ddbQueryAllByPartitionKey;
132
116
  private ddbDeleteAllByPartitionKey;
133
117
  createSession(sessionKey: string, data?: any, ttlInSeconds?: number, options?: {
@@ -152,5 +136,15 @@ export default class LambderSessionManager {
152
136
  * session record.
153
137
  */
154
138
  deleteSessionAllByKey(sessionKey: string): Promise<boolean>;
139
+ /**
140
+ * Marks the data of every session of the given sessionKey stale, so each
141
+ * renews via dataRefresh on its next read: "this subject's roles or
142
+ * permissions changed, apply it now", without logging the subject out
143
+ * (deleteSessionAllByKey) and without waiting for the data TTL. Stamps
144
+ * dataExpiresAt only, conditionally on the record still existing, so it
145
+ * neither resurrects a session deleted in between nor overwrites a
146
+ * concurrent write. Requires dataRefresh to be configured.
147
+ */
148
+ expireSessionDataAllByKey(sessionKey: string): Promise<boolean>;
155
149
  regenerateSession(session: LambderSessionContext): Promise<LambderCreatedSession>;
156
150
  }
@@ -1,7 +1,16 @@
1
1
  import crypto from "crypto";
2
2
  import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
3
- import { DynamoDBDocumentClient, QueryCommand, DeleteCommand, PutCommand, GetCommand } from "@aws-sdk/lib-dynamodb";
4
- import { brotliCompressText, brotliRestoreText } from "../stores/LambderDdbCompression.js";
3
+ import { DynamoDBDocumentClient, QueryCommand, DeleteCommand, PutCommand, GetCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb";
4
+ import { brotliCompressText, brotliRestoreText, resolveCompressionOption, } from "../stores/LambderDdbCompression.js";
5
+ /**
6
+ * Session compression defaults: every record compressed (see
7
+ * LambderCompressionOption for the option's shape and toggle semantics).
8
+ * A compressed record carries the data's JSON as Brotli bytes (`dataBr`)
9
+ * beside its byte length (`dataBytes`), the scheme LambderDdbCache and
10
+ * LambderDdbIdempotency use; below minBytes, or with compression off, the
11
+ * record keeps a plain `data` attribute.
12
+ */
13
+ const SESSION_COMPRESSION_DEFAULTS = { minBytes: 0, quality: 5 };
5
14
  /**
6
15
  * Wraps errors thrown by the dataRefresh callback so they stay
7
16
  * distinguishable from "no session": fetchSessionIfExists() swallows missing
@@ -37,7 +46,7 @@ export default class LambderSessionManager {
37
46
  slidingWriteIntervalSeconds;
38
47
  dataRefresh;
39
48
  compression;
40
- constructor({ tableName, tableRegion, partitionKey, sortKey, sessionSalt, enableSlidingExpiration = true, slidingWriteIntervalSeconds, dataRefresh, compression = true, }) {
49
+ constructor({ tableName, tableRegion, partitionKey, sortKey, sessionSalt, enableSlidingExpiration = true, slidingWriteIntervalSeconds, dataRefresh, compression, }) {
41
50
  this.tableName = tableName;
42
51
  this.sessionSalt = sessionSalt;
43
52
  this.partitionKey = partitionKey;
@@ -45,19 +54,7 @@ export default class LambderSessionManager {
45
54
  this.enableSlidingExpiration = enableSlidingExpiration;
46
55
  this.slidingWriteIntervalSeconds = slidingWriteIntervalSeconds ?? null;
47
56
  this.dataRefresh = dataRefresh ?? null;
48
- const compressionConfig = compression === true ? {} : compression;
49
- this.compression = compressionConfig ? {
50
- minBytes: compressionConfig.minBytes ?? 0,
51
- quality: compressionConfig.quality ?? 5,
52
- } : null;
53
- if (this.compression) {
54
- if (!Number.isSafeInteger(this.compression.minBytes) || this.compression.minBytes < 0) {
55
- throw new Error("compression.minBytes must be a non-negative integer");
56
- }
57
- if (!Number.isInteger(this.compression.quality) || this.compression.quality < 0 || this.compression.quality > 11) {
58
- throw new Error("compression.quality must be an integer from 0 to 11");
59
- }
60
- }
57
+ this.compression = resolveCompressionOption(compression, SESSION_COMPRESSION_DEFAULTS);
61
58
  const ddbClient = new DynamoDBClient({ region: tableRegion });
62
59
  this.ddbDocumentClient = DynamoDBDocumentClient.from(ddbClient);
63
60
  }
@@ -93,7 +90,6 @@ export default class LambderSessionManager {
93
90
  * Persists a session record. With compression on, `data` is stored as
94
91
  * Brotli bytes (`dataBr`) beside its JSON byte length (`dataBytes`)
95
92
  * once the JSON reaches minBytes; otherwise it stays a plain attribute.
96
- * See LambderSessionCompressionConfig.
97
93
  */
98
94
  async ddbPutItem(session) {
99
95
  const { data, ...item } = session;
@@ -112,11 +108,13 @@ export default class LambderSessionManager {
112
108
  return await this.ddbDocumentClient.send(new DeleteCommand({ TableName: this.tableName, Key: key, }));
113
109
  }
114
110
  ;
111
+ /** Sort keys of every session under a partition (the callers only need the keys). */
115
112
  async ddbQueryAllByPartitionKey(partitionValue) {
116
113
  const params = {
117
114
  TableName: this.tableName,
118
115
  KeyConditionExpression: "#pk = :pv",
119
- ExpressionAttributeNames: { "#pk": this.partitionKey },
116
+ ProjectionExpression: "#sk",
117
+ ExpressionAttributeNames: { "#pk": this.partitionKey, "#sk": this.sortKey },
120
118
  ExpressionAttributeValues: { ":pv": partitionValue },
121
119
  };
122
120
  const queryResults = [];
@@ -345,6 +343,40 @@ export default class LambderSessionManager {
345
343
  return true;
346
344
  }
347
345
  ;
346
+ /**
347
+ * Marks the data of every session of the given sessionKey stale, so each
348
+ * renews via dataRefresh on its next read: "this subject's roles or
349
+ * permissions changed, apply it now", without logging the subject out
350
+ * (deleteSessionAllByKey) and without waiting for the data TTL. Stamps
351
+ * dataExpiresAt only, conditionally on the record still existing, so it
352
+ * neither resurrects a session deleted in between nor overwrites a
353
+ * concurrent write. Requires dataRefresh to be configured.
354
+ */
355
+ async expireSessionDataAllByKey(sessionKey) {
356
+ if (!this.dataRefresh)
357
+ throw new Error("dataRefresh is not configured. Pass session.dataRefresh at creation to enable.");
358
+ const partitionValue = this.sessionUserKeyHasher(sessionKey);
359
+ const now = Math.floor(Date.now() / 1000);
360
+ for (const item of await this.ddbQueryAllByPartitionKey(partitionValue)) {
361
+ try {
362
+ await this.ddbDocumentClient.send(new UpdateCommand({
363
+ TableName: this.tableName,
364
+ Key: { [this.partitionKey]: partitionValue, [this.sortKey]: item[this.sortKey] },
365
+ UpdateExpression: "SET #dataExpiresAt = :now",
366
+ ConditionExpression: "attribute_exists(#sk)",
367
+ ExpressionAttributeNames: { "#dataExpiresAt": "dataExpiresAt", "#sk": this.sortKey },
368
+ ExpressionAttributeValues: { ":now": now },
369
+ }));
370
+ }
371
+ catch (err) {
372
+ // Deleted between the query and the update: nothing left to expire.
373
+ if (err.name !== "ConditionalCheckFailedException")
374
+ throw err;
375
+ }
376
+ }
377
+ return true;
378
+ }
379
+ ;
348
380
  async regenerateSession(session) {
349
381
  if (!session)
350
382
  throw new Error("Invalid session");
@@ -1,4 +1,5 @@
1
1
  import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
+ import { type LambderCompressionOption } from "./LambderDdbCompression.js";
2
3
  export interface LambderDdbCacheOptions {
3
4
  tableName: string;
4
5
  region?: string;
@@ -7,7 +8,13 @@ export interface LambderDdbCacheOptions {
7
8
  namespace?: string;
8
9
  defaultTtlSeconds?: number;
9
10
  chunkBytes?: number;
10
- compressionQuality?: number;
11
+ /**
12
+ * Brotli compression of stored values. `true` (the default) is
13
+ * `{ minBytes: 0, quality: 5 }`: every value compressed; `false` stores
14
+ * values plain; an object overrides the defaults. The manifest records
15
+ * each value's encoding, so it can be switched on or off on a live table.
16
+ */
17
+ compression?: LambderCompressionOption;
11
18
  maxValueBytes?: number;
12
19
  memoryMaxBytes?: number;
13
20
  client?: DynamoDBClient;
@@ -22,8 +29,10 @@ export interface LambderDdbCacheGetOrSetOptions extends LambderDdbCacheSetOption
22
29
  /**
23
30
  * Persistent JSON cache backed by DynamoDB.
24
31
  *
25
- * Values are Brotli-compressed. Values within the safe DynamoDB item budget are
26
- * stored directly in the manifest for a single-request read; larger values are
32
+ * Values are Brotli-compressed by default (`compression` option; the manifest
33
+ * records each value's encoding, so the option can be switched on a live
34
+ * table). Values within the safe DynamoDB item budget are stored directly in
35
+ * the manifest for a single-request read; larger values are
27
36
  * split into versioned binary chunks. A manifest is written only after every
28
37
  * chunk succeeds, so readers see either the previous complete version or the
29
38
  * new complete version. DynamoDB TTL is cleanup only; every read also checks
@@ -41,7 +50,7 @@ export declare class LambderDdbCache {
41
50
  private readonly client;
42
51
  private readonly defaultTtlSeconds;
43
52
  private readonly chunkBytes;
44
- private readonly compressionQuality;
53
+ private readonly compression;
45
54
  private readonly maxValueBytes;
46
55
  private readonly memory;
47
56
  private readonly inFlight;
@@ -64,6 +73,8 @@ export declare class LambderDdbCache {
64
73
  private readChunks;
65
74
  private invalidateManifest;
66
75
  private batchWrite;
76
+ /** The JSON text of a stored payload. */
77
+ private decode;
67
78
  private remember;
68
79
  private normalizeKey;
69
80
  private partitionKey;