@microsoft/agents-hosting 1.9.0-beta.3.gb2d23c1c44 → 1.9.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/README.md +182 -4
- package/dist/package.json +3 -3
- package/dist/src/app/agentApplication.d.ts +4 -3
- package/dist/src/app/agentApplication.js +24 -21
- package/dist/src/app/agentApplication.js.map +1 -1
- package/dist/src/app/agentApplicationBuilder.d.ts +9 -2
- package/dist/src/app/agentApplicationBuilder.js +9 -0
- package/dist/src/app/agentApplicationBuilder.js.map +1 -1
- package/dist/src/app/agentApplicationOptions.d.ts +16 -4
- package/dist/src/app/auth/authorization.d.ts +54 -0
- package/dist/src/app/auth/authorization.js +63 -2
- package/dist/src/app/auth/authorization.js.map +1 -1
- package/dist/src/app/auth/authorizationManager.d.ts +0 -8
- package/dist/src/app/auth/authorizationManager.js +104 -178
- package/dist/src/app/auth/authorizationManager.js.map +1 -1
- package/dist/src/app/auth/handlerStorage.d.ts +5 -3
- package/dist/src/app/auth/handlerStorage.js +36 -10
- package/dist/src/app/auth/handlerStorage.js.map +1 -1
- package/dist/src/app/auth/types.d.ts +2 -2
- package/dist/src/app/index.d.ts +1 -0
- package/dist/src/app/proactive/proactive.js +10 -6
- package/dist/src/app/proactive/proactive.js.map +1 -1
- package/dist/src/app/proactive/proactiveOptions.d.ts +2 -2
- package/dist/src/app/rateLimit.d.ts +315 -0
- package/dist/src/app/rateLimit.js +333 -0
- package/dist/src/app/rateLimit.js.map +1 -0
- package/dist/src/app/turnState.d.ts +6 -3
- package/dist/src/app/turnState.js +43 -4
- package/dist/src/app/turnState.js.map +1 -1
- package/dist/src/auth/authConfiguration.d.ts +10 -3
- package/dist/src/auth/authConfiguration.js +292 -259
- package/dist/src/auth/authConfiguration.js.map +1 -1
- package/dist/src/auth/delegatedTokenCredential.d.ts +29 -0
- package/dist/src/auth/delegatedTokenCredential.js +49 -0
- package/dist/src/auth/delegatedTokenCredential.js.map +1 -0
- package/dist/src/auth/msal/msalTokenCredential.js +1 -1
- package/dist/src/auth/msal/msalTokenCredential.js.map +1 -1
- package/dist/src/auth/settings.d.ts +8 -70
- package/dist/src/auth/settings.js +5 -67
- package/dist/src/auth/settings.js.map +1 -1
- package/dist/src/cloudAdapter.d.ts +5 -0
- package/dist/src/cloudAdapter.js +13 -103
- package/dist/src/cloudAdapter.js.map +1 -1
- package/dist/src/configuration/botFrameworkEnvironmentCompatibility.d.ts +23 -0
- package/dist/src/configuration/botFrameworkEnvironmentCompatibility.js +183 -0
- package/dist/src/configuration/botFrameworkEnvironmentCompatibility.js.map +1 -0
- package/dist/src/configuration/configuration.d.ts +83 -0
- package/dist/src/configuration/configuration.js +836 -0
- package/dist/src/configuration/configuration.js.map +1 -0
- package/dist/src/configuration/configurationSource.d.ts +57 -0
- package/dist/src/configuration/configurationSource.js +7 -0
- package/dist/src/configuration/configurationSource.js.map +1 -0
- package/dist/src/configuration/environmentConfiguration.d.ts +14 -0
- package/dist/src/configuration/environmentConfiguration.js +271 -0
- package/dist/src/configuration/environmentConfiguration.js.map +1 -0
- package/dist/src/configuration/index.d.ts +7 -0
- package/dist/src/configuration/index.js +14 -0
- package/dist/src/configuration/index.js.map +1 -0
- package/dist/src/configuration/jsonFileConfigurationSource.d.ts +15 -0
- package/dist/src/configuration/jsonFileConfigurationSource.js +58 -0
- package/dist/src/configuration/jsonFileConfigurationSource.js.map +1 -0
- package/dist/src/createCloudAdapter.d.ts +30 -5
- package/dist/src/createCloudAdapter.js +17 -10
- package/dist/src/createCloudAdapter.js.map +1 -1
- package/dist/src/errorHelper.js +124 -5
- package/dist/src/errorHelper.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/observability/traces.d.ts +6 -4
- package/dist/src/observability/traces.js +10 -9
- package/dist/src/observability/traces.js.map +1 -1
- package/dist/src/outboundHostValidator.d.ts +4 -1
- package/dist/src/outboundHostValidator.js +25 -48
- package/dist/src/outboundHostValidator.js.map +1 -1
- package/dist/src/state/agentState.d.ts +8 -3
- package/dist/src/state/agentState.js +26 -8
- package/dist/src/state/agentState.js.map +1 -1
- package/dist/src/state/conversationState.d.ts +2 -2
- package/dist/src/state/conversationState.js.map +1 -1
- package/dist/src/state/userState.d.ts +2 -2
- package/dist/src/state/userState.js.map +1 -1
- package/dist/src/statusCodes.d.ts +5 -0
- package/dist/src/statusCodes.js +5 -0
- package/dist/src/statusCodes.js.map +1 -1
- package/dist/src/storage/fileStorage.d.ts +83 -68
- package/dist/src/storage/fileStorage.js +279 -105
- package/dist/src/storage/fileStorage.js.map +1 -1
- package/dist/src/storage/index.d.ts +1 -0
- package/dist/src/storage/index.js +1 -0
- package/dist/src/storage/index.js.map +1 -1
- package/dist/src/storage/memoryStorage.d.ts +85 -60
- package/dist/src/storage/memoryStorage.js +244 -87
- package/dist/src/storage/memoryStorage.js.map +1 -1
- package/dist/src/storage/storage.d.ts +100 -2
- package/dist/src/storage/storage.js +25 -0
- package/dist/src/storage/storage.js.map +1 -1
- package/dist/src/storage/storageCompatibility.d.ts +17 -0
- package/dist/src/storage/storageCompatibility.js +157 -0
- package/dist/src/storage/storageCompatibility.js.map +1 -0
- package/dist/src/storage/storageExpiry.d.ts +6 -0
- package/dist/src/storage/storageExpiry.js +20 -0
- package/dist/src/storage/storageExpiry.js.map +1 -0
- package/dist/src/utils/env.d.ts +43 -0
- package/dist/src/utils/env.js +42 -6
- package/dist/src/utils/env.js.map +1 -1
- package/dist/src/utils.d.ts +1 -0
- package/dist/src/utils.js +10 -1
- package/dist/src/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/app/agentApplication.ts +27 -14
- package/src/app/agentApplicationBuilder.ts +13 -2
- package/src/app/agentApplicationOptions.ts +18 -4
- package/src/app/auth/authorization.ts +67 -2
- package/src/app/auth/authorizationManager.ts +126 -204
- package/src/app/auth/handlerStorage.ts +51 -7
- package/src/app/auth/types.ts +2 -2
- package/src/app/index.ts +7 -0
- package/src/app/proactive/proactive.ts +19 -9
- package/src/app/proactive/proactiveOptions.ts +2 -2
- package/src/app/rateLimit.ts +659 -0
- package/src/app/turnState.ts +50 -7
- package/src/auth/authConfiguration.ts +415 -260
- package/src/auth/delegatedTokenCredential.ts +49 -0
- package/src/auth/msal/msalTokenCredential.ts +1 -1
- package/src/auth/settings.ts +5 -82
- package/src/cloudAdapter.ts +38 -113
- package/src/configuration/botFrameworkEnvironmentCompatibility.ts +245 -0
- package/src/configuration/configuration.ts +1076 -0
- package/src/configuration/configurationSource.ts +76 -0
- package/src/configuration/environmentConfiguration.ts +377 -0
- package/src/configuration/index.ts +19 -0
- package/src/configuration/jsonFileConfigurationSource.ts +74 -0
- package/src/createCloudAdapter.ts +52 -9
- package/src/errorHelper.ts +151 -5
- package/src/index.ts +1 -0
- package/src/observability/traces.ts +8 -7
- package/src/outboundHostValidator.ts +37 -44
- package/src/state/agentState.ts +39 -13
- package/src/state/conversationState.ts +2 -2
- package/src/state/userState.ts +2 -2
- package/src/statusCodes.ts +6 -0
- package/src/storage/fileStorage.ts +299 -106
- package/src/storage/index.ts +1 -0
- package/src/storage/memoryStorage.ts +268 -85
- package/src/storage/storage.ts +114 -2
- package/src/storage/storageCompatibility.ts +185 -0
- package/src/storage/storageExpiry.ts +21 -0
- package/src/utils/env.ts +63 -0
- package/src/utils.ts +10 -0
|
@@ -3,170 +3,363 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
import { randomUUID } from 'node:crypto'
|
|
9
|
+
import { ExceptionHelper } from '@microsoft/agents-activity'
|
|
10
|
+
import { debug, redactString, trace } from '@microsoft/agents-telemetry'
|
|
11
|
+
import { Errors } from '../errorHelper'
|
|
10
12
|
import { StorageTraceDefinitions } from '../observability'
|
|
13
|
+
import { getStorageWriteExpiry } from './storageExpiry'
|
|
14
|
+
import {
|
|
15
|
+
StorageDeleteOptions,
|
|
16
|
+
StorageDeleteResults,
|
|
17
|
+
StorageOperationStatus,
|
|
18
|
+
StorageReadResults,
|
|
19
|
+
StorageWriteMode,
|
|
20
|
+
StorageWriteOptions,
|
|
21
|
+
StorageWriteResults,
|
|
22
|
+
Storage,
|
|
23
|
+
StorageV2,
|
|
24
|
+
StoreItem,
|
|
25
|
+
} from './storage'
|
|
26
|
+
|
|
27
|
+
const logger = debug('agents:file-storage')
|
|
28
|
+
|
|
29
|
+
class FileStorageInternals {
|
|
30
|
+
private readonly statePath: string
|
|
31
|
+
private readonly versionsPath: string
|
|
32
|
+
private readonly expirationsPath: string
|
|
33
|
+
readonly state: Record<string, unknown>
|
|
34
|
+
readonly versions: Record<string, string>
|
|
35
|
+
readonly expirations: Record<string, number>
|
|
36
|
+
|
|
37
|
+
constructor (folder: string) {
|
|
38
|
+
fs.mkdirSync(folder, { recursive: true })
|
|
39
|
+
this.statePath = path.join(folder, 'state.json')
|
|
40
|
+
this.versionsPath = path.join(folder, 'state.versions.json')
|
|
41
|
+
this.expirationsPath = path.join(folder, 'state.expirations.json')
|
|
42
|
+
if (!fs.existsSync(this.statePath)) fs.writeFileSync(this.statePath, '{}')
|
|
43
|
+
this.state = JSON.parse(fs.readFileSync(this.statePath, 'utf8')) as Record<string, unknown>
|
|
44
|
+
this.versions = fs.existsSync(this.versionsPath)
|
|
45
|
+
? JSON.parse(fs.readFileSync(this.versionsPath, 'utf8')) as Record<string, string>
|
|
46
|
+
: {}
|
|
47
|
+
this.expirations = fs.existsSync(this.expirationsPath)
|
|
48
|
+
? JSON.parse(fs.readFileSync(this.expirationsPath, 'utf8')) as Record<string, number>
|
|
49
|
+
: {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
flush (): void {
|
|
53
|
+
fs.writeFileSync(this.statePath, JSON.stringify(this.state, null, 2))
|
|
54
|
+
if (fs.existsSync(this.versionsPath) || Object.keys(this.versions).length > 0) {
|
|
55
|
+
fs.writeFileSync(this.versionsPath, JSON.stringify(this.versions, null, 2))
|
|
56
|
+
}
|
|
57
|
+
if (fs.existsSync(this.expirationsPath) || Object.keys(this.expirations).length > 0) {
|
|
58
|
+
fs.writeFileSync(this.expirationsPath, JSON.stringify(this.expirations, null, 2))
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getOrCreateVersion (key: string): string {
|
|
63
|
+
const version = this.versions[key] ?? randomUUID()
|
|
64
|
+
this.versions[key] = version
|
|
65
|
+
return version
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
isExpired (key: string): boolean {
|
|
69
|
+
const expiresAt = this.expirations[key]
|
|
70
|
+
return expiresAt !== undefined && expiresAt <= Date.now()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
remove (key: string): void {
|
|
74
|
+
delete this.state[key]
|
|
75
|
+
delete this.versions[key]
|
|
76
|
+
delete this.expirations[key]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
11
79
|
|
|
12
80
|
/**
|
|
13
81
|
* A file-based storage implementation that persists data to the local filesystem.
|
|
14
82
|
*
|
|
15
83
|
* @remarks
|
|
16
|
-
* FileStorage stores all data in a single JSON file named
|
|
84
|
+
* FileStorage stores all data in a single JSON file named `state.json` within a specified folder.
|
|
17
85
|
* This implementation is suitable for development scenarios, local testing, and single-instance
|
|
18
86
|
* deployments where shared state across multiple instances is not required.
|
|
19
87
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* wrapped in Promise interfaces
|
|
88
|
+
* Values remain a key-value JSON object in `state.json`. V2 keeps generated storage versions in
|
|
89
|
+
* `state.versions.json` so a value's own `eTag` property is not changed. All operations use
|
|
90
|
+
* synchronous file I/O wrapped in Promise interfaces. Use {@link FileStorageV2} for the
|
|
91
|
+
* structured StorageV2 contract.
|
|
92
|
+
* V2 supports create-only, replace, and expected-version conditions.
|
|
93
|
+
*
|
|
94
|
+
* The inherited constructor creates the folder and `state.json` when needed, then loads values
|
|
95
|
+
* and V2 version metadata into memory. It creates `state.versions.json` on the first V2 write.
|
|
23
96
|
*
|
|
24
97
|
* ### Warning
|
|
25
98
|
* This implementation does not provide:
|
|
26
99
|
* - Thread safety for concurrent access
|
|
27
|
-
* - Optimistic concurrency control (eTag support)
|
|
28
100
|
* - Atomic operations across multiple keys
|
|
29
101
|
* - Scale for large datasets
|
|
30
102
|
*
|
|
31
|
-
* For production scenarios requiring these features,
|
|
32
|
-
* database-backed storage implementations instead.
|
|
103
|
+
* For production scenarios requiring these features, use a database-backed storage implementation.
|
|
33
104
|
*
|
|
34
105
|
* @example
|
|
35
106
|
* ```typescript
|
|
36
|
-
* const
|
|
107
|
+
* const legacyStorage = new FileStorage('./data')
|
|
108
|
+
* const storageV2 = new FileStorageV2('./data-v2')
|
|
37
109
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* 'conversation456': { turn: 5, context: 'discussing weather' }
|
|
42
|
-
* });
|
|
110
|
+
* await storageV2.write({
|
|
111
|
+
* user123: { name: 'John', lastSeen: new Date().toISOString() }
|
|
112
|
+
* })
|
|
43
113
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* console.log(data.user123); // { name: 'John', lastSeen: '...' }
|
|
114
|
+
* const result = await storageV2.read(['user123'])
|
|
115
|
+
* console.log(result.user123.value)
|
|
47
116
|
*
|
|
48
|
-
*
|
|
49
|
-
* await storage.delete(['conversation456']);
|
|
117
|
+
* await storageV2.delete(['user123'])
|
|
50
118
|
* ```
|
|
51
|
-
*
|
|
52
119
|
*/
|
|
53
|
-
export class FileStorage implements Storage {
|
|
54
|
-
|
|
55
|
-
|
|
120
|
+
export class FileStorage extends FileStorageInternals implements Storage {
|
|
121
|
+
/**
|
|
122
|
+
* Reads legacy items from `state.json`.
|
|
123
|
+
*
|
|
124
|
+
* @param keys The keys to read; must not be empty.
|
|
125
|
+
* @returns Existing items keyed by storage key. Missing keys are omitted.
|
|
126
|
+
*/
|
|
127
|
+
async read (keys: string[]): Promise<StoreItem> {
|
|
128
|
+
return trace(StorageTraceDefinitions.read, async ({ record }) => {
|
|
129
|
+
record({ keyCount: keys?.length })
|
|
130
|
+
if (!keys || keys.length === 0) {
|
|
131
|
+
throw ExceptionHelper.generateException(ReferenceError, Errors.StorageReadKeysRequired)
|
|
132
|
+
}
|
|
133
|
+
const items = Object.fromEntries(keys
|
|
134
|
+
.filter(key => Boolean(this.state[key]))
|
|
135
|
+
.map(key => [key, this.state[key]])) as StoreItem
|
|
136
|
+
return items
|
|
137
|
+
})
|
|
138
|
+
}
|
|
56
139
|
|
|
57
140
|
/**
|
|
58
|
-
*
|
|
141
|
+
* Writes legacy items to `state.json`.
|
|
59
142
|
*
|
|
60
|
-
* @param
|
|
61
|
-
* @throws
|
|
143
|
+
* @param changes The items to write, keyed by storage key.
|
|
144
|
+
* @throws If `changes` is invalid or the file cannot be written.
|
|
145
|
+
*/
|
|
146
|
+
async write (changes: StoreItem): Promise<void> {
|
|
147
|
+
return trace(StorageTraceDefinitions.write, async ({ record }) => {
|
|
148
|
+
record({ keyCount: changes ? Object.keys(changes).length : undefined })
|
|
149
|
+
if (!changes || typeof changes !== 'object' || Array.isArray(changes)) {
|
|
150
|
+
throw ExceptionHelper.generateException(ReferenceError, Errors.StorageWriteChangesRequired)
|
|
151
|
+
}
|
|
152
|
+
Object.assign(this.state, changes)
|
|
153
|
+
for (const key of Object.keys(changes)) {
|
|
154
|
+
delete this.versions[key]
|
|
155
|
+
delete this.expirations[key]
|
|
156
|
+
}
|
|
157
|
+
this.flush()
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Deletes legacy items from `state.json`.
|
|
62
163
|
*
|
|
63
|
-
* @
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
164
|
+
* @param keys The keys to delete; must not be empty. Missing keys are ignored.
|
|
165
|
+
*/
|
|
166
|
+
async delete (keys: string[]): Promise<void> {
|
|
167
|
+
return trace(StorageTraceDefinitions.delete, async ({ record }) => {
|
|
168
|
+
record({ keyCount: keys?.length })
|
|
169
|
+
if (!keys || keys.length === 0) {
|
|
170
|
+
throw ExceptionHelper.generateException(ReferenceError, Errors.StorageDeleteKeysRequired)
|
|
171
|
+
}
|
|
172
|
+
for (const key of keys) {
|
|
173
|
+
this.remove(key)
|
|
174
|
+
}
|
|
175
|
+
this.flush()
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* A file-backed provider for the structured {@link StorageV2} contract.
|
|
182
|
+
*
|
|
183
|
+
* This is the V2 counterpart to {@link FileStorage}. Values are stored in `state.json`, while
|
|
184
|
+
* generated storage versions are stored separately in `state.versions.json`; an `eTag` property
|
|
185
|
+
* in an application value is therefore preserved. Reads and mutations return a result for every
|
|
186
|
+
* requested key and support create-only, replace, and expected-version conditions.
|
|
187
|
+
*
|
|
188
|
+
* Like {@link FileStorage}, this provider is for development, local testing, and single-instance
|
|
189
|
+
* deployments. It does not provide cross-process concurrency or multi-key atomicity.
|
|
190
|
+
*/
|
|
191
|
+
export class FileStorageV2 extends StorageV2 {
|
|
192
|
+
private readonly internals: FileStorageInternals
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Creates a V2 file storage provider.
|
|
68
196
|
*
|
|
197
|
+
* @param folder The absolute or relative folder where `state.json` is stored.
|
|
198
|
+
* @throws May throw filesystem errors if the folder or state file cannot be created or read.
|
|
69
199
|
*/
|
|
70
200
|
constructor (folder: string) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
fs.mkdirSync(folder, { recursive: true })
|
|
74
|
-
}
|
|
75
|
-
if (!fs.existsSync(path.join(folder, 'state.json'))) {
|
|
76
|
-
fs.writeFileSync(path.join(folder, 'state.json'), '{}')
|
|
77
|
-
}
|
|
78
|
-
const data = fs.readFileSync(path.join(folder, 'state.json'), 'utf8')
|
|
79
|
-
this._stateFile = JSON.parse(data)
|
|
201
|
+
super()
|
|
202
|
+
this.internals = new FileStorageInternals(folder)
|
|
80
203
|
}
|
|
81
204
|
|
|
82
205
|
/**
|
|
83
|
-
* Reads
|
|
84
|
-
*
|
|
85
|
-
* @param keys Array of keys to read from storage
|
|
86
|
-
* @returns Promise resolving to an object containing the requested items (keys that don't exist are omitted)
|
|
87
|
-
* @throws ReferenceError if keys array is empty or undefined
|
|
88
|
-
*
|
|
89
|
-
* @remarks
|
|
90
|
-
* This method reads from the in-memory cache that was loaded during construction,
|
|
91
|
-
* making it very fast but potentially returning stale data if the file was
|
|
92
|
-
* modified by external processes.
|
|
206
|
+
* Reads items and their stored versions from the local state files.
|
|
93
207
|
*
|
|
208
|
+
* @param keys The keys to read. Empty batches are valid.
|
|
209
|
+
* @returns A result for every key, with `notFound` for missing items. Returned values are cloned.
|
|
94
210
|
*/
|
|
95
|
-
read (keys: string[])
|
|
211
|
+
async read<T extends object = Record<string, unknown>> (keys: string[]): Promise<StorageReadResults<T>> {
|
|
96
212
|
return trace(StorageTraceDefinitions.read, async ({ record }) => {
|
|
97
213
|
record({ keyCount: keys?.length })
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
data[key] = item
|
|
107
|
-
}
|
|
214
|
+
validateV2Keys(keys)
|
|
215
|
+
let createdVersion = false
|
|
216
|
+
const results = Object.fromEntries(keys.map(key => {
|
|
217
|
+
if (!Object.prototype.hasOwnProperty.call(this.internals.state, key) || this.internals.isExpired(key)) {
|
|
218
|
+
if (this.internals.isExpired(key)) {
|
|
219
|
+
logger.info('Item expired, deleting from storage', { key: redactString(key, true) })
|
|
220
|
+
this.internals.remove(key)
|
|
221
|
+
createdVersion = true
|
|
108
222
|
}
|
|
109
|
-
|
|
223
|
+
return [key, { key, status: StorageOperationStatus.NotFound }]
|
|
110
224
|
}
|
|
111
|
-
|
|
225
|
+
const value = structuredClone(this.internals.state[key]) as T
|
|
226
|
+
const version = this.internals.versions[key]
|
|
227
|
+
createdVersion ||= version === undefined
|
|
228
|
+
return [key, {
|
|
229
|
+
key,
|
|
230
|
+
status: StorageOperationStatus.Succeeded,
|
|
231
|
+
value,
|
|
232
|
+
version: version ?? this.internals.getOrCreateVersion(key),
|
|
233
|
+
}]
|
|
234
|
+
}))
|
|
235
|
+
if (createdVersion) this.internals.flush()
|
|
236
|
+
return results
|
|
112
237
|
})
|
|
113
238
|
}
|
|
114
239
|
|
|
115
240
|
/**
|
|
116
|
-
* Writes
|
|
117
|
-
*
|
|
118
|
-
* @param changes Object containing key-value pairs to write to storage
|
|
119
|
-
* @returns Promise that resolves when the write operation completes
|
|
120
|
-
*
|
|
121
|
-
* @remarks
|
|
122
|
-
* This method updates both the in-memory cache and writes the entire state
|
|
123
|
-
* to the state.json file. The file is written with pretty-printing (2-space indentation)
|
|
124
|
-
* for better readability during development and debugging.
|
|
125
|
-
*
|
|
126
|
-
* > [!NOTE]
|
|
127
|
-
* > This implementation does not support eTag-based optimistic concurrency control.
|
|
128
|
-
* > Any eTag values in the changes object are ignored.
|
|
241
|
+
* Writes cloned items and generated versions to the local state files.
|
|
129
242
|
*
|
|
243
|
+
* @param changes The values to write, keyed by storage key.
|
|
244
|
+
* @param options Create-only, replace, or expected-version conditions.
|
|
245
|
+
* @returns A result for every supplied key, including concurrency-condition outcomes.
|
|
130
246
|
*/
|
|
131
|
-
write (changes:
|
|
247
|
+
async write<T extends object = Record<string, unknown>> (changes: Record<string, T>, options?: StorageWriteOptions): Promise<StorageWriteResults> {
|
|
132
248
|
return trace(StorageTraceDefinitions.write, async ({ record }) => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
249
|
+
record({ keyCount: changes ? Object.keys(changes).length : undefined })
|
|
250
|
+
validateExpectedVersion(options?.expectedVersion)
|
|
251
|
+
validateV2Changes(changes)
|
|
252
|
+
|
|
253
|
+
const results: StorageWriteResults = {}
|
|
254
|
+
const expiresAt = getStorageWriteExpiry(options)
|
|
255
|
+
const mode = options?.mode ?? StorageWriteMode.Upsert
|
|
256
|
+
validateWriteMode(mode)
|
|
257
|
+
let changed = false
|
|
258
|
+
let createdVersion = false
|
|
259
|
+
for (const [key, value] of Object.entries(changes)) {
|
|
260
|
+
if (this.internals.isExpired(key)) {
|
|
261
|
+
logger.info('Item expired, deleting from storage', { key: redactString(key, true) })
|
|
262
|
+
this.internals.remove(key)
|
|
263
|
+
changed = true
|
|
264
|
+
}
|
|
265
|
+
const current = this.internals.state[key]
|
|
266
|
+
createdVersion ||= current !== undefined && this.internals.versions[key] === undefined
|
|
267
|
+
const currentVersion = current === undefined
|
|
268
|
+
? undefined
|
|
269
|
+
: this.internals.getOrCreateVersion(key)
|
|
270
|
+
if (mode === StorageWriteMode.CreateOnly && current !== undefined) {
|
|
271
|
+
results[key] = { key, status: StorageOperationStatus.Conflict, version: currentVersion }
|
|
272
|
+
} else if (mode === StorageWriteMode.Replace && current === undefined) {
|
|
273
|
+
results[key] = { key, status: StorageOperationStatus.NotFound }
|
|
274
|
+
} else if (options?.expectedVersion !== undefined && options.expectedVersion !== currentVersion) {
|
|
275
|
+
results[key] = { key, status: StorageOperationStatus.ConditionNotMet, version: currentVersion }
|
|
276
|
+
} else {
|
|
277
|
+
const version = randomUUID()
|
|
278
|
+
this.internals.state[key] = structuredClone(value)
|
|
279
|
+
this.internals.versions[key] = version
|
|
280
|
+
if (expiresAt === undefined) delete this.internals.expirations[key]
|
|
281
|
+
else this.internals.expirations[key] = expiresAt
|
|
282
|
+
results[key] = { key, status: StorageOperationStatus.Succeeded, version }
|
|
283
|
+
changed = true
|
|
284
|
+
}
|
|
137
285
|
}
|
|
138
|
-
|
|
286
|
+
if (changed || createdVersion) this.internals.flush()
|
|
287
|
+
return results
|
|
139
288
|
})
|
|
140
289
|
}
|
|
141
290
|
|
|
142
291
|
/**
|
|
143
|
-
* Deletes
|
|
144
|
-
*
|
|
145
|
-
* @param keys Array of keys to delete from storage
|
|
146
|
-
* @returns Promise that resolves when the delete operation completes
|
|
147
|
-
*
|
|
148
|
-
* @throws ReferenceError if keys array is empty or undefined
|
|
149
|
-
*
|
|
150
|
-
* @remarks
|
|
151
|
-
* This method removes the specified keys from both the in-memory cache
|
|
152
|
-
* and writes the updated state to the state.json file. Keys that don't
|
|
153
|
-
* exist in storage are silently ignored.
|
|
292
|
+
* Deletes items and their versions from the local state files.
|
|
154
293
|
*
|
|
294
|
+
* @param keys The keys to delete. Empty batches are valid.
|
|
295
|
+
* @param options An optional expected storage version.
|
|
296
|
+
* @returns A result for every supplied key.
|
|
155
297
|
*/
|
|
156
|
-
delete (keys: string[])
|
|
298
|
+
async delete (keys: string[], options?: StorageDeleteOptions): Promise<StorageDeleteResults> {
|
|
157
299
|
return trace(StorageTraceDefinitions.delete, async ({ record }) => {
|
|
158
300
|
record({ keyCount: keys?.length })
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
301
|
+
validateExpectedVersion(options?.expectedVersion)
|
|
302
|
+
validateV2Keys(keys)
|
|
303
|
+
|
|
304
|
+
const results: StorageDeleteResults = {}
|
|
305
|
+
let changed = false
|
|
306
|
+
let createdVersion = false
|
|
307
|
+
for (const key of keys) {
|
|
308
|
+
if (this.internals.isExpired(key)) {
|
|
309
|
+
logger.info('Item expired, deleting from storage', { key: redactString(key, true) })
|
|
310
|
+
this.internals.remove(key)
|
|
311
|
+
changed = true
|
|
312
|
+
}
|
|
313
|
+
const current = this.internals.state[key]
|
|
314
|
+
createdVersion ||= current !== undefined && this.internals.versions[key] === undefined
|
|
315
|
+
const currentVersion = current === undefined
|
|
316
|
+
? undefined
|
|
317
|
+
: this.internals.getOrCreateVersion(key)
|
|
318
|
+
if (current === undefined) {
|
|
319
|
+
results[key] = { key, status: StorageOperationStatus.NotFound }
|
|
320
|
+
} else if (options?.expectedVersion !== undefined && options.expectedVersion !== currentVersion) {
|
|
321
|
+
results[key] = { key, status: StorageOperationStatus.ConditionNotMet, version: currentVersion }
|
|
162
322
|
} else {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
fs.writeFileSync(this._folder + '/state.json', JSON.stringify(this._stateFile, null, 2))
|
|
167
|
-
resolve(undefined)
|
|
323
|
+
this.internals.remove(key)
|
|
324
|
+
results[key] = { key, status: StorageOperationStatus.Succeeded, version: currentVersion }
|
|
325
|
+
changed = true
|
|
168
326
|
}
|
|
169
|
-
}
|
|
327
|
+
}
|
|
328
|
+
if (changed || createdVersion) this.internals.flush()
|
|
329
|
+
return results
|
|
170
330
|
})
|
|
171
331
|
}
|
|
172
332
|
}
|
|
333
|
+
|
|
334
|
+
function validateV2Keys (keys: string[]): void {
|
|
335
|
+
if (!Array.isArray(keys)) {
|
|
336
|
+
throw ExceptionHelper.generateException(ReferenceError, Errors.StorageReadKeysRequired)
|
|
337
|
+
}
|
|
338
|
+
if (keys.some(key => typeof key !== 'string' || key.trim() === '')) {
|
|
339
|
+
throw ExceptionHelper.generateException(ReferenceError, Errors.StorageV2KeyRequired)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function validateV2Changes (changes: Record<string, unknown>): void {
|
|
344
|
+
if (changes === null || typeof changes !== 'object' || Array.isArray(changes)) {
|
|
345
|
+
throw ExceptionHelper.generateException(ReferenceError, Errors.StorageWriteChangesRequired)
|
|
346
|
+
}
|
|
347
|
+
if (Object.keys(changes).some(key => key.trim() === '')) {
|
|
348
|
+
throw ExceptionHelper.generateException(ReferenceError, Errors.StorageV2KeyRequired)
|
|
349
|
+
}
|
|
350
|
+
if (Object.values(changes).some(value => value === null || typeof value !== 'object' || Array.isArray(value))) {
|
|
351
|
+
throw ExceptionHelper.generateException(TypeError, Errors.StorageV2ValueRequired)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function validateExpectedVersion (expectedVersion: string | undefined): void {
|
|
356
|
+
if (expectedVersion === '') {
|
|
357
|
+
throw ExceptionHelper.generateException(RangeError, Errors.StorageV2ExpectedVersionEmpty)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function validateWriteMode (mode: StorageWriteMode): void {
|
|
362
|
+
if (!Object.values(StorageWriteMode).includes(mode)) {
|
|
363
|
+
throw ExceptionHelper.generateException(RangeError, Errors.StorageV2WriteModeUnsupported, undefined, { mode: String(mode) })
|
|
364
|
+
}
|
|
365
|
+
}
|
package/src/storage/index.ts
CHANGED