@aligent/appbuilder-util-lib 0.2.0 → 0.2.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.
- package/docs/functions/createDatabaseStorageClient.md +135 -0
- package/docs/functions/createFileStorageClient.md +102 -0
- package/docs/modules/aio-persistent-state.md +45 -18
- package/package.json +8 -2
- package/src/aio-persistent-state/constants.d.ts +6 -0
- package/src/aio-persistent-state/constants.js +9 -0
- package/src/aio-persistent-state/index.d.ts +2 -0
- package/src/aio-persistent-state/index.js +18 -0
- package/src/aio-persistent-state/state-database.d.ts +106 -0
- package/src/aio-persistent-state/state-database.js +212 -0
- package/src/aio-persistent-state/state-files.d.ts +84 -0
- package/src/aio-persistent-state/state-files.js +184 -0
- package/src/aio-persistent-state/utils.d.ts +25 -0
- package/src/aio-persistent-state/utils.js +29 -0
- package/src/index.d.ts +4 -0
- package/src/index.js +1 -2
- package/tsconfig.lib.tsbuildinfo +1 -0
- package/docs/functions/get.md +0 -42
- package/docs/functions/put.md +0 -49
- package/src/aio-persistent-state/aio-persistent-state.js +0 -151
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
[**@aligent/appbuilder-util-lib**](../modules.md)
|
|
2
|
+
|
|
3
|
+
***
|
|
4
|
+
|
|
5
|
+
[@aligent/appbuilder-util-lib](../modules.md) / [aio-persistent-state](../modules/aio-persistent-state.md) / createDatabaseStorageClient
|
|
6
|
+
|
|
7
|
+
# Function: createDatabaseStorageClient()
|
|
8
|
+
|
|
9
|
+
> **createDatabaseStorageClient**\<`T`\>(`config`): `DatabaseStorageClient<T>`
|
|
10
|
+
|
|
11
|
+
Creates a hybrid State + Database storage client for typed JSON objects.
|
|
12
|
+
|
|
13
|
+
Each client manages ONE document identified by `dbDocumentId`. Calling `put()` will overwrite any existing data. For storing multiple items, create separate clients with different `key` and `dbDocumentId` values.
|
|
14
|
+
|
|
15
|
+
All clients share the same underlying Adobe I/O State and Database instances — they are lightweight wrappers that provide typed access to specific documents.
|
|
16
|
+
|
|
17
|
+
## Type Parameters
|
|
18
|
+
|
|
19
|
+
### T
|
|
20
|
+
|
|
21
|
+
`T extends Document`
|
|
22
|
+
|
|
23
|
+
The type of data to store. Must be JSON-serializable.
|
|
24
|
+
|
|
25
|
+
## Parameters
|
|
26
|
+
|
|
27
|
+
### config
|
|
28
|
+
|
|
29
|
+
`DatabaseStorageClientConfig<T>`
|
|
30
|
+
|
|
31
|
+
Configuration options for the client.
|
|
32
|
+
|
|
33
|
+
| Property | Type | Required | Description |
|
|
34
|
+
|----------|------|----------|-------------|
|
|
35
|
+
| `key` | `string` | Yes | Key for State cache |
|
|
36
|
+
| `dbCollection` | `string` | Yes | Database collection name |
|
|
37
|
+
| `dbDocumentId` | `string` | Yes | Document ID in the collection |
|
|
38
|
+
| `ttl` | `number` | No | State cache TTL in seconds (default: 31,536,000 = 1 year) |
|
|
39
|
+
|
|
40
|
+
## Returns
|
|
41
|
+
|
|
42
|
+
`DatabaseStorageClient<T>`
|
|
43
|
+
|
|
44
|
+
A client object with `put`, `get`, `exists`, and `delete` methods.
|
|
45
|
+
|
|
46
|
+
### Methods
|
|
47
|
+
|
|
48
|
+
#### `put(data, logger?): Promise<void>`
|
|
49
|
+
|
|
50
|
+
Stores a typed object. Data is JSON-serialized for State and stored as a native document in Database.
|
|
51
|
+
|
|
52
|
+
#### `get(logger?): Promise<T | undefined>`
|
|
53
|
+
|
|
54
|
+
Retrieves the stored object. Returns `undefined` if not found.
|
|
55
|
+
|
|
56
|
+
#### `exists(logger?): Promise<boolean>`
|
|
57
|
+
|
|
58
|
+
Checks if data exists without returning it.
|
|
59
|
+
|
|
60
|
+
#### `delete(logger?): Promise<void>`
|
|
61
|
+
|
|
62
|
+
Deletes data from both State and Database.
|
|
63
|
+
|
|
64
|
+
## Example
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { createDatabaseStorageClient } from '@aligent/appbuilder-util-lib';
|
|
68
|
+
|
|
69
|
+
// Define your data type
|
|
70
|
+
interface UserPreferences {
|
|
71
|
+
theme: 'light' | 'dark';
|
|
72
|
+
notifications: boolean;
|
|
73
|
+
language: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const prefsClient = createDatabaseStorageClient<UserPreferences>({
|
|
77
|
+
key: 'user-prefs',
|
|
78
|
+
dbCollection: 'preferences',
|
|
79
|
+
dbDocumentId: 'user-123',
|
|
80
|
+
ttl: 86400, // Optional: 1 day TTL
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Store typed data
|
|
84
|
+
await prefsClient.put({
|
|
85
|
+
theme: 'dark',
|
|
86
|
+
notifications: true,
|
|
87
|
+
language: 'en',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Retrieve typed data (returns UserPreferences | undefined)
|
|
91
|
+
const prefs = await prefsClient.get();
|
|
92
|
+
if (prefs) {
|
|
93
|
+
console.log(prefs.theme); // TypeScript knows this is 'light' | 'dark'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check existence
|
|
97
|
+
if (await prefsClient.exists()) {
|
|
98
|
+
console.log('Preferences exist');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Delete data
|
|
102
|
+
await prefsClient.delete();
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Behaviour
|
|
106
|
+
|
|
107
|
+
### Storage format
|
|
108
|
+
|
|
109
|
+
Data is stored differently in each tier:
|
|
110
|
+
|
|
111
|
+
| Operation | State | Database |
|
|
112
|
+
|-----------|-------|----------|
|
|
113
|
+
| `put(data)` | `JSON.stringify(data)` → stored as string | `{ _id, ...data }` → stored as native document |
|
|
114
|
+
| `get()` | `JSON.parse(value)` → returned as object | Remove `_id` → returned as object |
|
|
115
|
+
|
|
116
|
+
### 1 MB State size limit
|
|
117
|
+
|
|
118
|
+
Values exceeding 1 MB (when JSON-serialized) are stored in Database only. On read, such values are returned without being cached in State.
|
|
119
|
+
|
|
120
|
+
### Read flow
|
|
121
|
+
|
|
122
|
+
1. Try State first (fast, but subject to TTL expiry)
|
|
123
|
+
2. On cache miss, fall back to Database (persistent, no TTL)
|
|
124
|
+
3. Self-heal: restore data to State for future reads (if within 1 MB limit)
|
|
125
|
+
|
|
126
|
+
### Write flow
|
|
127
|
+
|
|
128
|
+
1. Write to Database first (durability)
|
|
129
|
+
2. Cache in State (if within 1 MB limit)
|
|
130
|
+
|
|
131
|
+
## Notes
|
|
132
|
+
|
|
133
|
+
- Data must be JSON-serializable (no `Date` objects, `Map`, `Set`, `BigInt`, or circular references without custom handling)
|
|
134
|
+
- Adobe I/O Database uses AWS DocumentDB (MongoDB-compatible) under the hood
|
|
135
|
+
- The `_id` field is automatically managed; do not include it in your data type
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
[**@aligent/appbuilder-util-lib**](../modules.md)
|
|
2
|
+
|
|
3
|
+
***
|
|
4
|
+
|
|
5
|
+
[@aligent/appbuilder-util-lib](../modules.md) / [aio-persistent-state](../modules/aio-persistent-state.md) / createFileStorageClient
|
|
6
|
+
|
|
7
|
+
# Function: createFileStorageClient()
|
|
8
|
+
|
|
9
|
+
> **createFileStorageClient**(`config`): `FileStorageClient`
|
|
10
|
+
|
|
11
|
+
Creates a hybrid State + Files storage client for string data.
|
|
12
|
+
|
|
13
|
+
Each client manages ONE key. Calling `put()` will overwrite any existing data. For storing multiple items, create separate clients with different `key` values.
|
|
14
|
+
|
|
15
|
+
All clients share the same underlying Adobe I/O State and Files instances — they are lightweight wrappers that provide typed access to specific keys.
|
|
16
|
+
|
|
17
|
+
## Parameters
|
|
18
|
+
|
|
19
|
+
### config
|
|
20
|
+
|
|
21
|
+
`FileStorageClientConfig`
|
|
22
|
+
|
|
23
|
+
Configuration options for the client.
|
|
24
|
+
|
|
25
|
+
| Property | Type | Required | Description |
|
|
26
|
+
|----------|------|----------|-------------|
|
|
27
|
+
| `key` | `string` | Yes | Unique identifier for the data in State and Files storage |
|
|
28
|
+
| `ttl` | `number` | No | State cache TTL in seconds (default: 31,536,000 = 1 year) |
|
|
29
|
+
|
|
30
|
+
## Returns
|
|
31
|
+
|
|
32
|
+
`FileStorageClient`
|
|
33
|
+
|
|
34
|
+
A client object with `put`, `get`, `exists`, and `delete` methods.
|
|
35
|
+
|
|
36
|
+
### Methods
|
|
37
|
+
|
|
38
|
+
#### `put(value, logger?): Promise<void>`
|
|
39
|
+
|
|
40
|
+
Stores a string value. Values exceeding 1 MB are stored in Files only (State is skipped).
|
|
41
|
+
|
|
42
|
+
#### `get(logger?): Promise<string | undefined>`
|
|
43
|
+
|
|
44
|
+
Retrieves the stored value. Returns `undefined` if not found.
|
|
45
|
+
|
|
46
|
+
#### `exists(logger?): Promise<boolean>`
|
|
47
|
+
|
|
48
|
+
Checks if data exists without returning it.
|
|
49
|
+
|
|
50
|
+
#### `delete(logger?): Promise<void>`
|
|
51
|
+
|
|
52
|
+
Deletes data from both State and Files.
|
|
53
|
+
|
|
54
|
+
## Example
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { createFileStorageClient } from '@aligent/appbuilder-util-lib';
|
|
58
|
+
|
|
59
|
+
const configClient = createFileStorageClient({
|
|
60
|
+
key: 'app-config',
|
|
61
|
+
ttl: 86400, // Optional: 1 day TTL
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Store a JSON string
|
|
65
|
+
await configClient.put(JSON.stringify({ theme: 'dark', locale: 'en' }));
|
|
66
|
+
|
|
67
|
+
// Retrieve the value (returns string | undefined)
|
|
68
|
+
const config = await configClient.get();
|
|
69
|
+
if (config) {
|
|
70
|
+
console.log(JSON.parse(config)); // { theme: 'dark', locale: 'en' }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check existence
|
|
74
|
+
if (await configClient.exists()) {
|
|
75
|
+
console.log('Config exists');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Delete data
|
|
79
|
+
await configClient.delete();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Behaviour
|
|
83
|
+
|
|
84
|
+
### 1 MB State size limit
|
|
85
|
+
|
|
86
|
+
Adobe I/O State imposes a maximum value size of 1 MB per entry. This client handles it automatically:
|
|
87
|
+
|
|
88
|
+
- **On write (`put`)**: Values exceeding 1 MB are stored in Files only; State is skipped with a warning log.
|
|
89
|
+
- **On read (`get`)**: If the value from Files exceeds 1 MB, it is returned directly without being cached in State.
|
|
90
|
+
|
|
91
|
+
Values within the 1 MB limit are stored in both layers so that subsequent reads are served from the faster State cache.
|
|
92
|
+
|
|
93
|
+
### Read flow
|
|
94
|
+
|
|
95
|
+
1. Try State first (fast, but subject to TTL expiry)
|
|
96
|
+
2. On cache miss, fall back to Files (persistent, no TTL)
|
|
97
|
+
3. Self-heal: restore data to State for future reads (if within 1 MB limit)
|
|
98
|
+
|
|
99
|
+
### Write flow
|
|
100
|
+
|
|
101
|
+
1. Write to Files first (durability)
|
|
102
|
+
2. Cache in State (if within 1 MB limit)
|
|
@@ -6,34 +6,61 @@
|
|
|
6
6
|
|
|
7
7
|
# Module: aio-persistent-state
|
|
8
8
|
|
|
9
|
-
A utility library for persistent, high-performance key-value storage
|
|
10
|
-
|
|
9
|
+
A utility library for persistent, high-performance key-value storage in Adobe App Builder runtime. Provides two hybrid storage clients:
|
|
10
|
+
|
|
11
|
+
- **State + Files** — for string data up to 1 MB with automatic overflow handling
|
|
12
|
+
- **State + Database** — for typed JSON objects with MongoDB-like storage
|
|
11
13
|
|
|
12
14
|
## Overview
|
|
13
15
|
|
|
14
|
-
This library provides
|
|
15
|
-
- **Adobe I/O Lib State** — fast access and short-term caching
|
|
16
|
-
- **Adobe I/O Lib Files** — long-term durability and backup
|
|
16
|
+
This library provides high-level abstractions for key-value storage using Adobe App Builder's runtime services. Both clients use a two-tier architecture:
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
```
|
|
19
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
20
|
+
│ State (fast, TTL-based) ←──cache──→ Files/Database (permanent) │
|
|
21
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
22
|
+
```
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
### Why two tiers?
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
Adobe I/O State enforces a **TTL (Time-To-Live)** on all entries, so cached data will eventually expire. Files and Database storage have no such expiration, making them suitable for persistent data. By combining both, we get speed from State and persistence from the durable tier.
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
- **On write (`put`)**: values exceeding 1 MB are stored in Files only; the State cache is skipped.
|
|
26
|
-
- **On read (`get`)**: if the value retrieved from Files exceeds 1 MB, it is returned directly without being cached in State.
|
|
28
|
+
### Storage options
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
| Feature | State + Files | State + Database |
|
|
31
|
+
|----------------|---------------------------|---------------------------------|
|
|
32
|
+
| Data type | `string` | Generic `T` (JSON-serializable) |
|
|
33
|
+
| Max value size | Unlimited (1 MB cached) | Limited by Database |
|
|
34
|
+
| Query support | Key-based only | MongoDB-like queries |
|
|
35
|
+
| Best for | Large strings, JSON blobs | Structured typed data |
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
## Interfaces
|
|
31
38
|
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
39
|
+
- [FileStorageClientConfig](../interfaces/FileStorageClientConfig.md)
|
|
40
|
+
- [FileStorageClient](../interfaces/FileStorageClient.md)
|
|
41
|
+
- [DatabaseStorageClientConfig](../interfaces/DatabaseStorageClientConfig.md)
|
|
42
|
+
- [DatabaseStorageClient](../interfaces/DatabaseStorageClient.md)
|
|
35
43
|
|
|
36
44
|
## Functions
|
|
37
45
|
|
|
38
|
-
- [
|
|
39
|
-
- [
|
|
46
|
+
- [createFileStorageClient](../functions/createFileStorageClient.md)
|
|
47
|
+
- [createDatabaseStorageClient](../functions/createDatabaseStorageClient.md)
|
|
48
|
+
|
|
49
|
+
## Behaviour
|
|
50
|
+
|
|
51
|
+
### Read flow
|
|
52
|
+
|
|
53
|
+
1. Try State first (fast path)
|
|
54
|
+
2. On cache miss, fall back to Files/Database
|
|
55
|
+
3. Self-heal: restore data to State cache for future reads
|
|
56
|
+
|
|
57
|
+
### Write flow
|
|
58
|
+
|
|
59
|
+
- **Files client**: Write to Files first (durability), then cache in State
|
|
60
|
+
- **Database client**: Write to Database first, then cache in State
|
|
61
|
+
|
|
62
|
+
### Error handling
|
|
63
|
+
|
|
64
|
+
- `get()` returns `undefined` when data is not found
|
|
65
|
+
- All methods throw on actual storage errors (connection failures, etc.)
|
|
66
|
+
- Errors are logged before being re-thrown
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aligent/appbuilder-util-lib",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A utility library to simplify and standardise common Adobe App Builder tasks.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -11,10 +11,16 @@
|
|
|
11
11
|
"directory": "packages/appbuilder-util-lib"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@adobe/aio-
|
|
14
|
+
"@adobe/aio-lib-core-logging": "^3.0.2",
|
|
15
|
+
"@adobe/aio-lib-db": "^0.1.0-beta.6",
|
|
16
|
+
"@adobe/aio-lib-files": "^4.1.2",
|
|
17
|
+
"@adobe/aio-lib-state": "^5.3.1",
|
|
15
18
|
"base64url": "^3.0.1"
|
|
16
19
|
},
|
|
17
20
|
"author": "Aligent",
|
|
18
21
|
"license": "MIT",
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"mongodb": "^7.1.0"
|
|
24
|
+
},
|
|
19
25
|
"types": "./src/index.d.ts"
|
|
20
26
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Default TTL for I/O State: 1 year in seconds (maximum allowed by Adobe I/O State) */
|
|
2
|
+
export declare const DEFAULT_ONE_YEAR_TTL_SECONDS = 31536000;
|
|
3
|
+
/** Maximum length (in characters) of a base64url-encoded key accepted by State. */
|
|
4
|
+
export declare const MAX_KEY_SIZE = 1024;
|
|
5
|
+
/** Maximum value size (in bytes) accepted by Adobe I/O State (1 MB). */
|
|
6
|
+
export declare const MAX_STATE_VALUE_SIZE: number;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_STATE_VALUE_SIZE = exports.MAX_KEY_SIZE = exports.DEFAULT_ONE_YEAR_TTL_SECONDS = void 0;
|
|
4
|
+
/** Default TTL for I/O State: 1 year in seconds (maximum allowed by Adobe I/O State) */
|
|
5
|
+
exports.DEFAULT_ONE_YEAR_TTL_SECONDS = 31536000;
|
|
6
|
+
/** Maximum length (in characters) of a base64url-encoded key accepted by State. */
|
|
7
|
+
exports.MAX_KEY_SIZE = 1024;
|
|
8
|
+
/** Maximum value size (in bytes) accepted by Adobe I/O State (1 MB). */
|
|
9
|
+
exports.MAX_STATE_VALUE_SIZE = 1024 * 1024; // 1MB
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./state-database"), exports);
|
|
18
|
+
__exportStar(require("./state-files"), exports);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module aio-persistent-state/database
|
|
3
|
+
*
|
|
4
|
+
* A two-tier key-value storage library for Adobe App Builder runtime that
|
|
5
|
+
* combines Adobe I/O State and Adobe I/O Database to achieve both fast access
|
|
6
|
+
* and long-term durability.
|
|
7
|
+
*
|
|
8
|
+
* **Why both State and Database?**
|
|
9
|
+
* Adobe I/O State enforces a TTL (Time-To-Live) on all entries, meaning
|
|
10
|
+
* cached data will expire and be evicted automatically. Database storage has
|
|
11
|
+
* no such expiration, making it suitable for persistent, long-lived data.
|
|
12
|
+
* By writing to Database for durability and using State as a fast-access cache,
|
|
13
|
+
* we get the best of both: speed from State and persistence from Database.
|
|
14
|
+
*
|
|
15
|
+
* **Architecture:**
|
|
16
|
+
* ```
|
|
17
|
+
* ┌─────────────────────────────────────────────────────────────────────────┐
|
|
18
|
+
* │ State (fast, TTL-based) ←──cache──→ Database (permanent backup) │
|
|
19
|
+
* └─────────────────────────────────────────────────────────────────────────┘
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* **Write flow:** Database first and then State if the value is within the State size limit (1MB).
|
|
23
|
+
* **Read flow:** State first → Database fallback → self-heal (restore to State)
|
|
24
|
+
*
|
|
25
|
+
* @see https://developer.adobe.com/app-builder/docs/guides/app_builder_guides/storage/database
|
|
26
|
+
*/
|
|
27
|
+
import AioLogger from '@adobe/aio-lib-core-logging';
|
|
28
|
+
import { type Document, type InferIdType } from '@adobe/aio-lib-db';
|
|
29
|
+
/**
|
|
30
|
+
* Configuration options for creating a database storage client.
|
|
31
|
+
*/
|
|
32
|
+
export interface DatabaseStorageClientConfig<T> {
|
|
33
|
+
/** Key used to store data in Adobe I/O State */
|
|
34
|
+
key: string;
|
|
35
|
+
/** Collection name in Adobe I/O Database */
|
|
36
|
+
dbCollection: string;
|
|
37
|
+
/** Document ID in the database collection */
|
|
38
|
+
dbDocumentId: InferIdType<T & {
|
|
39
|
+
_id: string;
|
|
40
|
+
}>;
|
|
41
|
+
/** TTL (time-to-live) for State storage in seconds. Default: 31536000 (1 year) */
|
|
42
|
+
ttl?: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Interface for a hybrid State + database storage client.
|
|
46
|
+
*/
|
|
47
|
+
export interface DatabaseStorageClient<T extends Document> {
|
|
48
|
+
/**
|
|
49
|
+
* Save data to both State and Database.
|
|
50
|
+
* Values exceeding 1MB are stored in Database only.
|
|
51
|
+
*/
|
|
52
|
+
put(data: T, logger?: ReturnType<typeof AioLogger>): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Retrieve data using State-first, Database-fallback strategy.
|
|
55
|
+
* Includes self-healing: restores to State if found in Database and if the value is within the 1MB size limit.
|
|
56
|
+
* `undefined` is returned if no data is found.
|
|
57
|
+
*/
|
|
58
|
+
get(logger?: ReturnType<typeof AioLogger>): Promise<T | undefined>;
|
|
59
|
+
/**
|
|
60
|
+
* Check if data exists without returning it.
|
|
61
|
+
*/
|
|
62
|
+
exists(logger?: ReturnType<typeof AioLogger>): Promise<boolean>;
|
|
63
|
+
/**
|
|
64
|
+
* Delete data from both State and Database.
|
|
65
|
+
*/
|
|
66
|
+
delete(logger?: ReturnType<typeof AioLogger>): Promise<void>;
|
|
67
|
+
/** The configuration used to create this client */
|
|
68
|
+
readonly config: Readonly<Required<DatabaseStorageClientConfig<T>>>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Create a hybrid storage client to manage a single document of a specific data type.
|
|
72
|
+
*
|
|
73
|
+
* Each client manages ONE document identified by `dbDocumentId`. Calling `put()`
|
|
74
|
+
* will overwrite any existing data. For storing multiple items, create separate
|
|
75
|
+
* clients with different `key` and `dbDocumentId` values.
|
|
76
|
+
*
|
|
77
|
+
* All clients share the same underlying Adobe I/O State and Database - they are
|
|
78
|
+
* lightweight wrappers that provide typed access to specific documents.
|
|
79
|
+
*
|
|
80
|
+
* The client provides `put`, `get`, `exists`, and `delete` functions to manage data.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* interface UserPreferences {
|
|
85
|
+
* theme: 'light' | 'dark';
|
|
86
|
+
* notifications: boolean;
|
|
87
|
+
* }
|
|
88
|
+
*
|
|
89
|
+
* // Each client manages a single document in the shared storage
|
|
90
|
+
* const prefsClient = createDatabaseStorageClient<UserPreferences>({
|
|
91
|
+
* key: 'user-prefs',
|
|
92
|
+
* dbCollection: 'preferences',
|
|
93
|
+
* dbDocumentId: 'user-prefs', // Fixed document ID - only one document per client
|
|
94
|
+
* });
|
|
95
|
+
*
|
|
96
|
+
* // Save preferences (overwrites any existing data)
|
|
97
|
+
* await prefsClient.put({ theme: 'dark', notifications: true });
|
|
98
|
+
*
|
|
99
|
+
* // Retrieve preferences
|
|
100
|
+
* const prefs = await prefsClient.get();
|
|
101
|
+
* if (prefs) {
|
|
102
|
+
* console.log('Theme:', prefs.theme); // Output: "Theme: dark"
|
|
103
|
+
* }
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export declare function createDatabaseStorageClient<T extends Document>(config: DatabaseStorageClientConfig<T>): DatabaseStorageClient<T>;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module aio-persistent-state/database
|
|
4
|
+
*
|
|
5
|
+
* A two-tier key-value storage library for Adobe App Builder runtime that
|
|
6
|
+
* combines Adobe I/O State and Adobe I/O Database to achieve both fast access
|
|
7
|
+
* and long-term durability.
|
|
8
|
+
*
|
|
9
|
+
* **Why both State and Database?**
|
|
10
|
+
* Adobe I/O State enforces a TTL (Time-To-Live) on all entries, meaning
|
|
11
|
+
* cached data will expire and be evicted automatically. Database storage has
|
|
12
|
+
* no such expiration, making it suitable for persistent, long-lived data.
|
|
13
|
+
* By writing to Database for durability and using State as a fast-access cache,
|
|
14
|
+
* we get the best of both: speed from State and persistence from Database.
|
|
15
|
+
*
|
|
16
|
+
* **Architecture:**
|
|
17
|
+
* ```
|
|
18
|
+
* ┌─────────────────────────────────────────────────────────────────────────┐
|
|
19
|
+
* │ State (fast, TTL-based) ←──cache──→ Database (permanent backup) │
|
|
20
|
+
* └─────────────────────────────────────────────────────────────────────────┘
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* **Write flow:** Database first and then State if the value is within the State size limit (1MB).
|
|
24
|
+
* **Read flow:** State first → Database fallback → self-heal (restore to State)
|
|
25
|
+
*
|
|
26
|
+
* @see https://developer.adobe.com/app-builder/docs/guides/app_builder_guides/storage/database
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.createDatabaseStorageClient = createDatabaseStorageClient;
|
|
30
|
+
const aio_lib_db_1 = require("@adobe/aio-lib-db");
|
|
31
|
+
const aio_lib_state_1 = require("@adobe/aio-lib-state");
|
|
32
|
+
const constants_1 = require("./constants");
|
|
33
|
+
const utils_1 = require("./utils");
|
|
34
|
+
let stateLibPromise;
|
|
35
|
+
let dbLibPromise;
|
|
36
|
+
/**
|
|
37
|
+
* Returns a lazily-initialised Adobe I/O State instance.
|
|
38
|
+
* The promise is cached so the SDK is only initialised once per process.
|
|
39
|
+
* If initialisation fails, the cache is cleared so the next call retries.
|
|
40
|
+
*
|
|
41
|
+
* @returns A promise that resolves to the Adobe I/O State instance.
|
|
42
|
+
*/
|
|
43
|
+
function getStateLib() {
|
|
44
|
+
stateLibPromise ??= (0, aio_lib_state_1.init)().catch(err => {
|
|
45
|
+
stateLibPromise = undefined;
|
|
46
|
+
throw err;
|
|
47
|
+
});
|
|
48
|
+
return stateLibPromise;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns a lazily-initialised Adobe I/O Database instance.
|
|
52
|
+
* The promise is cached so the SDK is only initialised once per process.
|
|
53
|
+
* If initialisation fails, the cache is cleared so the next call retries.
|
|
54
|
+
*
|
|
55
|
+
* @returns A promise that resolves to the Adobe I/O Database instance.
|
|
56
|
+
*/
|
|
57
|
+
function getDbLib() {
|
|
58
|
+
dbLibPromise ??= (0, aio_lib_db_1.init)().catch(err => {
|
|
59
|
+
dbLibPromise = undefined;
|
|
60
|
+
throw err;
|
|
61
|
+
});
|
|
62
|
+
return dbLibPromise;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Create a hybrid storage client to manage a single document of a specific data type.
|
|
66
|
+
*
|
|
67
|
+
* Each client manages ONE document identified by `dbDocumentId`. Calling `put()`
|
|
68
|
+
* will overwrite any existing data. For storing multiple items, create separate
|
|
69
|
+
* clients with different `key` and `dbDocumentId` values.
|
|
70
|
+
*
|
|
71
|
+
* All clients share the same underlying Adobe I/O State and Database - they are
|
|
72
|
+
* lightweight wrappers that provide typed access to specific documents.
|
|
73
|
+
*
|
|
74
|
+
* The client provides `put`, `get`, `exists`, and `delete` functions to manage data.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* interface UserPreferences {
|
|
79
|
+
* theme: 'light' | 'dark';
|
|
80
|
+
* notifications: boolean;
|
|
81
|
+
* }
|
|
82
|
+
*
|
|
83
|
+
* // Each client manages a single document in the shared storage
|
|
84
|
+
* const prefsClient = createDatabaseStorageClient<UserPreferences>({
|
|
85
|
+
* key: 'user-prefs',
|
|
86
|
+
* dbCollection: 'preferences',
|
|
87
|
+
* dbDocumentId: 'user-prefs', // Fixed document ID - only one document per client
|
|
88
|
+
* });
|
|
89
|
+
*
|
|
90
|
+
* // Save preferences (overwrites any existing data)
|
|
91
|
+
* await prefsClient.put({ theme: 'dark', notifications: true });
|
|
92
|
+
*
|
|
93
|
+
* // Retrieve preferences
|
|
94
|
+
* const prefs = await prefsClient.get();
|
|
95
|
+
* if (prefs) {
|
|
96
|
+
* console.log('Theme:', prefs.theme); // Output: "Theme: dark"
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
function createDatabaseStorageClient(config) {
|
|
101
|
+
const fullConfig = {
|
|
102
|
+
ttl: constants_1.DEFAULT_ONE_YEAR_TTL_SECONDS,
|
|
103
|
+
...config,
|
|
104
|
+
};
|
|
105
|
+
const encodedKey = (0, utils_1.encodeKey)(fullConfig.key);
|
|
106
|
+
return {
|
|
107
|
+
config: fullConfig,
|
|
108
|
+
/**
|
|
109
|
+
* Save data to both State (fast access) and Database (permanent backup). The data must be JSON-serializable.
|
|
110
|
+
*/
|
|
111
|
+
async put(data, logger) {
|
|
112
|
+
try {
|
|
113
|
+
const jsonData = JSON.stringify(data);
|
|
114
|
+
// Initialize both State and Database clients in parallel
|
|
115
|
+
const [state, db] = await Promise.all([getStateLib(), getDbLib()]);
|
|
116
|
+
// Connect to database and get the collection
|
|
117
|
+
const dbClient = await db.connect();
|
|
118
|
+
const collection = await dbClient.collection(fullConfig.dbCollection);
|
|
119
|
+
// Prepare the database document (flat structure with _id)
|
|
120
|
+
const dbDocument = {
|
|
121
|
+
_id: fullConfig.dbDocumentId,
|
|
122
|
+
...data,
|
|
123
|
+
};
|
|
124
|
+
await collection.replaceOne({ _id: fullConfig.dbDocumentId }, dbDocument, {
|
|
125
|
+
upsert: true,
|
|
126
|
+
});
|
|
127
|
+
(logger ?? utils_1.defaultLogger).debug(`Data saved to Database (key: ${fullConfig.key})`);
|
|
128
|
+
// Values exceeding the 1MB State limit are stored in Database only
|
|
129
|
+
if (Buffer.byteLength(jsonData) > constants_1.MAX_STATE_VALUE_SIZE) {
|
|
130
|
+
(logger ?? utils_1.defaultLogger).warn(`Value for key "${fullConfig.key}" exceeds ${constants_1.MAX_STATE_VALUE_SIZE} bytes, storing in Database only`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await state.put(encodedKey, jsonData, { ttl: fullConfig.ttl });
|
|
134
|
+
(logger ?? utils_1.defaultLogger).debug(`Data saved to State (key: ${fullConfig.key})`);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
(logger ?? utils_1.defaultLogger).error(`Failed to put key: ${fullConfig.key}`, JSON.stringify(error, null, 2));
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
/**
|
|
142
|
+
* Retrieve data using State-first, Database-fallback strategy. If State has expired, data is restored
|
|
143
|
+
* from Database automatically (self-healing). `undefined` is returned if no data is found in either State or
|
|
144
|
+
* Database.
|
|
145
|
+
*/
|
|
146
|
+
async get(logger) {
|
|
147
|
+
try {
|
|
148
|
+
// Try State first (fast path)
|
|
149
|
+
const state = await getStateLib();
|
|
150
|
+
const stateResult = await state.get(encodedKey);
|
|
151
|
+
if (stateResult?.value !== undefined) {
|
|
152
|
+
(logger ?? utils_1.defaultLogger).debug(`Data retrieved from State (key: ${fullConfig.key})`);
|
|
153
|
+
return JSON.parse(stateResult.value);
|
|
154
|
+
}
|
|
155
|
+
// State miss - try Database fallback
|
|
156
|
+
(logger ?? utils_1.defaultLogger).debug('State miss - trying Database fallback');
|
|
157
|
+
const db = await getDbLib();
|
|
158
|
+
const dbClient = await db.connect();
|
|
159
|
+
const collection = await dbClient.collection(fullConfig.dbCollection);
|
|
160
|
+
const doc = await collection.findOne({
|
|
161
|
+
_id: fullConfig.dbDocumentId,
|
|
162
|
+
}, { projection: { _id: 0 } } // Exclude _id from the result to get original data shape
|
|
163
|
+
);
|
|
164
|
+
// Return `undefined` when document not found (this is expected, not an error)
|
|
165
|
+
if (doc === null) {
|
|
166
|
+
(logger ?? utils_1.defaultLogger).debug('Data not found in State or Database');
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
// Re-populate State cache if value is within the 1MB size limit
|
|
170
|
+
const jsonData = JSON.stringify(doc);
|
|
171
|
+
if (Buffer.byteLength(jsonData) <= constants_1.MAX_STATE_VALUE_SIZE) {
|
|
172
|
+
await state.put(encodedKey, jsonData, {
|
|
173
|
+
ttl: fullConfig.ttl,
|
|
174
|
+
});
|
|
175
|
+
// Self-healing: restore to State
|
|
176
|
+
(logger ?? utils_1.defaultLogger).debug('Data restored from Database to State (self-healing)');
|
|
177
|
+
}
|
|
178
|
+
return doc;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
(logger ?? utils_1.defaultLogger).error(`Failed to get key: ${fullConfig.key}`, JSON.stringify(error, null, 2));
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
/**
|
|
186
|
+
* Check if data exists without returning it.
|
|
187
|
+
*/
|
|
188
|
+
async exists(logger) {
|
|
189
|
+
const data = await this.get(logger);
|
|
190
|
+
return data !== undefined;
|
|
191
|
+
},
|
|
192
|
+
/**
|
|
193
|
+
* Delete data from both State and Database.
|
|
194
|
+
*/
|
|
195
|
+
async delete(logger) {
|
|
196
|
+
try {
|
|
197
|
+
const [state, db] = await Promise.all([getStateLib(), getDbLib()]);
|
|
198
|
+
const dbClient = await db.connect();
|
|
199
|
+
const collection = await dbClient.collection(fullConfig.dbCollection);
|
|
200
|
+
await Promise.all([
|
|
201
|
+
state.delete(encodedKey),
|
|
202
|
+
collection.deleteOne({ _id: fullConfig.dbDocumentId }),
|
|
203
|
+
]);
|
|
204
|
+
(logger ?? utils_1.defaultLogger).debug(`Data deleted from State (key: ${fullConfig.key}) and Database`);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
(logger ?? utils_1.defaultLogger).error(`Failed to delete key: ${fullConfig.key}`, JSON.stringify(error, null, 2));
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module aio-persistent-state
|
|
3
|
+
*
|
|
4
|
+
* A two-tier key-value storage library for Adobe App Builder runtime that
|
|
5
|
+
* combines Adobe I/O State and Adobe I/O Files to achieve both fast access
|
|
6
|
+
* and long-term durability.
|
|
7
|
+
*
|
|
8
|
+
* **Why both State and Files?**
|
|
9
|
+
* Adobe I/O State enforces a TTL (Time-To-Live) on all entries, meaning
|
|
10
|
+
* cached data will expire and be evicted automatically. Files storage has
|
|
11
|
+
* no such expiration, making it suitable for persistent, long-lived data.
|
|
12
|
+
* By writing to Files for durability and using State as a fast-access cache,
|
|
13
|
+
* we get the best of both: speed from State and persistence from Files.
|
|
14
|
+
*
|
|
15
|
+
* **1MB size limit:**
|
|
16
|
+
* Adobe I/O State imposes a maximum value size of 1 MB per entry. Values
|
|
17
|
+
* that exceed this limit are stored in Files only, bypassing the State
|
|
18
|
+
* cache entirely. On read, such values are served directly from Files
|
|
19
|
+
* without being cached in State.
|
|
20
|
+
*/
|
|
21
|
+
import AioLogger from '@adobe/aio-lib-core-logging';
|
|
22
|
+
/**
|
|
23
|
+
* Configuration options for creating a file storage client.
|
|
24
|
+
*/
|
|
25
|
+
export interface FileStorageClientConfig {
|
|
26
|
+
/** Key used to identify this data in State and Files storage */
|
|
27
|
+
key: string;
|
|
28
|
+
/** TTL (time-to-live) for State storage in seconds. Default: 31536000 (1 year) */
|
|
29
|
+
ttl?: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Interface for a hybrid State + Files storage client.
|
|
33
|
+
*/
|
|
34
|
+
export interface FileStorageClient {
|
|
35
|
+
/**
|
|
36
|
+
* Stores a value, writing through to both State and Files.
|
|
37
|
+
* Values exceeding 1MB are stored in Files only.
|
|
38
|
+
*/
|
|
39
|
+
put(value: string, logger?: ReturnType<typeof AioLogger>): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Retrieves a value, using State as a cache layer in front of Files.
|
|
42
|
+
* Values exceeding 1MB are stored and retrieved from Files only.
|
|
43
|
+
*/
|
|
44
|
+
get(logger?: ReturnType<typeof AioLogger>): Promise<string | undefined>;
|
|
45
|
+
/**
|
|
46
|
+
* Check if data exists without returning it.
|
|
47
|
+
*/
|
|
48
|
+
exists(logger?: ReturnType<typeof AioLogger>): Promise<boolean>;
|
|
49
|
+
/**
|
|
50
|
+
* Delete data from both State and Files.
|
|
51
|
+
*/
|
|
52
|
+
delete(logger?: ReturnType<typeof AioLogger>): Promise<void>;
|
|
53
|
+
/** The configuration used to create this client */
|
|
54
|
+
readonly config: Readonly<Required<FileStorageClientConfig>>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Create a hybrid storage client that uses State as a cache and Files as durable storage.
|
|
58
|
+
*
|
|
59
|
+
* Each client manages ONE key. Calling `put()` will overwrite any existing data.
|
|
60
|
+
* For storing multiple items, create separate clients with different `key` values.
|
|
61
|
+
*
|
|
62
|
+
* All clients share the same underlying Adobe I/O State and Files instances - they are
|
|
63
|
+
* lightweight wrappers that provide typed access to specific keys.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* const configClient = createFileStorageClient({ key: 'app-config' });
|
|
68
|
+
*
|
|
69
|
+
* // Store a JSON string
|
|
70
|
+
* await configClient.put(JSON.stringify({ theme: 'dark' }));
|
|
71
|
+
*
|
|
72
|
+
* // Retrieve the value
|
|
73
|
+
* const config = await configClient.get();
|
|
74
|
+
* if (config) {
|
|
75
|
+
* console.log(JSON.parse(config)); // { theme: 'dark' }
|
|
76
|
+
* }
|
|
77
|
+
*
|
|
78
|
+
* // Check existence and delete
|
|
79
|
+
* if (await configClient.exists()) {
|
|
80
|
+
* await configClient.delete();
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare function createFileStorageClient(config: FileStorageClientConfig): FileStorageClient;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createFileStorageClient = createFileStorageClient;
|
|
4
|
+
const aio_lib_files_1 = require("@adobe/aio-lib-files");
|
|
5
|
+
const aio_lib_state_1 = require("@adobe/aio-lib-state");
|
|
6
|
+
const constants_1 = require("./constants");
|
|
7
|
+
const utils_1 = require("./utils");
|
|
8
|
+
let stateLibPromise;
|
|
9
|
+
let filesLibPromise;
|
|
10
|
+
/**
|
|
11
|
+
* Returns a lazily-initialised Adobe I/O State instance.
|
|
12
|
+
* The promise is cached so the SDK is only initialised once per process.
|
|
13
|
+
* If initialisation fails, the cache is cleared so the next call retries.
|
|
14
|
+
*
|
|
15
|
+
* @returns A promise that resolves to the Adobe I/O State instance.
|
|
16
|
+
*/
|
|
17
|
+
function getStateLib() {
|
|
18
|
+
stateLibPromise ??= (0, aio_lib_state_1.init)().catch(err => {
|
|
19
|
+
stateLibPromise = undefined;
|
|
20
|
+
throw err;
|
|
21
|
+
});
|
|
22
|
+
return stateLibPromise;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Returns a lazily-initialised Adobe I/O Files instance.
|
|
26
|
+
* The promise is cached so the SDK is only initialised once per process.
|
|
27
|
+
* If initialisation fails, the cache is cleared so the next call retries.
|
|
28
|
+
*
|
|
29
|
+
* @returns A promise that resolves to the Adobe I/O Files instance.
|
|
30
|
+
*/
|
|
31
|
+
function getFilesLib() {
|
|
32
|
+
filesLibPromise ??= (0, aio_lib_files_1.init)().catch(err => {
|
|
33
|
+
filesLibPromise = undefined;
|
|
34
|
+
throw err;
|
|
35
|
+
});
|
|
36
|
+
return filesLibPromise;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create a hybrid storage client that uses State as a cache and Files as durable storage.
|
|
40
|
+
*
|
|
41
|
+
* Each client manages ONE key. Calling `put()` will overwrite any existing data.
|
|
42
|
+
* For storing multiple items, create separate clients with different `key` values.
|
|
43
|
+
*
|
|
44
|
+
* All clients share the same underlying Adobe I/O State and Files instances - they are
|
|
45
|
+
* lightweight wrappers that provide typed access to specific keys.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const configClient = createFileStorageClient({ key: 'app-config' });
|
|
50
|
+
*
|
|
51
|
+
* // Store a JSON string
|
|
52
|
+
* await configClient.put(JSON.stringify({ theme: 'dark' }));
|
|
53
|
+
*
|
|
54
|
+
* // Retrieve the value
|
|
55
|
+
* const config = await configClient.get();
|
|
56
|
+
* if (config) {
|
|
57
|
+
* console.log(JSON.parse(config)); // { theme: 'dark' }
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* // Check existence and delete
|
|
61
|
+
* if (await configClient.exists()) {
|
|
62
|
+
* await configClient.delete();
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
function createFileStorageClient(config) {
|
|
67
|
+
const fullConfig = {
|
|
68
|
+
ttl: constants_1.DEFAULT_ONE_YEAR_TTL_SECONDS,
|
|
69
|
+
...config,
|
|
70
|
+
};
|
|
71
|
+
const encodedKey = (0, utils_1.encodeKey)(fullConfig.key);
|
|
72
|
+
const filePath = `${fullConfig.key}.json`;
|
|
73
|
+
return {
|
|
74
|
+
config: fullConfig,
|
|
75
|
+
/**
|
|
76
|
+
* Retrieves a value by key, using State as a cache layer in front of Files.
|
|
77
|
+
*
|
|
78
|
+
* 1. Attempts to read from State (fast, but subject to TTL expiry).
|
|
79
|
+
* 2. On a cache miss, falls back to Files (persistent, no TTL).
|
|
80
|
+
* 3. If the value from Files is within the 1 MB State limit, it is
|
|
81
|
+
* written back into State so subsequent reads are served from cache.
|
|
82
|
+
*
|
|
83
|
+
* @returns The stored string value, or `undefined` if not found.
|
|
84
|
+
* @throws {Error} If a storage operation fails (other than "not found").
|
|
85
|
+
*/
|
|
86
|
+
async get(logger) {
|
|
87
|
+
try {
|
|
88
|
+
const stateLib = await getStateLib();
|
|
89
|
+
// Try to retrieve from State cache (may be missing due to TTL expiry)
|
|
90
|
+
const cache = await stateLib.get(encodedKey);
|
|
91
|
+
if (cache?.value !== undefined) {
|
|
92
|
+
(logger ?? utils_1.defaultLogger).debug(`Data retrieved from State (key: ${fullConfig.key})`);
|
|
93
|
+
return cache.value;
|
|
94
|
+
}
|
|
95
|
+
// Fallback: read from Files (persistent, no TTL)
|
|
96
|
+
(logger ?? utils_1.defaultLogger).debug('State miss - trying Files fallback');
|
|
97
|
+
const filesLib = await getFilesLib();
|
|
98
|
+
const buffer = await filesLib.read(filePath);
|
|
99
|
+
const value = buffer.toString();
|
|
100
|
+
// Re-populate State cache if value is within the 1MB size limit
|
|
101
|
+
if (Buffer.byteLength(value) <= constants_1.MAX_STATE_VALUE_SIZE) {
|
|
102
|
+
await stateLib.put(encodedKey, value, { ttl: fullConfig.ttl });
|
|
103
|
+
// Self-healing: restore to State
|
|
104
|
+
(logger ?? utils_1.defaultLogger).debug('Data restored from Files to State (self-healing)');
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
(logger ?? utils_1.defaultLogger).error(`Failed to get key: ${fullConfig.key}`, JSON.stringify(error, null, 2));
|
|
110
|
+
// Return `undefined` when data is not found and throw other errors
|
|
111
|
+
if (error instanceof Error &&
|
|
112
|
+
'code' in error &&
|
|
113
|
+
error.code === 'ERROR_FILE_NOT_EXISTS') {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
/**
|
|
120
|
+
* Stores a value by key, writing through to both Files and State.
|
|
121
|
+
*
|
|
122
|
+
* 1. Always writes to Files first to guarantee durability (no TTL).
|
|
123
|
+
* 2. If the value is within the 1 MB State limit, it is also written to
|
|
124
|
+
* State for fast subsequent reads.
|
|
125
|
+
* 3. Values exceeding 1 MB are stored in Files only; State is skipped
|
|
126
|
+
* because it cannot hold entries larger than 1 MB.
|
|
127
|
+
*
|
|
128
|
+
* @param value - The string value to store.
|
|
129
|
+
* @throws {Error} If the encoded key exceeds the maximum size or a
|
|
130
|
+
* storage operation fails.
|
|
131
|
+
*/
|
|
132
|
+
async put(value, logger) {
|
|
133
|
+
try {
|
|
134
|
+
const filesLib = await getFilesLib();
|
|
135
|
+
// Always write to Files first for durability (no TTL)
|
|
136
|
+
await filesLib.write(filePath, value);
|
|
137
|
+
(logger ?? utils_1.defaultLogger).debug(`Data saved to File (file path: ${filePath})`);
|
|
138
|
+
// Values exceeding the 1MB State limit are stored in Files only
|
|
139
|
+
if (Buffer.byteLength(value) > constants_1.MAX_STATE_VALUE_SIZE) {
|
|
140
|
+
(logger ?? utils_1.defaultLogger).warn(`Value for key "${fullConfig.key}" exceeds ${constants_1.MAX_STATE_VALUE_SIZE} bytes, storing in file only`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// Also cache in State for fast access (subject to TTL expiry)
|
|
144
|
+
const stateLib = await getStateLib();
|
|
145
|
+
await stateLib.put(encodedKey, value, {
|
|
146
|
+
ttl: fullConfig.ttl,
|
|
147
|
+
});
|
|
148
|
+
(logger ?? utils_1.defaultLogger).debug(`Data saved to State (key: ${fullConfig.key})`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
(logger ?? utils_1.defaultLogger).error(`Failed to put key: ${fullConfig.key}`, JSON.stringify(error, null, 2));
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
/**
|
|
157
|
+
* Check if data exists without returning it.
|
|
158
|
+
*
|
|
159
|
+
* Returns `true` if data is found in either State or Files storage,
|
|
160
|
+
* even if the stored value is an empty string. Returns `false` only
|
|
161
|
+
* when the underlying file does not exist (i.e., `get()` returns `undefined`).
|
|
162
|
+
*
|
|
163
|
+
* @returns `true` if data exists (including empty strings), `false` if not found.
|
|
164
|
+
*/
|
|
165
|
+
async exists(logger) {
|
|
166
|
+
const value = await this.get(logger);
|
|
167
|
+
return value !== undefined;
|
|
168
|
+
},
|
|
169
|
+
/**
|
|
170
|
+
* Delete data from both State and Files.
|
|
171
|
+
*/
|
|
172
|
+
async delete(logger) {
|
|
173
|
+
try {
|
|
174
|
+
const [stateLib, filesLib] = await Promise.all([getStateLib(), getFilesLib()]);
|
|
175
|
+
await Promise.all([stateLib.delete(encodedKey), filesLib.delete(filePath)]);
|
|
176
|
+
(logger ?? utils_1.defaultLogger).debug(`Data deleted from State (key: ${fullConfig.key}) and File`);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
(logger ?? utils_1.defaultLogger).error(`Failed to delete key: ${fullConfig.key}`, JSON.stringify(error, null, 2));
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Default logger instance for the aio-persistent-state module. */
|
|
2
|
+
export declare const defaultLogger: {
|
|
3
|
+
LogProvider: typeof import("@adobe/aio-lib-core-logging/types/WinstonLogger") | typeof import("@adobe/aio-lib-core-logging/types/DebugLogger");
|
|
4
|
+
logger: import("@adobe/aio-lib-core-logging/types/WinstonLogger") | import("@adobe/aio-lib-core-logging/types/DebugLogger");
|
|
5
|
+
setDefaults(moduleName: any, config: any): void;
|
|
6
|
+
config: {};
|
|
7
|
+
generateLabel(moduleName: any, config: any): string;
|
|
8
|
+
close(): void;
|
|
9
|
+
error(...data?: (object | string)[]): void;
|
|
10
|
+
warn(...data?: (object | string)[]): void;
|
|
11
|
+
info(...data?: (object | string)[]): void;
|
|
12
|
+
log(...data?: (object | string)[]): void;
|
|
13
|
+
verbose(...data?: (object | string)[]): void;
|
|
14
|
+
debug(...data?: (object | string)[]): void;
|
|
15
|
+
silly(...data?: (object | string)[]): void;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Encodes a key using base64url so it is safe for use in Adobe I/O State,
|
|
19
|
+
* which requires keys to match the pattern `/^[a-zA-Z0-9-_.]{1,1024}$/`.
|
|
20
|
+
*
|
|
21
|
+
* @param key - The original key string.
|
|
22
|
+
* @returns The base64url-encoded key.
|
|
23
|
+
* @throws {Error} If the encoded key exceeds 1024 characters ({@link MAX_KEY_SIZE}).
|
|
24
|
+
*/
|
|
25
|
+
export declare function encodeKey(key: string): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.defaultLogger = void 0;
|
|
7
|
+
exports.encodeKey = encodeKey;
|
|
8
|
+
const aio_lib_core_logging_1 = __importDefault(require("@adobe/aio-lib-core-logging"));
|
|
9
|
+
const base64url_1 = __importDefault(require("base64url"));
|
|
10
|
+
const constants_1 = require("./constants");
|
|
11
|
+
/** Default logger instance for the aio-persistent-state module. */
|
|
12
|
+
exports.defaultLogger = (0, aio_lib_core_logging_1.default)('aio-persistent-state', {
|
|
13
|
+
level: 'info',
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Encodes a key using base64url so it is safe for use in Adobe I/O State,
|
|
17
|
+
* which requires keys to match the pattern `/^[a-zA-Z0-9-_.]{1,1024}$/`.
|
|
18
|
+
*
|
|
19
|
+
* @param key - The original key string.
|
|
20
|
+
* @returns The base64url-encoded key.
|
|
21
|
+
* @throws {Error} If the encoded key exceeds 1024 characters ({@link MAX_KEY_SIZE}).
|
|
22
|
+
*/
|
|
23
|
+
function encodeKey(key) {
|
|
24
|
+
const encodedKey = base64url_1.default.encode(key);
|
|
25
|
+
if (encodedKey.length > constants_1.MAX_KEY_SIZE) {
|
|
26
|
+
throw new Error(`Encoded key exceeds maximum size of ${constants_1.MAX_KEY_SIZE} characters`);
|
|
27
|
+
}
|
|
28
|
+
return encodedKey;
|
|
29
|
+
}
|
package/src/index.d.ts
ADDED
package/src/index.js
CHANGED
|
@@ -37,5 +37,4 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.PersistentState = void 0;
|
|
40
|
-
|
|
41
|
-
exports.PersistentState = PersistentState;
|
|
40
|
+
exports.PersistentState = __importStar(require("./aio-persistent-state"));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"5.9.3"}
|
package/docs/functions/get.md
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
[**@aligent/appbuilder-util-lib**](../modules.md)
|
|
2
|
-
|
|
3
|
-
***
|
|
4
|
-
|
|
5
|
-
[@aligent/appbuilder-util-lib](../modules.md) / [aio-persistent-state](../modules/aio-persistent-state.md) / PersistentState.get
|
|
6
|
-
|
|
7
|
-
# Function: PersistentState.get()
|
|
8
|
-
|
|
9
|
-
> **get**(`key`): `Promise`\<`string`\>
|
|
10
|
-
|
|
11
|
-
Retrieves a value by key, using State as a cache layer in front of Files.
|
|
12
|
-
|
|
13
|
-
1. Attempts to read from State (fast, but subject to TTL expiry).
|
|
14
|
-
2. On a cache miss, falls back to Files (persistent, no TTL).
|
|
15
|
-
3. If the value from Files is within the 1 MB State limit, it is
|
|
16
|
-
written back into State so subsequent reads are served from cache.
|
|
17
|
-
|
|
18
|
-
## Parameters
|
|
19
|
-
|
|
20
|
-
### key
|
|
21
|
-
|
|
22
|
-
`string`
|
|
23
|
-
|
|
24
|
-
The key to look up.
|
|
25
|
-
|
|
26
|
-
## Returns
|
|
27
|
-
|
|
28
|
-
`Promise`\<`string`\>
|
|
29
|
-
|
|
30
|
-
The stored string value.
|
|
31
|
-
|
|
32
|
-
## Throws
|
|
33
|
-
|
|
34
|
-
If the encoded key exceeds the maximum size or both storage layers fail.
|
|
35
|
-
|
|
36
|
-
## Example
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
import { PersistentState } from '@aligent/appbuilder-util-lib';
|
|
40
|
-
|
|
41
|
-
const value = await PersistentState.get('myKey');
|
|
42
|
-
```
|
package/docs/functions/put.md
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
[**@aligent/appbuilder-util-lib**](../modules.md)
|
|
2
|
-
|
|
3
|
-
***
|
|
4
|
-
|
|
5
|
-
[@aligent/appbuilder-util-lib](../modules.md) / [aio-persistent-state](../modules/aio-persistent-state.md) / PersistentState.put
|
|
6
|
-
|
|
7
|
-
# Function: PersistentState.put()
|
|
8
|
-
|
|
9
|
-
> **put**(`key`, `value`): `Promise`\<`string`\>
|
|
10
|
-
|
|
11
|
-
Stores a value by key, writing through to both Files and State.
|
|
12
|
-
|
|
13
|
-
1. Always writes to Files first to guarantee durability (no TTL).
|
|
14
|
-
2. If the value is within the 1 MB State limit, it is also written to
|
|
15
|
-
State for fast subsequent reads.
|
|
16
|
-
3. Values exceeding 1 MB are stored in Files only; State is skipped
|
|
17
|
-
because it cannot hold entries larger than 1 MB.
|
|
18
|
-
|
|
19
|
-
## Parameters
|
|
20
|
-
|
|
21
|
-
### key
|
|
22
|
-
|
|
23
|
-
`string`
|
|
24
|
-
|
|
25
|
-
The key under which to store the value.
|
|
26
|
-
|
|
27
|
-
### value
|
|
28
|
-
|
|
29
|
-
`string`
|
|
30
|
-
|
|
31
|
-
The string value to store.
|
|
32
|
-
|
|
33
|
-
## Returns
|
|
34
|
-
|
|
35
|
-
`Promise`\<`string`\>
|
|
36
|
-
|
|
37
|
-
The base64url-encoded key.
|
|
38
|
-
|
|
39
|
-
## Throws
|
|
40
|
-
|
|
41
|
-
If the encoded key exceeds the maximum size or a storage operation fails.
|
|
42
|
-
|
|
43
|
-
## Example
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
import { PersistentState } from '@aligent/appbuilder-util-lib';
|
|
47
|
-
|
|
48
|
-
const encodedKey = await PersistentState.put('myKey', 'myValue');
|
|
49
|
-
```
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.get = get;
|
|
7
|
-
exports.put = put;
|
|
8
|
-
/**
|
|
9
|
-
* @module aio-persistent-state
|
|
10
|
-
*
|
|
11
|
-
* A two-tier key-value storage library for Adobe App Builder runtime that
|
|
12
|
-
* combines Adobe I/O State and Adobe I/O Files to achieve both fast access
|
|
13
|
-
* and long-term durability.
|
|
14
|
-
*
|
|
15
|
-
* **Why both State and Files?**
|
|
16
|
-
* Adobe I/O State enforces a TTL (Time-To-Live) on all entries, meaning
|
|
17
|
-
* cached data will expire and be evicted automatically. Files storage has
|
|
18
|
-
* no such expiration, making it suitable for persistent, long-lived data.
|
|
19
|
-
* By writing to Files for durability and using State as a fast-access cache,
|
|
20
|
-
* we get the best of both: speed from State and persistence from Files.
|
|
21
|
-
*
|
|
22
|
-
* **1MB size limit:**
|
|
23
|
-
* Adobe I/O State imposes a maximum value size of 1 MB per entry. Values
|
|
24
|
-
* that exceed this limit are stored in Files only, bypassing the State
|
|
25
|
-
* cache entirely. On read, such values are served directly from Files
|
|
26
|
-
* without being cached in State.
|
|
27
|
-
*/
|
|
28
|
-
const aio_sdk_1 = require("@adobe/aio-sdk");
|
|
29
|
-
const base64url_1 = __importDefault(require("base64url"));
|
|
30
|
-
const logger = aio_sdk_1.Core.Logger('aio-persistent-state', { level: 'info' });
|
|
31
|
-
let stateLibPromise;
|
|
32
|
-
let filesLibPromise;
|
|
33
|
-
/**
|
|
34
|
-
* Returns a lazily-initialised Adobe I/O State instance.
|
|
35
|
-
* The promise is cached so the SDK is only initialised once per process.
|
|
36
|
-
* If initialisation fails, the cache is cleared so the next call retries.
|
|
37
|
-
*
|
|
38
|
-
* @returns A promise that resolves to the Adobe I/O State instance.
|
|
39
|
-
*/
|
|
40
|
-
function getStateLib() {
|
|
41
|
-
stateLibPromise ??= aio_sdk_1.State.init().catch(err => {
|
|
42
|
-
stateLibPromise = undefined;
|
|
43
|
-
throw err;
|
|
44
|
-
});
|
|
45
|
-
return stateLibPromise;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Returns a lazily-initialised Adobe I/O Files instance.
|
|
49
|
-
* The promise is cached so the SDK is only initialised once per process.
|
|
50
|
-
* If initialisation fails, the cache is cleared so the next call retries.
|
|
51
|
-
*
|
|
52
|
-
* @returns A promise that resolves to the Adobe I/O Files instance.
|
|
53
|
-
*/
|
|
54
|
-
function getFilesLib() {
|
|
55
|
-
filesLibPromise ??= aio_sdk_1.File.init().catch(err => {
|
|
56
|
-
filesLibPromise = undefined;
|
|
57
|
-
throw err;
|
|
58
|
-
});
|
|
59
|
-
return filesLibPromise;
|
|
60
|
-
}
|
|
61
|
-
/** Maximum length (in characters) of a base64url-encoded key accepted by State. */
|
|
62
|
-
const MAX_KEY_SIZE = 1024;
|
|
63
|
-
/** Maximum value size (in bytes) accepted by Adobe I/O State (1 MB). */
|
|
64
|
-
const MAX_VALUE_SIZE = 1024 * 1024; // 1MB
|
|
65
|
-
/**
|
|
66
|
-
* Encodes a key using base64url so it is safe for use in Adobe I/O State,
|
|
67
|
-
* which requires keys to match the pattern `/^[a-zA-Z0-9-_.]{1,1024}$/`.
|
|
68
|
-
*
|
|
69
|
-
* @param key - The original key string.
|
|
70
|
-
* @returns The base64url-encoded key.
|
|
71
|
-
* @throws {Error} If the encoded key exceeds 1024 characters ({@link MAX_KEY_SIZE}).
|
|
72
|
-
*/
|
|
73
|
-
function encodeKey(key) {
|
|
74
|
-
const encodedKey = base64url_1.default.encode(key);
|
|
75
|
-
if (encodedKey.length > MAX_KEY_SIZE) {
|
|
76
|
-
throw new Error(`Encoded key exceeds maximum size of ${MAX_KEY_SIZE} characters`);
|
|
77
|
-
}
|
|
78
|
-
return encodedKey;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Retrieves a value by key, using State as a cache layer in front of Files.
|
|
82
|
-
*
|
|
83
|
-
* 1. Attempts to read from State (fast, but subject to TTL expiry).
|
|
84
|
-
* 2. On a cache miss, falls back to Files (persistent, no TTL).
|
|
85
|
-
* 3. If the value from Files is within the 1 MB State limit, it is
|
|
86
|
-
* written back into State so subsequent reads are served from cache.
|
|
87
|
-
*
|
|
88
|
-
* @param key - The key to look up.
|
|
89
|
-
* @returns The stored string value.
|
|
90
|
-
* @throws {Error} If the encoded key exceeds the maximum size or both
|
|
91
|
-
* storage layers fail.
|
|
92
|
-
*/
|
|
93
|
-
async function get(key) {
|
|
94
|
-
try {
|
|
95
|
-
const stateLib = await getStateLib();
|
|
96
|
-
const encodedKey = encodeKey(key);
|
|
97
|
-
// Try to retrieve from State cache (may be missing due to TTL expiry)
|
|
98
|
-
const cache = await stateLib.get(encodedKey);
|
|
99
|
-
if (cache?.value != null) {
|
|
100
|
-
return cache.value;
|
|
101
|
-
}
|
|
102
|
-
// Fallback: read from Files (persistent, no TTL)
|
|
103
|
-
const filesLib = await getFilesLib();
|
|
104
|
-
const buffer = await filesLib.read(`${key}.json`);
|
|
105
|
-
const value = buffer.toString();
|
|
106
|
-
// Re-populate State cache if value is within the 1MB size limit
|
|
107
|
-
if (Buffer.byteLength(value) <= MAX_VALUE_SIZE) {
|
|
108
|
-
await stateLib.put(encodedKey, value);
|
|
109
|
-
}
|
|
110
|
-
return value;
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
logger.error(`Failed to get key: ${key}`, JSON.stringify(error, null, 2));
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Stores a value by key, writing through to both Files and State.
|
|
119
|
-
*
|
|
120
|
-
* 1. Always writes to Files first to guarantee durability (no TTL).
|
|
121
|
-
* 2. If the value is within the 1 MB State limit, it is also written to
|
|
122
|
-
* State for fast subsequent reads.
|
|
123
|
-
* 3. Values exceeding 1 MB are stored in Files only; State is skipped
|
|
124
|
-
* because it cannot hold entries larger than 1 MB.
|
|
125
|
-
*
|
|
126
|
-
* @param key - The key under which to store the value.
|
|
127
|
-
* @param value - The string value to store.
|
|
128
|
-
* @returns The base64url-encoded key.
|
|
129
|
-
* @throws {Error} If the encoded key exceeds the maximum size or a
|
|
130
|
-
* storage operation fails.
|
|
131
|
-
*/
|
|
132
|
-
async function put(key, value) {
|
|
133
|
-
try {
|
|
134
|
-
const encodedKey = encodeKey(key);
|
|
135
|
-
const filesLib = await getFilesLib();
|
|
136
|
-
// Always write to Files first for durability (no TTL)
|
|
137
|
-
await filesLib.write(`${key}.json`, value);
|
|
138
|
-
// Values exceeding the 1MB State limit are stored in Files only
|
|
139
|
-
if (Buffer.byteLength(value) > MAX_VALUE_SIZE) {
|
|
140
|
-
logger.warn(`Value for key "${key}" exceeds ${MAX_VALUE_SIZE} bytes, storing in file only`);
|
|
141
|
-
return encodedKey;
|
|
142
|
-
}
|
|
143
|
-
// Also cache in State for fast access (subject to TTL expiry)
|
|
144
|
-
const stateLib = await getStateLib();
|
|
145
|
-
return await stateLib.put(encodedKey, value);
|
|
146
|
-
}
|
|
147
|
-
catch (error) {
|
|
148
|
-
logger.error(`Failed to put key: ${key}`, JSON.stringify(error, null, 2));
|
|
149
|
-
throw error;
|
|
150
|
-
}
|
|
151
|
-
}
|