@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.
Files changed (150) hide show
  1. package/README.md +182 -4
  2. package/dist/package.json +3 -3
  3. package/dist/src/app/agentApplication.d.ts +4 -3
  4. package/dist/src/app/agentApplication.js +24 -21
  5. package/dist/src/app/agentApplication.js.map +1 -1
  6. package/dist/src/app/agentApplicationBuilder.d.ts +9 -2
  7. package/dist/src/app/agentApplicationBuilder.js +9 -0
  8. package/dist/src/app/agentApplicationBuilder.js.map +1 -1
  9. package/dist/src/app/agentApplicationOptions.d.ts +16 -4
  10. package/dist/src/app/auth/authorization.d.ts +54 -0
  11. package/dist/src/app/auth/authorization.js +63 -2
  12. package/dist/src/app/auth/authorization.js.map +1 -1
  13. package/dist/src/app/auth/authorizationManager.d.ts +0 -8
  14. package/dist/src/app/auth/authorizationManager.js +104 -178
  15. package/dist/src/app/auth/authorizationManager.js.map +1 -1
  16. package/dist/src/app/auth/handlerStorage.d.ts +5 -3
  17. package/dist/src/app/auth/handlerStorage.js +36 -10
  18. package/dist/src/app/auth/handlerStorage.js.map +1 -1
  19. package/dist/src/app/auth/types.d.ts +2 -2
  20. package/dist/src/app/index.d.ts +1 -0
  21. package/dist/src/app/proactive/proactive.js +10 -6
  22. package/dist/src/app/proactive/proactive.js.map +1 -1
  23. package/dist/src/app/proactive/proactiveOptions.d.ts +2 -2
  24. package/dist/src/app/rateLimit.d.ts +315 -0
  25. package/dist/src/app/rateLimit.js +333 -0
  26. package/dist/src/app/rateLimit.js.map +1 -0
  27. package/dist/src/app/turnState.d.ts +6 -3
  28. package/dist/src/app/turnState.js +43 -4
  29. package/dist/src/app/turnState.js.map +1 -1
  30. package/dist/src/auth/authConfiguration.d.ts +10 -3
  31. package/dist/src/auth/authConfiguration.js +292 -259
  32. package/dist/src/auth/authConfiguration.js.map +1 -1
  33. package/dist/src/auth/delegatedTokenCredential.d.ts +29 -0
  34. package/dist/src/auth/delegatedTokenCredential.js +49 -0
  35. package/dist/src/auth/delegatedTokenCredential.js.map +1 -0
  36. package/dist/src/auth/msal/msalTokenCredential.js +1 -1
  37. package/dist/src/auth/msal/msalTokenCredential.js.map +1 -1
  38. package/dist/src/auth/settings.d.ts +8 -70
  39. package/dist/src/auth/settings.js +5 -67
  40. package/dist/src/auth/settings.js.map +1 -1
  41. package/dist/src/cloudAdapter.d.ts +5 -0
  42. package/dist/src/cloudAdapter.js +13 -103
  43. package/dist/src/cloudAdapter.js.map +1 -1
  44. package/dist/src/configuration/botFrameworkEnvironmentCompatibility.d.ts +23 -0
  45. package/dist/src/configuration/botFrameworkEnvironmentCompatibility.js +183 -0
  46. package/dist/src/configuration/botFrameworkEnvironmentCompatibility.js.map +1 -0
  47. package/dist/src/configuration/configuration.d.ts +83 -0
  48. package/dist/src/configuration/configuration.js +836 -0
  49. package/dist/src/configuration/configuration.js.map +1 -0
  50. package/dist/src/configuration/configurationSource.d.ts +57 -0
  51. package/dist/src/configuration/configurationSource.js +7 -0
  52. package/dist/src/configuration/configurationSource.js.map +1 -0
  53. package/dist/src/configuration/environmentConfiguration.d.ts +14 -0
  54. package/dist/src/configuration/environmentConfiguration.js +271 -0
  55. package/dist/src/configuration/environmentConfiguration.js.map +1 -0
  56. package/dist/src/configuration/index.d.ts +7 -0
  57. package/dist/src/configuration/index.js +14 -0
  58. package/dist/src/configuration/index.js.map +1 -0
  59. package/dist/src/configuration/jsonFileConfigurationSource.d.ts +15 -0
  60. package/dist/src/configuration/jsonFileConfigurationSource.js +58 -0
  61. package/dist/src/configuration/jsonFileConfigurationSource.js.map +1 -0
  62. package/dist/src/createCloudAdapter.d.ts +30 -5
  63. package/dist/src/createCloudAdapter.js +17 -10
  64. package/dist/src/createCloudAdapter.js.map +1 -1
  65. package/dist/src/errorHelper.js +124 -5
  66. package/dist/src/errorHelper.js.map +1 -1
  67. package/dist/src/index.d.ts +1 -0
  68. package/dist/src/index.js +1 -0
  69. package/dist/src/index.js.map +1 -1
  70. package/dist/src/observability/traces.d.ts +6 -4
  71. package/dist/src/observability/traces.js +10 -9
  72. package/dist/src/observability/traces.js.map +1 -1
  73. package/dist/src/outboundHostValidator.d.ts +4 -1
  74. package/dist/src/outboundHostValidator.js +25 -48
  75. package/dist/src/outboundHostValidator.js.map +1 -1
  76. package/dist/src/state/agentState.d.ts +8 -3
  77. package/dist/src/state/agentState.js +26 -8
  78. package/dist/src/state/agentState.js.map +1 -1
  79. package/dist/src/state/conversationState.d.ts +2 -2
  80. package/dist/src/state/conversationState.js.map +1 -1
  81. package/dist/src/state/userState.d.ts +2 -2
  82. package/dist/src/state/userState.js.map +1 -1
  83. package/dist/src/statusCodes.d.ts +5 -0
  84. package/dist/src/statusCodes.js +5 -0
  85. package/dist/src/statusCodes.js.map +1 -1
  86. package/dist/src/storage/fileStorage.d.ts +83 -68
  87. package/dist/src/storage/fileStorage.js +279 -105
  88. package/dist/src/storage/fileStorage.js.map +1 -1
  89. package/dist/src/storage/index.d.ts +1 -0
  90. package/dist/src/storage/index.js +1 -0
  91. package/dist/src/storage/index.js.map +1 -1
  92. package/dist/src/storage/memoryStorage.d.ts +85 -60
  93. package/dist/src/storage/memoryStorage.js +244 -87
  94. package/dist/src/storage/memoryStorage.js.map +1 -1
  95. package/dist/src/storage/storage.d.ts +100 -2
  96. package/dist/src/storage/storage.js +25 -0
  97. package/dist/src/storage/storage.js.map +1 -1
  98. package/dist/src/storage/storageCompatibility.d.ts +17 -0
  99. package/dist/src/storage/storageCompatibility.js +157 -0
  100. package/dist/src/storage/storageCompatibility.js.map +1 -0
  101. package/dist/src/storage/storageExpiry.d.ts +6 -0
  102. package/dist/src/storage/storageExpiry.js +20 -0
  103. package/dist/src/storage/storageExpiry.js.map +1 -0
  104. package/dist/src/utils/env.d.ts +43 -0
  105. package/dist/src/utils/env.js +42 -6
  106. package/dist/src/utils/env.js.map +1 -1
  107. package/dist/src/utils.d.ts +1 -0
  108. package/dist/src/utils.js +10 -1
  109. package/dist/src/utils.js.map +1 -1
  110. package/package.json +3 -3
  111. package/src/app/agentApplication.ts +27 -14
  112. package/src/app/agentApplicationBuilder.ts +13 -2
  113. package/src/app/agentApplicationOptions.ts +18 -4
  114. package/src/app/auth/authorization.ts +67 -2
  115. package/src/app/auth/authorizationManager.ts +126 -204
  116. package/src/app/auth/handlerStorage.ts +51 -7
  117. package/src/app/auth/types.ts +2 -2
  118. package/src/app/index.ts +7 -0
  119. package/src/app/proactive/proactive.ts +19 -9
  120. package/src/app/proactive/proactiveOptions.ts +2 -2
  121. package/src/app/rateLimit.ts +659 -0
  122. package/src/app/turnState.ts +50 -7
  123. package/src/auth/authConfiguration.ts +415 -260
  124. package/src/auth/delegatedTokenCredential.ts +49 -0
  125. package/src/auth/msal/msalTokenCredential.ts +1 -1
  126. package/src/auth/settings.ts +5 -82
  127. package/src/cloudAdapter.ts +38 -113
  128. package/src/configuration/botFrameworkEnvironmentCompatibility.ts +245 -0
  129. package/src/configuration/configuration.ts +1076 -0
  130. package/src/configuration/configurationSource.ts +76 -0
  131. package/src/configuration/environmentConfiguration.ts +377 -0
  132. package/src/configuration/index.ts +19 -0
  133. package/src/configuration/jsonFileConfigurationSource.ts +74 -0
  134. package/src/createCloudAdapter.ts +52 -9
  135. package/src/errorHelper.ts +151 -5
  136. package/src/index.ts +1 -0
  137. package/src/observability/traces.ts +8 -7
  138. package/src/outboundHostValidator.ts +37 -44
  139. package/src/state/agentState.ts +39 -13
  140. package/src/state/conversationState.ts +2 -2
  141. package/src/state/userState.ts +2 -2
  142. package/src/statusCodes.ts +6 -0
  143. package/src/storage/fileStorage.ts +299 -106
  144. package/src/storage/index.ts +1 -0
  145. package/src/storage/memoryStorage.ts +268 -85
  146. package/src/storage/storage.ts +114 -2
  147. package/src/storage/storageCompatibility.ts +185 -0
  148. package/src/storage/storageExpiry.ts +21 -0
  149. package/src/utils/env.ts +63 -0
  150. package/src/utils.ts +10 -0
