nextjs-dynamodb-cache 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/chunk-BX5KU2C7.js +59 -0
- package/dist/chunk-BX5KU2C7.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +25 -0
- package/dist/cli.js.map +1 -0
- package/dist/create-table.d.ts +24 -0
- package/dist/create-table.js +7 -0
- package/dist/create-table.js.map +1 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +161 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sufyan Osamah
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# nextjs-dynamodb-cache
|
|
2
|
+
|
|
3
|
+
Drop-in [Next.js CacheHandler](https://nextjs.org/docs/app/api-reference/next-config-js/cacheHandler) that stores ISR cache and tag-based revalidation in DynamoDB.
|
|
4
|
+
|
|
5
|
+
Built for Next.js deployments on **AWS Amplify, Lambda, App Runner, ECS**, or anywhere your filesystem is ephemeral — so your ISR cache and `revalidateTag()` calls actually survive deploys and work across instances.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm install nextjs-dynamodb-cache
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
The default Next.js cache handler writes to disk. On Amplify/Lambda/Fargate that means:
|
|
14
|
+
|
|
15
|
+
- ISR pages re-render from scratch after every deploy
|
|
16
|
+
- `revalidateTag()` only invalidates the instance that received the call
|
|
17
|
+
- Multiple instances serve inconsistent stale content
|
|
18
|
+
|
|
19
|
+
This package replaces the filesystem cache with DynamoDB. Cache entries are shared across all instances and survive deploys. Tag revalidation uses a GSI so it's a single query, not a full-table scan.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
### 1. Create the DynamoDB table
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx nextjs-dynamodb-cache-create-table --table my-app-cache --region eu-central-1
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This is idempotent. It creates the table, the `tag-index` GSI, and enables TTL on `expiresAt`. PAY_PER_REQUEST billing by default.
|
|
30
|
+
|
|
31
|
+
Or programmatically:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { createCacheTable } from "nextjs-dynamodb-cache/create-table";
|
|
35
|
+
|
|
36
|
+
await createCacheTable({ tableName: "my-app-cache", region: "eu-central-1" });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Register the handler
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
// next.config.js
|
|
43
|
+
module.exports = {
|
|
44
|
+
cacheHandler: require.resolve("./cache-handler.mjs"),
|
|
45
|
+
cacheMaxMemorySize: 0, // disable in-memory cache so DDB is authoritative
|
|
46
|
+
};
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
// cache-handler.mjs
|
|
51
|
+
import { DynamoDBCacheHandler } from "nextjs-dynamodb-cache";
|
|
52
|
+
|
|
53
|
+
export default DynamoDBCacheHandler.configure({
|
|
54
|
+
tableName: "my-app-cache",
|
|
55
|
+
region: "eu-central-1",
|
|
56
|
+
// optional: defaultTtlSeconds, keyPrefix, log, credentials
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
That's it. `revalidateTag()` and `revalidatePath()` now work cluster-wide.
|
|
61
|
+
|
|
62
|
+
## IAM
|
|
63
|
+
|
|
64
|
+
The Lambda/Amplify role needs:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"Effect": "Allow",
|
|
69
|
+
"Action": [
|
|
70
|
+
"dynamodb:GetItem",
|
|
71
|
+
"dynamodb:PutItem",
|
|
72
|
+
"dynamodb:DeleteItem",
|
|
73
|
+
"dynamodb:Query",
|
|
74
|
+
"dynamodb:BatchWriteItem"
|
|
75
|
+
],
|
|
76
|
+
"Resource": [
|
|
77
|
+
"arn:aws:dynamodb:eu-central-1:*:table/my-app-cache",
|
|
78
|
+
"arn:aws:dynamodb:eu-central-1:*:table/my-app-cache/index/*"
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Options
|
|
84
|
+
|
|
85
|
+
| Option | Default | Description |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| `tableName` | env `NEXT_CACHE_DYNAMODB_TABLE` or `nextjs-cache` | DynamoDB table name |
|
|
88
|
+
| `region` | env `NEXT_CACHE_AWS_REGION` / `AWS_REGION` / `eu-central-1` | AWS region |
|
|
89
|
+
| `credentials` | default chain | Explicit AWS credentials |
|
|
90
|
+
| `tagIndexName` | `tag-index` | GSI name used for `revalidateTag` |
|
|
91
|
+
| `defaultTtlSeconds` | `604800` (7 days) | TTL when Next.js omits `revalidate` |
|
|
92
|
+
| `keyPrefix` | `""` | Prefix for keys (multi-app table sharing) |
|
|
93
|
+
| `log` | `true` | Verbose logs on errors and revalidations |
|
|
94
|
+
|
|
95
|
+
## How it works
|
|
96
|
+
|
|
97
|
+
- **Schema:** single table, partition key `key`. Each entry has `value`, `expiresAt` (TTL), and optional `tags`.
|
|
98
|
+
- **Tags:** for each tag a sentinel row `__tag__<tag>__<key>` is written. `revalidateTag(t)` queries the `tag-index` GSI on `tag = t`, collects the original cache keys, and batch-deletes them.
|
|
99
|
+
- **TTL:** DynamoDB native TTL on `expiresAt` cleans up stale entries automatically — no cron required.
|
|
100
|
+
- **Resilience:** every operation is wrapped in try/catch; a DynamoDB outage degrades to "cache miss," never a 500.
|
|
101
|
+
|
|
102
|
+
## Status
|
|
103
|
+
|
|
104
|
+
Battle-tested in production on Next.js 16 + Amplify hosting on the HonestDog marketplace. Compatible with Next.js 14, 15, and 16 cache-handler APIs.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/create-table.ts
|
|
2
|
+
import {
|
|
3
|
+
CreateTableCommand,
|
|
4
|
+
DescribeTableCommand,
|
|
5
|
+
DynamoDBClient,
|
|
6
|
+
ResourceInUseException,
|
|
7
|
+
UpdateTimeToLiveCommand
|
|
8
|
+
} from "@aws-sdk/client-dynamodb";
|
|
9
|
+
async function createCacheTable(options) {
|
|
10
|
+
const region = options.region ?? process.env.AWS_REGION ?? "eu-central-1";
|
|
11
|
+
const tagIndexName = options.tagIndexName ?? "tag-index";
|
|
12
|
+
const billingMode = options.billingMode ?? "PAY_PER_REQUEST";
|
|
13
|
+
const client = new DynamoDBClient({
|
|
14
|
+
region,
|
|
15
|
+
...options.credentials ? { credentials: options.credentials } : {}
|
|
16
|
+
});
|
|
17
|
+
try {
|
|
18
|
+
await client.send(
|
|
19
|
+
new CreateTableCommand({
|
|
20
|
+
TableName: options.tableName,
|
|
21
|
+
BillingMode: billingMode,
|
|
22
|
+
AttributeDefinitions: [
|
|
23
|
+
{ AttributeName: "key", AttributeType: "S" },
|
|
24
|
+
{ AttributeName: "tag", AttributeType: "S" }
|
|
25
|
+
],
|
|
26
|
+
KeySchema: [{ AttributeName: "key", KeyType: "HASH" }],
|
|
27
|
+
GlobalSecondaryIndexes: [
|
|
28
|
+
{
|
|
29
|
+
IndexName: tagIndexName,
|
|
30
|
+
KeySchema: [{ AttributeName: "tag", KeyType: "HASH" }],
|
|
31
|
+
Projection: { ProjectionType: "ALL" }
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error instanceof ResourceInUseException) {
|
|
38
|
+
return { created: false };
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
for (let i = 0; i < 60; i++) {
|
|
43
|
+
const status = await client.send(new DescribeTableCommand({ TableName: options.tableName })).then((r) => r.Table?.TableStatus);
|
|
44
|
+
if (status === "ACTIVE") break;
|
|
45
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
46
|
+
}
|
|
47
|
+
await client.send(
|
|
48
|
+
new UpdateTimeToLiveCommand({
|
|
49
|
+
TableName: options.tableName,
|
|
50
|
+
TimeToLiveSpecification: { AttributeName: "expiresAt", Enabled: true }
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
return { created: true };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
createCacheTable
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=chunk-BX5KU2C7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/create-table.ts"],"sourcesContent":["import {\n CreateTableCommand,\n DescribeTableCommand,\n DynamoDBClient,\n ResourceInUseException,\n UpdateTimeToLiveCommand,\n type DynamoDBClientConfig,\n} from \"@aws-sdk/client-dynamodb\";\n\nexport interface CreateTableOptions {\n tableName: string;\n region?: string;\n credentials?: DynamoDBClientConfig[\"credentials\"];\n /** GSI name for tag lookups. Defaults to `tag-index`. */\n tagIndexName?: string;\n /** Billing mode. Defaults to PAY_PER_REQUEST (recommended). */\n billingMode?: \"PAY_PER_REQUEST\" | \"PROVISIONED\";\n}\n\n/**\n * Idempotently create the DynamoDB table required by `nextjs-dynamodb-cache`.\n *\n * Schema:\n * - Partition key: `key` (S)\n * - GSI `tag-index`: partition key `tag` (S)\n * - TTL attribute: `expiresAt` (unix seconds)\n */\nexport async function createCacheTable(options: CreateTableOptions): Promise<{ created: boolean }> {\n const region = options.region ?? process.env.AWS_REGION ?? \"eu-central-1\";\n const tagIndexName = options.tagIndexName ?? \"tag-index\";\n const billingMode = options.billingMode ?? \"PAY_PER_REQUEST\";\n\n const client = new DynamoDBClient({\n region,\n ...(options.credentials ? { credentials: options.credentials } : {}),\n });\n\n try {\n await client.send(\n new CreateTableCommand({\n TableName: options.tableName,\n BillingMode: billingMode,\n AttributeDefinitions: [\n { AttributeName: \"key\", AttributeType: \"S\" },\n { AttributeName: \"tag\", AttributeType: \"S\" },\n ],\n KeySchema: [{ AttributeName: \"key\", KeyType: \"HASH\" }],\n GlobalSecondaryIndexes: [\n {\n IndexName: tagIndexName,\n KeySchema: [{ AttributeName: \"tag\", KeyType: \"HASH\" }],\n Projection: { ProjectionType: \"ALL\" },\n },\n ],\n }),\n );\n } catch (error) {\n if (error instanceof ResourceInUseException) {\n return { created: false };\n }\n throw error;\n }\n\n // Wait until ACTIVE so we can enable TTL.\n for (let i = 0; i < 60; i++) {\n const status = await client\n .send(new DescribeTableCommand({ TableName: options.tableName }))\n .then((r) => r.Table?.TableStatus);\n if (status === \"ACTIVE\") break;\n await new Promise((r) => setTimeout(r, 2000));\n }\n\n await client.send(\n new UpdateTimeToLiveCommand({\n TableName: options.tableName,\n TimeToLiveSpecification: { AttributeName: \"expiresAt\", Enabled: true },\n }),\n );\n\n return { created: true };\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAoBP,eAAsB,iBAAiB,SAA4D;AACjG,QAAM,SAAS,QAAQ,UAAU,QAAQ,IAAI,cAAc;AAC3D,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,cAAc,QAAQ,eAAe;AAE3C,QAAM,SAAS,IAAI,eAAe;AAAA,IAChC;AAAA,IACA,GAAI,QAAQ,cAAc,EAAE,aAAa,QAAQ,YAAY,IAAI,CAAC;AAAA,EACpE,CAAC;AAED,MAAI;AACF,UAAM,OAAO;AAAA,MACX,IAAI,mBAAmB;AAAA,QACrB,WAAW,QAAQ;AAAA,QACnB,aAAa;AAAA,QACb,sBAAsB;AAAA,UACpB,EAAE,eAAe,OAAO,eAAe,IAAI;AAAA,UAC3C,EAAE,eAAe,OAAO,eAAe,IAAI;AAAA,QAC7C;AAAA,QACA,WAAW,CAAC,EAAE,eAAe,OAAO,SAAS,OAAO,CAAC;AAAA,QACrD,wBAAwB;AAAA,UACtB;AAAA,YACE,WAAW;AAAA,YACX,WAAW,CAAC,EAAE,eAAe,OAAO,SAAS,OAAO,CAAC;AAAA,YACrD,YAAY,EAAE,gBAAgB,MAAM;AAAA,UACtC;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF,SAAS,OAAO;AACd,QAAI,iBAAiB,wBAAwB;AAC3C,aAAO,EAAE,SAAS,MAAM;AAAA,IAC1B;AACA,UAAM;AAAA,EACR;AAGA,WAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,UAAM,SAAS,MAAM,OAClB,KAAK,IAAI,qBAAqB,EAAE,WAAW,QAAQ,UAAU,CAAC,CAAC,EAC/D,KAAK,CAAC,MAAM,EAAE,OAAO,WAAW;AACnC,QAAI,WAAW,SAAU;AACzB,UAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,GAAI,CAAC;AAAA,EAC9C;AAEA,QAAM,OAAO;AAAA,IACX,IAAI,wBAAwB;AAAA,MAC1B,WAAW,QAAQ;AAAA,MACnB,yBAAyB,EAAE,eAAe,aAAa,SAAS,KAAK;AAAA,IACvE,CAAC;AAAA,EACH;AAEA,SAAO,EAAE,SAAS,KAAK;AACzB;","names":[]}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createCacheTable
|
|
4
|
+
} from "./chunk-BX5KU2C7.js";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
function arg(flag) {
|
|
8
|
+
const i = process.argv.indexOf(flag);
|
|
9
|
+
return i >= 0 ? process.argv[i + 1] : void 0;
|
|
10
|
+
}
|
|
11
|
+
async function main() {
|
|
12
|
+
const tableName = arg("--table") ?? process.env.NEXT_CACHE_DYNAMODB_TABLE;
|
|
13
|
+
if (!tableName) {
|
|
14
|
+
console.error("usage: nextjs-dynamodb-cache-create-table --table <name> [--region <region>]");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const region = arg("--region");
|
|
18
|
+
const result = await createCacheTable({ tableName, ...region ? { region } : {} });
|
|
19
|
+
console.log(result.created ? `Created table ${tableName}.` : `Table ${tableName} already exists.`);
|
|
20
|
+
}
|
|
21
|
+
main().catch((err) => {
|
|
22
|
+
console.error(err);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
|
25
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { createCacheTable } from \"./create-table.js\";\n\nfunction arg(flag: string): string | undefined {\n const i = process.argv.indexOf(flag);\n return i >= 0 ? process.argv[i + 1] : undefined;\n}\n\nasync function main(): Promise<void> {\n const tableName = arg(\"--table\") ?? process.env.NEXT_CACHE_DYNAMODB_TABLE;\n if (!tableName) {\n console.error(\"usage: nextjs-dynamodb-cache-create-table --table <name> [--region <region>]\");\n process.exit(1);\n }\n const region = arg(\"--region\");\n const result = await createCacheTable({ tableName, ...(region ? { region } : {}) });\n console.log(result.created ? `Created table ${tableName}.` : `Table ${tableName} already exists.`);\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":";;;;;;AAGA,SAAS,IAAI,MAAkC;AAC7C,QAAM,IAAI,QAAQ,KAAK,QAAQ,IAAI;AACnC,SAAO,KAAK,IAAI,QAAQ,KAAK,IAAI,CAAC,IAAI;AACxC;AAEA,eAAe,OAAsB;AACnC,QAAM,YAAY,IAAI,SAAS,KAAK,QAAQ,IAAI;AAChD,MAAI,CAAC,WAAW;AACd,YAAQ,MAAM,8EAA8E;AAC5F,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,QAAM,SAAS,IAAI,UAAU;AAC7B,QAAM,SAAS,MAAM,iBAAiB,EAAE,WAAW,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC,EAAG,CAAC;AAClF,UAAQ,IAAI,OAAO,UAAU,iBAAiB,SAAS,MAAM,SAAS,SAAS,kBAAkB;AACnG;AAEA,KAAK,EAAE,MAAM,CAAC,QAAQ;AACpB,UAAQ,MAAM,GAAG;AACjB,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DynamoDBClientConfig } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
|
|
3
|
+
interface CreateTableOptions {
|
|
4
|
+
tableName: string;
|
|
5
|
+
region?: string;
|
|
6
|
+
credentials?: DynamoDBClientConfig["credentials"];
|
|
7
|
+
/** GSI name for tag lookups. Defaults to `tag-index`. */
|
|
8
|
+
tagIndexName?: string;
|
|
9
|
+
/** Billing mode. Defaults to PAY_PER_REQUEST (recommended). */
|
|
10
|
+
billingMode?: "PAY_PER_REQUEST" | "PROVISIONED";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Idempotently create the DynamoDB table required by `nextjs-dynamodb-cache`.
|
|
14
|
+
*
|
|
15
|
+
* Schema:
|
|
16
|
+
* - Partition key: `key` (S)
|
|
17
|
+
* - GSI `tag-index`: partition key `tag` (S)
|
|
18
|
+
* - TTL attribute: `expiresAt` (unix seconds)
|
|
19
|
+
*/
|
|
20
|
+
declare function createCacheTable(options: CreateTableOptions): Promise<{
|
|
21
|
+
created: boolean;
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
export { type CreateTableOptions, createCacheTable };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { DynamoDBClientConfig } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
|
|
3
|
+
interface DynamoDBCacheOptions {
|
|
4
|
+
/** DynamoDB table name. Defaults to env `NEXT_CACHE_DYNAMODB_TABLE` or `nextjs-cache`. */
|
|
5
|
+
tableName?: string;
|
|
6
|
+
/** AWS region. Defaults to env `NEXT_CACHE_AWS_REGION` / `AWS_REGION` / `eu-central-1`. */
|
|
7
|
+
region?: string;
|
|
8
|
+
/** Optional explicit credentials. Otherwise the default AWS credential chain is used. */
|
|
9
|
+
credentials?: DynamoDBClientConfig["credentials"];
|
|
10
|
+
/** Name of the GSI used for tag-based revalidation. Defaults to `tag-index`. */
|
|
11
|
+
tagIndexName?: string;
|
|
12
|
+
/** Default TTL in seconds when the Next.js context omits `revalidate`. Defaults to 7 days. */
|
|
13
|
+
defaultTtlSeconds?: number;
|
|
14
|
+
/** Optional prefix added to every DynamoDB key (useful when sharing a table across apps). */
|
|
15
|
+
keyPrefix?: string;
|
|
16
|
+
/** Set to false to disable verbose logs. Defaults to true. */
|
|
17
|
+
log?: boolean;
|
|
18
|
+
}
|
|
19
|
+
interface NextCacheContext {
|
|
20
|
+
revalidate?: number | false;
|
|
21
|
+
tags?: string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Next.js CacheHandler backed by DynamoDB.
|
|
25
|
+
*
|
|
26
|
+
* Register it in `next.config.js`:
|
|
27
|
+
*
|
|
28
|
+
* ```js
|
|
29
|
+
* // next.config.js
|
|
30
|
+
* module.exports = {
|
|
31
|
+
* cacheHandler: require.resolve("./cache-handler.mjs"),
|
|
32
|
+
* cacheMaxMemorySize: 0,
|
|
33
|
+
* };
|
|
34
|
+
*
|
|
35
|
+
* // cache-handler.mjs
|
|
36
|
+
* import { DynamoDBCacheHandler } from "nextjs-dynamodb-cache";
|
|
37
|
+
* export default DynamoDBCacheHandler;
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
declare class DynamoDBCacheHandler {
|
|
41
|
+
private readonly tableName;
|
|
42
|
+
private readonly region;
|
|
43
|
+
private readonly tagIndexName;
|
|
44
|
+
private readonly defaultTtlSeconds;
|
|
45
|
+
private readonly keyPrefix;
|
|
46
|
+
private readonly log;
|
|
47
|
+
private readonly credentials?;
|
|
48
|
+
private docClient;
|
|
49
|
+
constructor(_nextOptions?: unknown);
|
|
50
|
+
/**
|
|
51
|
+
* Configure the handler globally before Next.js instantiates it.
|
|
52
|
+
* Call this from your `cache-handler.mjs` entrypoint.
|
|
53
|
+
*/
|
|
54
|
+
static globalConfig: DynamoDBCacheOptions | null;
|
|
55
|
+
static configure(options: DynamoDBCacheOptions): typeof DynamoDBCacheHandler;
|
|
56
|
+
private getClient;
|
|
57
|
+
private k;
|
|
58
|
+
get(rawKey: string): Promise<unknown | null>;
|
|
59
|
+
set(rawKey: string, data: unknown, ctx?: NextCacheContext): Promise<void>;
|
|
60
|
+
revalidateTag(tag: string): Promise<void>;
|
|
61
|
+
/** Manual deletion of a single key. Not called by Next.js but useful for ops scripts. */
|
|
62
|
+
delete(rawKey: string): Promise<void>;
|
|
63
|
+
private warn;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { DynamoDBCacheHandler, type DynamoDBCacheOptions, DynamoDBCacheHandler as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
3
|
+
import {
|
|
4
|
+
BatchWriteCommand,
|
|
5
|
+
DeleteCommand,
|
|
6
|
+
DynamoDBDocumentClient,
|
|
7
|
+
GetCommand,
|
|
8
|
+
PutCommand,
|
|
9
|
+
QueryCommand
|
|
10
|
+
} from "@aws-sdk/lib-dynamodb";
|
|
11
|
+
var DynamoDBCacheHandler = class _DynamoDBCacheHandler {
|
|
12
|
+
tableName;
|
|
13
|
+
region;
|
|
14
|
+
tagIndexName;
|
|
15
|
+
defaultTtlSeconds;
|
|
16
|
+
keyPrefix;
|
|
17
|
+
log;
|
|
18
|
+
credentials;
|
|
19
|
+
docClient = null;
|
|
20
|
+
// Next.js instantiates the handler with its own options bag; we accept and ignore it.
|
|
21
|
+
// Library users configure via the static `configure()` helper or env vars.
|
|
22
|
+
constructor(_nextOptions) {
|
|
23
|
+
const cfg = _DynamoDBCacheHandler.globalConfig ?? {};
|
|
24
|
+
this.tableName = cfg.tableName ?? process.env.NEXT_CACHE_DYNAMODB_TABLE ?? "nextjs-cache";
|
|
25
|
+
this.region = cfg.region ?? process.env.NEXT_CACHE_AWS_REGION ?? process.env.AWS_REGION ?? "eu-central-1";
|
|
26
|
+
this.tagIndexName = cfg.tagIndexName ?? "tag-index";
|
|
27
|
+
this.defaultTtlSeconds = cfg.defaultTtlSeconds ?? 7 * 24 * 60 * 60;
|
|
28
|
+
this.keyPrefix = cfg.keyPrefix ?? "";
|
|
29
|
+
this.log = cfg.log ?? true;
|
|
30
|
+
this.credentials = cfg.credentials;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Configure the handler globally before Next.js instantiates it.
|
|
34
|
+
* Call this from your `cache-handler.mjs` entrypoint.
|
|
35
|
+
*/
|
|
36
|
+
static globalConfig = null;
|
|
37
|
+
static configure(options) {
|
|
38
|
+
_DynamoDBCacheHandler.globalConfig = options;
|
|
39
|
+
return _DynamoDBCacheHandler;
|
|
40
|
+
}
|
|
41
|
+
getClient() {
|
|
42
|
+
if (!this.docClient) {
|
|
43
|
+
const config = { region: this.region };
|
|
44
|
+
if (this.credentials) config.credentials = this.credentials;
|
|
45
|
+
this.docClient = DynamoDBDocumentClient.from(new DynamoDBClient(config), {
|
|
46
|
+
marshallOptions: { removeUndefinedValues: true }
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return this.docClient;
|
|
50
|
+
}
|
|
51
|
+
k(key) {
|
|
52
|
+
return this.keyPrefix ? `${this.keyPrefix}${key}` : key;
|
|
53
|
+
}
|
|
54
|
+
async get(rawKey) {
|
|
55
|
+
const key = this.k(rawKey);
|
|
56
|
+
try {
|
|
57
|
+
const result = await this.getClient().send(
|
|
58
|
+
new GetCommand({ TableName: this.tableName, Key: { key } })
|
|
59
|
+
);
|
|
60
|
+
const item = result.Item;
|
|
61
|
+
if (!item) return null;
|
|
62
|
+
if (item.expiresAt && item.expiresAt < Math.floor(Date.now() / 1e3)) return null;
|
|
63
|
+
return item.value;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
this.warn("get", error);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async set(rawKey, data, ctx) {
|
|
70
|
+
const key = this.k(rawKey);
|
|
71
|
+
try {
|
|
72
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
73
|
+
const ttl = typeof ctx?.revalidate === "number" && ctx.revalidate > 0 ? ctx.revalidate : this.defaultTtlSeconds;
|
|
74
|
+
const expiresAt = now + ttl;
|
|
75
|
+
const tags = ctx?.tags ?? [];
|
|
76
|
+
const item = {
|
|
77
|
+
key,
|
|
78
|
+
value: data,
|
|
79
|
+
createdAt: now,
|
|
80
|
+
expiresAt,
|
|
81
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
82
|
+
tag: tags[0]
|
|
83
|
+
};
|
|
84
|
+
await this.getClient().send(new PutCommand({ TableName: this.tableName, Item: item }));
|
|
85
|
+
if (tags.length > 0) {
|
|
86
|
+
const mappings = tags.map((t) => ({
|
|
87
|
+
PutRequest: {
|
|
88
|
+
Item: { key: this.k(`__tag__${t}__${rawKey}`), cacheKey: key, tag: t, expiresAt }
|
|
89
|
+
}
|
|
90
|
+
}));
|
|
91
|
+
for (let i = 0; i < mappings.length; i += 25) {
|
|
92
|
+
await this.getClient().send(
|
|
93
|
+
new BatchWriteCommand({
|
|
94
|
+
RequestItems: { [this.tableName]: mappings.slice(i, i + 25) }
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
this.warn("set", error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async revalidateTag(tag) {
|
|
104
|
+
try {
|
|
105
|
+
const result = await this.getClient().send(
|
|
106
|
+
new QueryCommand({
|
|
107
|
+
TableName: this.tableName,
|
|
108
|
+
IndexName: this.tagIndexName,
|
|
109
|
+
KeyConditionExpression: "tag = :tag",
|
|
110
|
+
ExpressionAttributeValues: { ":tag": tag }
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
const items = result.Items ?? [];
|
|
114
|
+
if (items.length === 0) return;
|
|
115
|
+
const keysToDelete = /* @__PURE__ */ new Set();
|
|
116
|
+
for (const item of items) {
|
|
117
|
+
const cacheKey = item.cacheKey ?? item.key;
|
|
118
|
+
if (cacheKey && !cacheKey.includes("__tag__")) keysToDelete.add(cacheKey);
|
|
119
|
+
keysToDelete.add(item.key);
|
|
120
|
+
}
|
|
121
|
+
const deleteRequests = Array.from(keysToDelete).map((k) => ({
|
|
122
|
+
DeleteRequest: { Key: { key: k } }
|
|
123
|
+
}));
|
|
124
|
+
for (let i = 0; i < deleteRequests.length; i += 25) {
|
|
125
|
+
await this.getClient().send(
|
|
126
|
+
new BatchWriteCommand({
|
|
127
|
+
RequestItems: { [this.tableName]: deleteRequests.slice(i, i + 25) }
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (this.log) {
|
|
132
|
+
console.log(
|
|
133
|
+
`[nextjs-dynamodb-cache] revalidated tag "${tag}" (${keysToDelete.size} keys)`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
this.warn("revalidateTag", error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** Manual deletion of a single key. Not called by Next.js but useful for ops scripts. */
|
|
141
|
+
async delete(rawKey) {
|
|
142
|
+
try {
|
|
143
|
+
await this.getClient().send(
|
|
144
|
+
new DeleteCommand({ TableName: this.tableName, Key: { key: this.k(rawKey) } })
|
|
145
|
+
);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
this.warn("delete", error);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
warn(op, error) {
|
|
151
|
+
if (!this.log) return;
|
|
152
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
153
|
+
console.warn(`[nextjs-dynamodb-cache] ${op} failed: ${msg}`);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
var index_default = DynamoDBCacheHandler;
|
|
157
|
+
export {
|
|
158
|
+
DynamoDBCacheHandler,
|
|
159
|
+
index_default as default
|
|
160
|
+
};
|
|
161
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { DynamoDBClient, type DynamoDBClientConfig } from \"@aws-sdk/client-dynamodb\";\nimport {\n BatchWriteCommand,\n DeleteCommand,\n DynamoDBDocumentClient,\n GetCommand,\n PutCommand,\n QueryCommand,\n} from \"@aws-sdk/lib-dynamodb\";\n\nexport interface DynamoDBCacheOptions {\n /** DynamoDB table name. Defaults to env `NEXT_CACHE_DYNAMODB_TABLE` or `nextjs-cache`. */\n tableName?: string;\n /** AWS region. Defaults to env `NEXT_CACHE_AWS_REGION` / `AWS_REGION` / `eu-central-1`. */\n region?: string;\n /** Optional explicit credentials. Otherwise the default AWS credential chain is used. */\n credentials?: DynamoDBClientConfig[\"credentials\"];\n /** Name of the GSI used for tag-based revalidation. Defaults to `tag-index`. */\n tagIndexName?: string;\n /** Default TTL in seconds when the Next.js context omits `revalidate`. Defaults to 7 days. */\n defaultTtlSeconds?: number;\n /** Optional prefix added to every DynamoDB key (useful when sharing a table across apps). */\n keyPrefix?: string;\n /** Set to false to disable verbose logs. Defaults to true. */\n log?: boolean;\n}\n\ninterface NextCacheContext {\n revalidate?: number | false;\n tags?: string[];\n}\n\ninterface CachedItem {\n key: string;\n value: unknown;\n createdAt: number;\n expiresAt: number;\n tags?: string[];\n tag?: string;\n cacheKey?: string;\n}\n\n/**\n * Next.js CacheHandler backed by DynamoDB.\n *\n * Register it in `next.config.js`:\n *\n * ```js\n * // next.config.js\n * module.exports = {\n * cacheHandler: require.resolve(\"./cache-handler.mjs\"),\n * cacheMaxMemorySize: 0,\n * };\n *\n * // cache-handler.mjs\n * import { DynamoDBCacheHandler } from \"nextjs-dynamodb-cache\";\n * export default DynamoDBCacheHandler;\n * ```\n */\nexport class DynamoDBCacheHandler {\n private readonly tableName: string;\n private readonly region: string;\n private readonly tagIndexName: string;\n private readonly defaultTtlSeconds: number;\n private readonly keyPrefix: string;\n private readonly log: boolean;\n private readonly credentials?: DynamoDBClientConfig[\"credentials\"];\n private docClient: DynamoDBDocumentClient | null = null;\n\n // Next.js instantiates the handler with its own options bag; we accept and ignore it.\n // Library users configure via the static `configure()` helper or env vars.\n constructor(_nextOptions?: unknown) {\n const cfg = DynamoDBCacheHandler.globalConfig ?? {};\n this.tableName = cfg.tableName ?? process.env.NEXT_CACHE_DYNAMODB_TABLE ?? \"nextjs-cache\";\n this.region =\n cfg.region ??\n process.env.NEXT_CACHE_AWS_REGION ??\n process.env.AWS_REGION ??\n \"eu-central-1\";\n this.tagIndexName = cfg.tagIndexName ?? \"tag-index\";\n this.defaultTtlSeconds = cfg.defaultTtlSeconds ?? 7 * 24 * 60 * 60;\n this.keyPrefix = cfg.keyPrefix ?? \"\";\n this.log = cfg.log ?? true;\n this.credentials = cfg.credentials;\n }\n\n /**\n * Configure the handler globally before Next.js instantiates it.\n * Call this from your `cache-handler.mjs` entrypoint.\n */\n static globalConfig: DynamoDBCacheOptions | null = null;\n static configure(options: DynamoDBCacheOptions): typeof DynamoDBCacheHandler {\n DynamoDBCacheHandler.globalConfig = options;\n return DynamoDBCacheHandler;\n }\n\n private getClient(): DynamoDBDocumentClient {\n if (!this.docClient) {\n const config: DynamoDBClientConfig = { region: this.region };\n if (this.credentials) config.credentials = this.credentials;\n this.docClient = DynamoDBDocumentClient.from(new DynamoDBClient(config), {\n marshallOptions: { removeUndefinedValues: true },\n });\n }\n return this.docClient;\n }\n\n private k(key: string): string {\n return this.keyPrefix ? `${this.keyPrefix}${key}` : key;\n }\n\n async get(rawKey: string): Promise<unknown | null> {\n const key = this.k(rawKey);\n try {\n const result = await this.getClient().send(\n new GetCommand({ TableName: this.tableName, Key: { key } }),\n );\n const item = result.Item as CachedItem | undefined;\n if (!item) return null;\n if (item.expiresAt && item.expiresAt < Math.floor(Date.now() / 1000)) return null;\n return item.value;\n } catch (error) {\n this.warn(\"get\", error);\n return null;\n }\n }\n\n async set(rawKey: string, data: unknown, ctx?: NextCacheContext): Promise<void> {\n const key = this.k(rawKey);\n try {\n const now = Math.floor(Date.now() / 1000);\n const ttl =\n typeof ctx?.revalidate === \"number\" && ctx.revalidate > 0\n ? ctx.revalidate\n : this.defaultTtlSeconds;\n const expiresAt = now + ttl;\n const tags = ctx?.tags ?? [];\n\n const item: CachedItem = {\n key,\n value: data,\n createdAt: now,\n expiresAt,\n tags: tags.length > 0 ? tags : undefined,\n tag: tags[0],\n };\n\n await this.getClient().send(new PutCommand({ TableName: this.tableName, Item: item }));\n\n if (tags.length > 0) {\n const mappings = tags.map((t) => ({\n PutRequest: {\n Item: { key: this.k(`__tag__${t}__${rawKey}`), cacheKey: key, tag: t, expiresAt },\n },\n }));\n for (let i = 0; i < mappings.length; i += 25) {\n await this.getClient().send(\n new BatchWriteCommand({\n RequestItems: { [this.tableName]: mappings.slice(i, i + 25) },\n }),\n );\n }\n }\n } catch (error) {\n this.warn(\"set\", error);\n }\n }\n\n async revalidateTag(tag: string): Promise<void> {\n try {\n const result = await this.getClient().send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: this.tagIndexName,\n KeyConditionExpression: \"tag = :tag\",\n ExpressionAttributeValues: { \":tag\": tag },\n }),\n );\n\n const items = (result.Items as CachedItem[] | undefined) ?? [];\n if (items.length === 0) return;\n\n const keysToDelete = new Set<string>();\n for (const item of items) {\n const cacheKey = item.cacheKey ?? item.key;\n if (cacheKey && !cacheKey.includes(\"__tag__\")) keysToDelete.add(cacheKey);\n keysToDelete.add(item.key);\n }\n\n const deleteRequests = Array.from(keysToDelete).map((k) => ({\n DeleteRequest: { Key: { key: k } },\n }));\n for (let i = 0; i < deleteRequests.length; i += 25) {\n await this.getClient().send(\n new BatchWriteCommand({\n RequestItems: { [this.tableName]: deleteRequests.slice(i, i + 25) },\n }),\n );\n }\n\n if (this.log) {\n console.log(\n `[nextjs-dynamodb-cache] revalidated tag \"${tag}\" (${keysToDelete.size} keys)`,\n );\n }\n } catch (error) {\n this.warn(\"revalidateTag\", error);\n }\n }\n\n /** Manual deletion of a single key. Not called by Next.js but useful for ops scripts. */\n async delete(rawKey: string): Promise<void> {\n try {\n await this.getClient().send(\n new DeleteCommand({ TableName: this.tableName, Key: { key: this.k(rawKey) } }),\n );\n } catch (error) {\n this.warn(\"delete\", error);\n }\n }\n\n private warn(op: string, error: unknown): void {\n if (!this.log) return;\n const msg = error instanceof Error ? error.message : String(error);\n console.warn(`[nextjs-dynamodb-cache] ${op} failed: ${msg}`);\n }\n}\n\nexport default DynamoDBCacheHandler;\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nextjs-dynamodb-cache",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Drop-in Next.js 14/15/16 CacheHandler that stores ISR cache and tag revalidation in DynamoDB. Survives deploys on AWS Amplify, Lambda, App Runner, and other ephemeral runtimes.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nextjs",
|
|
7
|
+
"next.js",
|
|
8
|
+
"isr",
|
|
9
|
+
"cache",
|
|
10
|
+
"cache-handler",
|
|
11
|
+
"dynamodb",
|
|
12
|
+
"aws",
|
|
13
|
+
"amplify",
|
|
14
|
+
"lambda",
|
|
15
|
+
"revalidate",
|
|
16
|
+
"revalidatetag"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Sufyan Osamah",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/sufyman/nextjs-dynamodb-cache"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./create-table": {
|
|
33
|
+
"types": "./dist/create-table.d.ts",
|
|
34
|
+
"import": "./dist/create-table.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"bin": {
|
|
38
|
+
"nextjs-dynamodb-cache-create-table": "./dist/cli.js"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:watch": "vitest",
|
|
49
|
+
"prepublishOnly": "npm run build"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@aws-sdk/client-dynamodb": "^3.600.0",
|
|
53
|
+
"@aws-sdk/lib-dynamodb": "^3.600.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^20.0.0",
|
|
57
|
+
"aws-sdk-client-mock": "^4.0.0",
|
|
58
|
+
"tsup": "^8.0.0",
|
|
59
|
+
"typescript": "^5.4.0",
|
|
60
|
+
"vitest": "^1.6.0"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=18"
|
|
64
|
+
}
|
|
65
|
+
}
|