@aligent/appbuilder-util-lib 0.2.0 → 0.2.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.
@@ -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
- using Adobe I/O State (for caching) and Adobe I/O File (for durability).
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 a high-level abstraction for key-value storage using Adobe App Builder's runtime services. It combines:
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
- ### Why both State and Files?
18
+ ```
19
+ ┌─────────────────────────────────────────────────────────────────────────┐
20
+ │ State (fast, TTL-based) ←──cache──→ Files/Database (permanent) │
21
+ └─────────────────────────────────────────────────────────────────────────┘
22
+ ```
19
23
 
20
- Adobe I/O State enforces a **TTL (Time-To-Live)** on all entries, so cached data will eventually expire and be evicted. Files storage has no such expiration, making it suitable for persistent, long-lived data. By writing to Files for durability and using State as a fast-access cache, we get speed from State and persistence from Files.
24
+ ### Why two tiers?
21
25
 
22
- ### 1 MB State size limit
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
- Adobe I/O State imposes a **maximum value size of 1 MB** per entry. This library works around the limit automatically:
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
- Values within the 1 MB limit are stored in both layers so that subsequent reads are served from the faster State cache.
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
- ### How it works
37
+ ## Interfaces
31
38
 
32
- - Data is retrieved quickly from State if present (cache hit)
33
- - If not found in State (cache miss, e.g. due to TTL expiry), the library loads from Files, then re-populates the State cache
34
- - On updates, the value is written to Files first for durability, then cached in State
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
- - [PersistentState.get](../functions/get.md)
39
- - [PersistentState.put](../functions/put.md)
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.0",
3
+ "version": "0.2.1-0",
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-sdk": "^6.0.0",
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,2 @@
1
+ export * from './state-database';
2
+ export * from './state-files';
@@ -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
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Main entry point for @aligent/appbuilder-util-lib
3
+ */
4
+ export * as PersistentState from './aio-persistent-state';
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
- const PersistentState = __importStar(require("./aio-persistent-state/aio-persistent-state"));
41
- exports.PersistentState = PersistentState;
40
+ exports.PersistentState = __importStar(require("./aio-persistent-state"));
@@ -0,0 +1 @@
1
+ {"version":"5.9.3"}
@@ -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
- ```
@@ -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
- }