@@ -3,170 +3,363 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import path from 'path'
7
- import fs from 'fs'
8
- import { Storage, StoreItem } from './storage'
9
- import { trace } from '@microsoft/agents-telemetry'
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 'state.json' within a specified folder.
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
- * The storage format is a simple key-value JSON object where keys are strings and values
21
- * can be any JSON-serializable data. All operations are synchronous file I/O operations
22
- * wrapped in Promise interfaces to match the Storage contract.
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, consider using
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 storage = new FileStorage('./data');
107
+ * const legacyStorage = new FileStorage('./data')
108
+ * const storageV2 = new FileStorageV2('./data-v2')
37
109
  *
38
- * // Write some data
39
- * await storage.write({
40
- * 'user123': { name: 'John', lastSeen: new Date().toISOString() },
41
- * 'conversation456': { turn: 5, context: 'discussing weather' }
42
- * });
110
+ * await storageV2.write({
111
+ * user123: { name: 'John', lastSeen: new Date().toISOString() }
112
+ * })
43
113
  *
44
- * // Read specific keys
45
- * const data = await storage.read(['user123']);
46
- * console.log(data.user123); // { name: 'John', lastSeen: '...' }
114
+ * const result = await storageV2.read(['user123'])
115
+ * console.log(result.user123.value)
47
116
  *
48
- * // Delete data
49
- * await storage.delete(['conversation456']);
117
+ * await storageV2.delete(['user123'])
50
118
  * ```
51
- *
52
119
  */
53
- export class FileStorage implements Storage {
54
- private _folder: string
55
- private _stateFile: Record<string, string>
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
- * Creates a new FileStorage instance that stores data in the specified folder.
141
+ * Writes legacy items to `state.json`.
59
142
  *
60
- * @param folder The absolute or relative path to the folder where the state.json file will be stored
61
- * @throws May throw filesystem errors if the folder cannot be created or accessed
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
- * @remarks
64
- * The constructor performs the following initialization steps:
65
- * 1. Creates the target folder if it doesn't exist (including parent directories)
66
- * 2. Creates an empty state.json file if it doesn't exist
67
- * 3. Loads existing data from state.json into memory for fast access
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
- this._folder = folder
72
- if (!fs.existsSync(folder)) {
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 store items from the filesystem storage.
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[]) : Promise<StoreItem> {
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
- return new Promise<StoreItem>((resolve, reject) => {
99
- if (!keys || keys.length === 0) {
100
- reject(new ReferenceError('Keys are required when reading.'))
101
- } else {
102
- const data: StoreItem = {}
103
- for (const key of keys) {
104
- const item = this._stateFile[key]
105
- if (item) {
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
- resolve(data)
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 store items to the filesystem storage.
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: StoreItem) : Promise<void> {
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
- const keys = Object.keys(changes)
134
- record({ keyCount: keys?.length })
135
- for (const key of keys) {
136
- this._stateFile[key] = changes[key]
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
- fs.writeFileSync(this._folder + '/state.json', JSON.stringify(this._stateFile, null, 2))
286
+ if (changed || createdVersion) this.internals.flush()
287
+ return results
139
288
  })
140
289
  }
141
290
 
142
291
  /**
143
- * Deletes store items from the filesystem storage.
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[]) : Promise<void> {
298
+ async delete (keys: string[], options?: StorageDeleteOptions): Promise<StorageDeleteResults> {
157
299
  return trace(StorageTraceDefinitions.delete, async ({ record }) => {
158
300
  record({ keyCount: keys?.length })
159
- return new Promise<void>((resolve, reject) => {
160
- if (!keys || keys.length === 0) {
161
- reject(new ReferenceError('Keys are required when deleting.'))
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
- for (const key of keys) {
164
- delete this._stateFile[key]
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
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './storage'
2
+ export * from './storageExpiry'
2
3
  export * from './memoryStorage'
3
4
  export * from './fileStorage'