@microsoft/agents-hosting-storage-blob 1.9.0-beta.8.gf58e66b2ee → 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 +26 -8
- package/dist/package.json +4 -4
- package/dist/src/blobsStorage.d.ts +68 -36
- package/dist/src/blobsStorage.js +286 -61
- package/dist/src/blobsStorage.js.map +1 -1
- package/dist/src/errorHelper.js +40 -0
- package/dist/src/errorHelper.js.map +1 -1
- package/package.json +4 -4
- package/src/blobsStorage.ts +303 -125
- package/src/errorHelper.ts +48 -0
package/README.md
CHANGED
|
@@ -2,15 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
This package allows to configure Azure Blob Storage as the backend for Agents conversation State
|
|
5
|
+
This package allows to configure Azure Blob Storage as the backend for Agents conversation State
|
|
6
|
+
|
|
7
|
+
`BlobsStorage` retains the legacy `Storage` contract. Use `BlobsStorageV2` for
|
|
8
|
+
the separate `StorageV2` contract; V2 calls return keyed operation results with
|
|
9
|
+
`value`, `status`, and `version`.
|
|
6
10
|
|
|
7
11
|
## Usage with connectionStrings
|
|
8
12
|
|
|
9
13
|
```ts
|
|
10
14
|
const blobStorage = new BlobsStorage(process.env.BLOB_CONTAINER_ID!, process.env.BLOB_STORAGE_CONNECTION_STRING!)
|
|
11
15
|
const conversationState = new ConversationState(blobStorage)
|
|
12
|
-
const userState = new UserState(blobStorage)
|
|
13
|
-
```
|
|
16
|
+
const userState = new UserState(blobStorage)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
const blobStorageV2 = new BlobsStorageV2(containerName, connectionString)
|
|
21
|
+
```
|
|
14
22
|
|
|
15
23
|
|
|
16
24
|
## Usage with EntraID authentication
|
|
@@ -18,9 +26,19 @@ const userState = new UserState(blobStorage)
|
|
|
18
26
|
>note: you must assign RBAC permissions to your storage account
|
|
19
27
|
|
|
20
28
|
```ts
|
|
21
|
-
const echo = new AgentApplication<TurnState>({
|
|
22
|
-
storage: new BlobsStorage('', undefined, undefined,
|
|
23
|
-
'https://agentsstate.blob.core.windows.net/nodejs-conversations',
|
|
24
|
-
new MsalTokenCredential(loadAuthConfigFromEnv()))
|
|
25
|
-
})
|
|
29
|
+
const echo = new AgentApplication<TurnState>({
|
|
30
|
+
storage: new BlobsStorage('', undefined, undefined,
|
|
31
|
+
'https://agentsstate.blob.core.windows.net/nodejs-conversations',
|
|
32
|
+
new MsalTokenCredential(loadAuthConfigFromEnv()))
|
|
33
|
+
})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## TTL writes
|
|
37
|
+
|
|
38
|
+
`BlobsStorage` supports the shared storage TTL option:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
await blobStorage.write({ 'session/123': { value: 'temporary' } }, { ttl: 3600 })
|
|
26
42
|
```
|
|
43
|
+
|
|
44
|
+
Expired blobs are omitted from reads and deleted on a best-effort basis when encountered. Physical auto-eviction still depends on Azure Blob lifecycle policies.
|
package/dist/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@microsoft/agents-hosting-storage-blob",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.1",
|
|
5
5
|
"homepage": "https://github.com/microsoft/Agents-for-js",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@azure/core-auth": "1.10.1",
|
|
20
20
|
"@azure/storage-blob": "12.31.0",
|
|
21
|
-
"@microsoft/agents-hosting": "1.9.
|
|
22
|
-
"@microsoft/agents-activity": "1.9.
|
|
23
|
-
"@microsoft/agents-telemetry": "1.9.
|
|
21
|
+
"@microsoft/agents-hosting": "1.9.1",
|
|
22
|
+
"@microsoft/agents-activity": "1.9.1",
|
|
23
|
+
"@microsoft/agents-telemetry": "1.9.1",
|
|
24
24
|
"zod": "3.25.75"
|
|
25
25
|
},
|
|
26
26
|
"license": "MIT",
|
|
@@ -1,57 +1,89 @@
|
|
|
1
1
|
import { TokenCredential } from '@azure/core-auth';
|
|
2
|
-
import { AnonymousCredential, StoragePipelineOptions, StorageSharedKeyCredential } from '@azure/storage-blob';
|
|
3
|
-
import { Storage, StoreItems } from '@microsoft/agents-hosting';
|
|
4
|
-
/**
|
|
5
|
-
* Options for configuring the BlobsStorage.
|
|
6
|
-
*/
|
|
2
|
+
import { AnonymousCredential, ContainerClient, StoragePipelineOptions, StorageSharedKeyCredential } from '@azure/storage-blob';
|
|
3
|
+
import { StorageDeleteOptions, StorageDeleteResults, StorageReadResults, StorageWriteOptions, StorageWriteResults, Storage, StorageV2, StoreItems } from '@microsoft/agents-hosting';
|
|
4
|
+
/** Options for configuring Blob storage. */
|
|
7
5
|
export interface BlobsStorageOptions {
|
|
6
|
+
/** Optional Azure Storage pipeline options to customize request behavior. */
|
|
7
|
+
storagePipelineOptions?: StoragePipelineOptions;
|
|
8
|
+
}
|
|
9
|
+
/** Shared Blob client setup and raw Blob primitives. */
|
|
10
|
+
declare class BlobsStorageInternals {
|
|
11
|
+
readonly _containerClient: ContainerClient;
|
|
12
|
+
private readonly _concurrency;
|
|
13
|
+
private _initializePromise?;
|
|
14
|
+
constructor(containerName: string, connectionString?: string, options?: BlobsStorageOptions, url?: string, credential?: StorageSharedKeyCredential | AnonymousCredential | TokenCredential);
|
|
15
|
+
_initialize(): Promise<unknown>;
|
|
16
|
+
getVersion(key: string): Promise<string | undefined>;
|
|
17
|
+
}
|
|
18
|
+
/** A legacy Azure Blob storage provider. */
|
|
19
|
+
export declare class BlobsStorage extends BlobsStorageInternals implements Storage {
|
|
20
|
+
constructor(containerName: string, connectionString?: string, options?: BlobsStorageOptions, url?: string, credential?: StorageSharedKeyCredential | AnonymousCredential | TokenCredential);
|
|
8
21
|
/**
|
|
9
|
-
*
|
|
22
|
+
* Reads legacy items from Azure Blob Storage.
|
|
23
|
+
*
|
|
24
|
+
* @param keys The keys to read.
|
|
25
|
+
* @returns Existing items keyed by storage key. Missing blobs are omitted and returned items
|
|
26
|
+
* include the Blob ETag as their legacy `eTag`.
|
|
10
27
|
*/
|
|
11
|
-
|
|
28
|
+
read(keys: string[]): Promise<StoreItems>;
|
|
29
|
+
/**
|
|
30
|
+
* Writes legacy items to Azure Blob Storage.
|
|
31
|
+
*
|
|
32
|
+
* @param changes The items to write, keyed by storage key.
|
|
33
|
+
* @throws If the input is invalid, Blob Storage cannot complete the write, or a legacy `eTag`
|
|
34
|
+
* condition fails.
|
|
35
|
+
*/
|
|
36
|
+
write(changes: StoreItems): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Deletes legacy items from Azure Blob Storage.
|
|
39
|
+
*
|
|
40
|
+
* @param keys The keys to delete. Missing blobs are ignored.
|
|
41
|
+
*/
|
|
42
|
+
delete(keys: string[]): Promise<void>;
|
|
12
43
|
}
|
|
13
44
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
45
|
+
* An Azure Blob Storage provider for the structured {@link StorageV2} contract.
|
|
46
|
+
*
|
|
47
|
+
* This is the V2 counterpart to {@link BlobsStorage} and accepts the same connection settings.
|
|
48
|
+
* It returns a result for every requested key and exposes the Azure Blob ETag as the separate
|
|
49
|
+
* storage `version`, preserving any `eTag` property in an application value. Writes support
|
|
50
|
+
* create-only, replace, and expected-version conditions to detect concurrent updates.
|
|
16
51
|
*/
|
|
17
|
-
export declare class
|
|
18
|
-
private readonly
|
|
19
|
-
private readonly _concurrency;
|
|
20
|
-
private _initializePromise?;
|
|
52
|
+
export declare class BlobsStorageV2 extends StorageV2 {
|
|
53
|
+
private readonly internals;
|
|
21
54
|
/**
|
|
22
|
-
* Creates a
|
|
55
|
+
* Creates a V2 Azure Blob Storage provider.
|
|
23
56
|
*
|
|
24
|
-
* @param containerName The name
|
|
25
|
-
* @param connectionString
|
|
26
|
-
* @param options Optional
|
|
27
|
-
* @param url
|
|
28
|
-
* @param credential
|
|
57
|
+
* @param containerName The Blob container name when using a connection string.
|
|
58
|
+
* @param connectionString The Azure Storage connection string.
|
|
59
|
+
* @param options Optional Azure Storage pipeline options.
|
|
60
|
+
* @param url The Blob service or container URL when using a credential.
|
|
61
|
+
* @param credential The credential used with `url`.
|
|
29
62
|
*/
|
|
30
63
|
constructor(containerName: string, connectionString?: string, options?: BlobsStorageOptions, url?: string, credential?: StorageSharedKeyCredential | AnonymousCredential | TokenCredential);
|
|
31
|
-
private toJSON;
|
|
32
|
-
private _initialize;
|
|
33
64
|
/**
|
|
34
|
-
* Reads
|
|
65
|
+
* Reads Blob values and ETag versions.
|
|
35
66
|
*
|
|
36
|
-
* @param keys
|
|
37
|
-
* @returns A
|
|
38
|
-
*
|
|
67
|
+
* @param keys The keys to read. Empty batches are valid.
|
|
68
|
+
* @returns A result for every key, with `notFound` for missing blobs and the Blob ETag as the
|
|
69
|
+
* version for successful reads.
|
|
39
70
|
*/
|
|
40
|
-
read(keys: string[]): Promise<
|
|
71
|
+
read<T extends object = Record<string, unknown>>(keys: string[]): Promise<StorageReadResults<T>>;
|
|
41
72
|
/**
|
|
42
|
-
* Writes
|
|
73
|
+
* Writes Blob values with optional concurrency conditions.
|
|
43
74
|
*
|
|
44
|
-
* @param changes The
|
|
45
|
-
* @
|
|
46
|
-
* @
|
|
75
|
+
* @param changes The values to write, keyed by storage key.
|
|
76
|
+
* @param options Create-only, replace, or expected-version conditions.
|
|
77
|
+
* @returns A result for every supplied key, including Blob concurrency-condition outcomes.
|
|
47
78
|
*/
|
|
48
|
-
write(changes:
|
|
79
|
+
write<T extends object = Record<string, unknown>>(changes: Record<string, T>, options?: StorageWriteOptions): Promise<StorageWriteResults>;
|
|
49
80
|
/**
|
|
50
|
-
* Deletes
|
|
81
|
+
* Deletes Blob values with an optional ETag version condition.
|
|
51
82
|
*
|
|
52
|
-
* @param keys
|
|
53
|
-
* @
|
|
54
|
-
* @
|
|
83
|
+
* @param keys The keys to delete. Empty batches are valid.
|
|
84
|
+
* @param options An optional expected Blob ETag version.
|
|
85
|
+
* @returns A result for every supplied key.
|
|
55
86
|
*/
|
|
56
|
-
delete(keys: string[]): Promise<
|
|
87
|
+
delete(keys: string[], options?: StorageDeleteOptions): Promise<StorageDeleteResults>;
|
|
57
88
|
}
|
|
89
|
+
export {};
|
package/dist/src/blobsStorage.js
CHANGED
|
@@ -36,11 +36,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.BlobsStorage = void 0;
|
|
39
|
+
exports.BlobsStorageV2 = exports.BlobsStorage = void 0;
|
|
40
40
|
const z = __importStar(require("zod"));
|
|
41
41
|
const consumers_1 = __importDefault(require("stream/consumers"));
|
|
42
42
|
const core_auth_1 = require("@azure/core-auth");
|
|
43
43
|
const storage_blob_1 = require("@azure/storage-blob");
|
|
44
|
+
const agents_hosting_1 = require("@microsoft/agents-hosting");
|
|
44
45
|
const agents_activity_1 = require("@microsoft/agents-activity");
|
|
45
46
|
const errorHelper_1 = require("./errorHelper");
|
|
46
47
|
const blobsTranscriptStore_1 = require("./blobsTranscriptStore");
|
|
@@ -48,40 +49,22 @@ const ignoreError_1 = require("./ignoreError");
|
|
|
48
49
|
const agents_telemetry_1 = require("@microsoft/agents-telemetry");
|
|
49
50
|
const observability_1 = require("./observability");
|
|
50
51
|
const logger = (0, agents_telemetry_1.debug)('agents:blob-storage');
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*/
|
|
55
|
-
class BlobsStorage {
|
|
56
|
-
/**
|
|
57
|
-
* Creates a new instance of the BlobsStorage class.
|
|
58
|
-
*
|
|
59
|
-
* @param containerName The name of the Blob container to use
|
|
60
|
-
* @param connectionString Optional, The Azure Storage connection string
|
|
61
|
-
* @param options Optional configuration settings for the storage provider
|
|
62
|
-
* @param url Optional URL to the blob service (used instead of connectionString if provided)
|
|
63
|
-
* @param credential Optional credential for authentication (used with url if provided)
|
|
64
|
-
*/
|
|
52
|
+
const expirationMetadataKey = 'agentsstorageexpiresat';
|
|
53
|
+
/** Shared Blob client setup and raw Blob primitives. */
|
|
54
|
+
class BlobsStorageInternals {
|
|
65
55
|
constructor(containerName, connectionString, options, url = '', credential) {
|
|
66
56
|
this._concurrency = Infinity;
|
|
67
57
|
if (url.trim() !== '') {
|
|
68
|
-
z.object({ url: z.string() }).parse({
|
|
69
|
-
url,
|
|
70
|
-
});
|
|
58
|
+
z.object({ url: z.string() }).parse({ url });
|
|
71
59
|
this._containerClient = new storage_blob_1.ContainerClient(url, credential, options === null || options === void 0 ? void 0 : options.storagePipelineOptions);
|
|
72
|
-
if (url.trim() === 'UseDevelopmentStorage=true;')
|
|
60
|
+
if (url.trim() === 'UseDevelopmentStorage=true;')
|
|
73
61
|
this._concurrency = 1;
|
|
74
|
-
}
|
|
75
62
|
}
|
|
76
63
|
else {
|
|
77
|
-
z.object({ connectionString: z.string(), containerName: z.string() }).parse({
|
|
78
|
-
connectionString,
|
|
79
|
-
containerName,
|
|
80
|
-
});
|
|
64
|
+
z.object({ connectionString: z.string(), containerName: z.string() }).parse({ connectionString, containerName });
|
|
81
65
|
this._containerClient = new storage_blob_1.ContainerClient(connectionString, containerName, options === null || options === void 0 ? void 0 : options.storagePipelineOptions);
|
|
82
|
-
if (connectionString.trim() === 'UseDevelopmentStorage=true;')
|
|
66
|
+
if (connectionString.trim() === 'UseDevelopmentStorage=true;')
|
|
83
67
|
this._concurrency = 1;
|
|
84
|
-
}
|
|
85
68
|
}
|
|
86
69
|
logger.info('BlobsStorage settings loaded', {
|
|
87
70
|
container: containerName,
|
|
@@ -92,21 +75,33 @@ class BlobsStorage {
|
|
|
92
75
|
pipeline: (options === null || options === void 0 ? void 0 : options.storagePipelineOptions) !== undefined ? 'custom' : 'default',
|
|
93
76
|
});
|
|
94
77
|
}
|
|
95
|
-
toJSON() {
|
|
96
|
-
return { name: 'BlobsStorage' };
|
|
97
|
-
}
|
|
98
78
|
_initialize() {
|
|
99
|
-
if (!this._initializePromise)
|
|
79
|
+
if (!this._initializePromise)
|
|
100
80
|
this._initializePromise = this._containerClient.createIfNotExists();
|
|
101
|
-
}
|
|
102
81
|
return this._initializePromise;
|
|
103
82
|
}
|
|
83
|
+
async getVersion(key) {
|
|
84
|
+
try {
|
|
85
|
+
return (await this._containerClient.getBlobClient((0, blobsTranscriptStore_1.sanitizeBlobKey)(key)).getProperties()).etag;
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
if ((0, ignoreError_1.isStatusCodeError)(404)(err))
|
|
89
|
+
return undefined;
|
|
90
|
+
throwStorageOperationError('read version', key, err);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** A legacy Azure Blob storage provider. */
|
|
95
|
+
class BlobsStorage extends BlobsStorageInternals {
|
|
96
|
+
constructor(containerName, connectionString, options, url = '', credential) {
|
|
97
|
+
super(containerName, connectionString, options, url, credential);
|
|
98
|
+
}
|
|
104
99
|
/**
|
|
105
|
-
* Reads
|
|
100
|
+
* Reads legacy items from Azure Blob Storage.
|
|
106
101
|
*
|
|
107
|
-
* @param keys
|
|
108
|
-
* @returns
|
|
109
|
-
*
|
|
102
|
+
* @param keys The keys to read.
|
|
103
|
+
* @returns Existing items keyed by storage key. Missing blobs are omitted and returned items
|
|
104
|
+
* include the Blob ETag as their legacy `eTag`.
|
|
110
105
|
*/
|
|
111
106
|
async read(keys) {
|
|
112
107
|
return (0, agents_telemetry_1.trace)(observability_1.BlobsStorageTraceDefinitions.read, async ({ record }) => {
|
|
@@ -117,60 +112,54 @@ class BlobsStorage {
|
|
|
117
112
|
const results = await Promise.all(keys.map(async (key) => {
|
|
118
113
|
const result = { key, value: undefined };
|
|
119
114
|
const blob = await (0, ignoreError_1.ignoreError)(this._containerClient.getBlobClient((0, blobsTranscriptStore_1.sanitizeBlobKey)(key)).download(), (0, ignoreError_1.isStatusCodeError)(404));
|
|
120
|
-
if (!blob)
|
|
121
|
-
return result;
|
|
122
|
-
}
|
|
123
|
-
const { etag: eTag, readableStreamBody } = blob;
|
|
124
|
-
if (!readableStreamBody) {
|
|
115
|
+
if (!(blob === null || blob === void 0 ? void 0 : blob.readableStreamBody))
|
|
125
116
|
return result;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
logger.debug(`Read blob: ${key}, eTag: ${eTag}`);
|
|
117
|
+
const parsed = await consumers_1.default.json(blob.readableStreamBody);
|
|
118
|
+
result.value = { ...parsed, eTag: blob.etag };
|
|
119
|
+
logger.debug(`Read blob: ${key}, eTag: ${blob.etag}`);
|
|
130
120
|
return result;
|
|
131
121
|
}));
|
|
132
|
-
return results.reduce((
|
|
122
|
+
return results.reduce((items, { key, value }) => value ? { ...items, [key]: value } : items, {});
|
|
133
123
|
});
|
|
134
124
|
}
|
|
135
125
|
/**
|
|
136
|
-
* Writes
|
|
126
|
+
* Writes legacy items to Azure Blob Storage.
|
|
137
127
|
*
|
|
138
|
-
* @param changes The items to write
|
|
139
|
-
* @
|
|
140
|
-
*
|
|
128
|
+
* @param changes The items to write, keyed by storage key.
|
|
129
|
+
* @throws If the input is invalid, Blob Storage cannot complete the write, or a legacy `eTag`
|
|
130
|
+
* condition fails.
|
|
141
131
|
*/
|
|
142
132
|
async write(changes) {
|
|
143
133
|
return (0, agents_telemetry_1.trace)(observability_1.BlobsStorageTraceDefinitions.write, async ({ record }) => {
|
|
134
|
+
record({ keyCount: changes ? Object.keys(changes).length : 0 });
|
|
144
135
|
z.record(z.unknown()).parse(changes);
|
|
145
|
-
record({ keyCount: Object.keys(changes).length });
|
|
146
136
|
await this._initialize();
|
|
147
137
|
await Promise.all(Object.entries(changes).map(async ([key, { eTag = '', ...change }]) => {
|
|
148
138
|
try {
|
|
149
139
|
const blob = this._containerClient.getBlockBlobClient((0, blobsTranscriptStore_1.sanitizeBlobKey)(key));
|
|
150
140
|
const serialized = JSON.stringify(change);
|
|
151
141
|
logger.debug(`Writing blob: ${key}, eTag: ${eTag}, size: ${serialized.length}`);
|
|
152
|
-
|
|
153
|
-
|
|
142
|
+
const conditions = typeof eTag === 'string' && eTag !== '*'
|
|
143
|
+
? { ifMatch: eTag }
|
|
144
|
+
: {};
|
|
145
|
+
await blob.upload(serialized, serialized.length, {
|
|
146
|
+
conditions,
|
|
154
147
|
blobHTTPHeaders: { blobContentType: 'application/json' },
|
|
155
148
|
});
|
|
156
149
|
}
|
|
157
150
|
catch (err) {
|
|
158
151
|
if (err.statusCode === 412) {
|
|
159
|
-
throw agents_activity_1.ExceptionHelper.generateException(Error, errorHelper_1.Errors.ETagConflict);
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
throw err;
|
|
152
|
+
throw agents_activity_1.ExceptionHelper.generateException(Error, errorHelper_1.Errors.ETagConflict, undefined, { key });
|
|
163
153
|
}
|
|
154
|
+
throw agents_activity_1.ExceptionHelper.generateException(Error, errorHelper_1.Errors.StorageWriteFailed, err, { key });
|
|
164
155
|
}
|
|
165
156
|
}));
|
|
166
157
|
});
|
|
167
158
|
}
|
|
168
159
|
/**
|
|
169
|
-
* Deletes
|
|
160
|
+
* Deletes legacy items from Azure Blob Storage.
|
|
170
161
|
*
|
|
171
|
-
* @param keys
|
|
172
|
-
* @returns A promise that resolves when the delete operation is complete
|
|
173
|
-
* @throws Will throw if keys parameter is invalid
|
|
162
|
+
* @param keys The keys to delete. Missing blobs are ignored.
|
|
174
163
|
*/
|
|
175
164
|
async delete(keys) {
|
|
176
165
|
return (0, agents_telemetry_1.trace)(observability_1.BlobsStorageTraceDefinitions.delete, async ({ record }) => {
|
|
@@ -178,9 +167,245 @@ class BlobsStorage {
|
|
|
178
167
|
record({ keyCount: (_a = keys === null || keys === void 0 ? void 0 : keys.length) !== null && _a !== void 0 ? _a : 0 });
|
|
179
168
|
z.object({ keys: z.array(z.string()) }).parse({ keys });
|
|
180
169
|
await this._initialize();
|
|
181
|
-
await Promise.all(keys.map(
|
|
170
|
+
await Promise.all(keys.map(key => (0, ignoreError_1.ignoreError)(this._containerClient.deleteBlob((0, blobsTranscriptStore_1.sanitizeBlobKey)(key)), (0, ignoreError_1.isStatusCodeError)(404))));
|
|
182
171
|
});
|
|
183
172
|
}
|
|
184
173
|
}
|
|
185
174
|
exports.BlobsStorage = BlobsStorage;
|
|
175
|
+
/**
|
|
176
|
+
* An Azure Blob Storage provider for the structured {@link StorageV2} contract.
|
|
177
|
+
*
|
|
178
|
+
* This is the V2 counterpart to {@link BlobsStorage} and accepts the same connection settings.
|
|
179
|
+
* It returns a result for every requested key and exposes the Azure Blob ETag as the separate
|
|
180
|
+
* storage `version`, preserving any `eTag` property in an application value. Writes support
|
|
181
|
+
* create-only, replace, and expected-version conditions to detect concurrent updates.
|
|
182
|
+
*/
|
|
183
|
+
class BlobsStorageV2 extends agents_hosting_1.StorageV2 {
|
|
184
|
+
/**
|
|
185
|
+
* Creates a V2 Azure Blob Storage provider.
|
|
186
|
+
*
|
|
187
|
+
* @param containerName The Blob container name when using a connection string.
|
|
188
|
+
* @param connectionString The Azure Storage connection string.
|
|
189
|
+
* @param options Optional Azure Storage pipeline options.
|
|
190
|
+
* @param url The Blob service or container URL when using a credential.
|
|
191
|
+
* @param credential The credential used with `url`.
|
|
192
|
+
*/
|
|
193
|
+
constructor(containerName, connectionString, options, url = '', credential) {
|
|
194
|
+
super();
|
|
195
|
+
this.internals = new BlobsStorageInternals(containerName, connectionString, options, url, credential);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Reads Blob values and ETag versions.
|
|
199
|
+
*
|
|
200
|
+
* @param keys The keys to read. Empty batches are valid.
|
|
201
|
+
* @returns A result for every key, with `notFound` for missing blobs and the Blob ETag as the
|
|
202
|
+
* version for successful reads.
|
|
203
|
+
*/
|
|
204
|
+
async read(keys) {
|
|
205
|
+
return (0, agents_telemetry_1.trace)(observability_1.BlobsStorageTraceDefinitions.read, async ({ record }) => {
|
|
206
|
+
var _a;
|
|
207
|
+
record({ keyCount: (_a = keys === null || keys === void 0 ? void 0 : keys.length) !== null && _a !== void 0 ? _a : 0 });
|
|
208
|
+
validateV2Keys(keys);
|
|
209
|
+
if (keys.length === 0)
|
|
210
|
+
return {};
|
|
211
|
+
await this.internals._initialize();
|
|
212
|
+
const results = {};
|
|
213
|
+
await Promise.all(keys.map(async (key) => {
|
|
214
|
+
try {
|
|
215
|
+
const { etag: version, metadata, readableStreamBody } = await this.internals._containerClient.getBlobClient((0, blobsTranscriptStore_1.sanitizeBlobKey)(key)).download();
|
|
216
|
+
if (!readableStreamBody) {
|
|
217
|
+
results[key] = { key, status: agents_hosting_1.StorageOperationStatus.NotFound };
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (isExpired(metadata === null || metadata === void 0 ? void 0 : metadata[expirationMetadataKey])) {
|
|
221
|
+
readableStreamBody.destroy();
|
|
222
|
+
logger.info('Blob expired, deleting from storage', {
|
|
223
|
+
key: (0, agents_telemetry_1.redactString)(key, true),
|
|
224
|
+
eTag: (0, agents_telemetry_1.redactString)(version, true),
|
|
225
|
+
expiresAt: metadata === null || metadata === void 0 ? void 0 : metadata[expirationMetadataKey],
|
|
226
|
+
});
|
|
227
|
+
await (0, ignoreError_1.ignoreError)(this.internals._containerClient.deleteBlob((0, blobsTranscriptStore_1.sanitizeBlobKey)(key), { conditions: { ifMatch: version } }), (0, ignoreError_1.isStatusCodeError)(404, 412));
|
|
228
|
+
results[key] = { key, status: agents_hosting_1.StorageOperationStatus.NotFound };
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const value = await consumers_1.default.json(readableStreamBody);
|
|
232
|
+
results[key] = { key, status: agents_hosting_1.StorageOperationStatus.Succeeded, value, version };
|
|
233
|
+
logger.debug(`Read blob: ${key}, eTag: ${version}`);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
if ((0, ignoreError_1.isStatusCodeError)(404)(err)) {
|
|
237
|
+
results[key] = { key, status: agents_hosting_1.StorageOperationStatus.NotFound };
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
throwStorageOperationError('read', key, err);
|
|
241
|
+
}
|
|
242
|
+
}));
|
|
243
|
+
return results;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Writes Blob values with optional concurrency conditions.
|
|
248
|
+
*
|
|
249
|
+
* @param changes The values to write, keyed by storage key.
|
|
250
|
+
* @param options Create-only, replace, or expected-version conditions.
|
|
251
|
+
* @returns A result for every supplied key, including Blob concurrency-condition outcomes.
|
|
252
|
+
*/
|
|
253
|
+
async write(changes, options) {
|
|
254
|
+
return (0, agents_telemetry_1.trace)(observability_1.BlobsStorageTraceDefinitions.write, async ({ record }) => {
|
|
255
|
+
var _a;
|
|
256
|
+
record({ keyCount: changes ? Object.keys(changes).length : 0 });
|
|
257
|
+
validateExpectedVersion(options === null || options === void 0 ? void 0 : options.expectedVersion);
|
|
258
|
+
validateV2Changes(changes);
|
|
259
|
+
const mode = (_a = options === null || options === void 0 ? void 0 : options.mode) !== null && _a !== void 0 ? _a : agents_hosting_1.StorageWriteMode.Upsert;
|
|
260
|
+
validateWriteMode(mode);
|
|
261
|
+
if (Object.keys(changes).length === 0)
|
|
262
|
+
return {};
|
|
263
|
+
await this.internals._initialize();
|
|
264
|
+
const results = {};
|
|
265
|
+
const expiresAt = (0, agents_hosting_1.getStorageWriteExpiry)(options);
|
|
266
|
+
await Promise.all(Object.entries(changes).map(async ([key, change]) => {
|
|
267
|
+
const blob = this.internals._containerClient.getBlockBlobClient((0, blobsTranscriptStore_1.sanitizeBlobKey)(key));
|
|
268
|
+
const needsCurrentVersion = mode !== agents_hosting_1.StorageWriteMode.Upsert || (options === null || options === void 0 ? void 0 : options.expectedVersion) !== undefined;
|
|
269
|
+
const currentVersion = needsCurrentVersion
|
|
270
|
+
? await this.internals.getVersion(key)
|
|
271
|
+
: undefined;
|
|
272
|
+
if (mode === agents_hosting_1.StorageWriteMode.CreateOnly && currentVersion !== undefined) {
|
|
273
|
+
results[key] = { key, status: agents_hosting_1.StorageOperationStatus.Conflict, version: currentVersion };
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (mode === agents_hosting_1.StorageWriteMode.Replace && currentVersion === undefined) {
|
|
277
|
+
results[key] = { key, status: agents_hosting_1.StorageOperationStatus.NotFound };
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if ((options === null || options === void 0 ? void 0 : options.expectedVersion) !== undefined && options.expectedVersion !== currentVersion) {
|
|
281
|
+
results[key] = { key, status: agents_hosting_1.StorageOperationStatus.ConditionNotMet, version: currentVersion };
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const serialized = JSON.stringify(change);
|
|
285
|
+
const conditions = mode === agents_hosting_1.StorageWriteMode.CreateOnly
|
|
286
|
+
? { ifNoneMatch: '*' }
|
|
287
|
+
: (options === null || options === void 0 ? void 0 : options.expectedVersion) !== undefined
|
|
288
|
+
? { ifMatch: options.expectedVersion }
|
|
289
|
+
: mode === agents_hosting_1.StorageWriteMode.Replace && currentVersion !== undefined
|
|
290
|
+
? { ifMatch: currentVersion }
|
|
291
|
+
: undefined;
|
|
292
|
+
try {
|
|
293
|
+
const response = await blob.upload(serialized, serialized.length, {
|
|
294
|
+
conditions,
|
|
295
|
+
blobHTTPHeaders: { blobContentType: 'application/json' },
|
|
296
|
+
metadata: expiresAt === undefined ? {} : { [expirationMetadataKey]: expiresAt.toString() },
|
|
297
|
+
});
|
|
298
|
+
results[key] = { key, status: agents_hosting_1.StorageOperationStatus.Succeeded, version: response.etag };
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
const statusCode = err.statusCode;
|
|
302
|
+
if (mode === agents_hosting_1.StorageWriteMode.CreateOnly && (statusCode === 409 || statusCode === 412)) {
|
|
303
|
+
results[key] = { key, status: agents_hosting_1.StorageOperationStatus.Conflict, version: currentVersion };
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (statusCode === 412) {
|
|
307
|
+
results[key] = { key, status: agents_hosting_1.StorageOperationStatus.ConditionNotMet, version: currentVersion };
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (statusCode === 404) {
|
|
311
|
+
results[key] = { key, status: agents_hosting_1.StorageOperationStatus.NotFound };
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
throwStorageOperationError('write', key, err);
|
|
315
|
+
}
|
|
316
|
+
}));
|
|
317
|
+
return results;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Deletes Blob values with an optional ETag version condition.
|
|
322
|
+
*
|
|
323
|
+
* @param keys The keys to delete. Empty batches are valid.
|
|
324
|
+
* @param options An optional expected Blob ETag version.
|
|
325
|
+
* @returns A result for every supplied key.
|
|
326
|
+
*/
|
|
327
|
+
async delete(keys, options) {
|
|
328
|
+
return (0, agents_telemetry_1.trace)(observability_1.BlobsStorageTraceDefinitions.delete, async ({ record }) => {
|
|
329
|
+
var _a;
|
|
330
|
+
record({ keyCount: (_a = keys === null || keys === void 0 ? void 0 : keys.length) !== null && _a !== void 0 ? _a : 0 });
|
|
331
|
+
validateExpectedVersion(options === null || options === void 0 ? void 0 : options.expectedVersion);
|
|
332
|
+
validateV2Keys(keys);
|
|
333
|
+
if (keys.length === 0)
|
|
334
|
+
return {};
|
|
335
|
+
await this.internals._initialize();
|
|
336
|
+
const results = {};
|
|
337
|
+
await Promise.all(keys.map(async (key) => {
|
|
338
|
+
let currentVersion;
|
|
339
|
+
if ((options === null || options === void 0 ? void 0 : options.expectedVersion) !== undefined) {
|
|
340
|
+
currentVersion = await this.internals.getVersion(key);
|
|
341
|
+
if (currentVersion === undefined) {
|
|
342
|
+
results[key] = { key, status: agents_hosting_1.StorageOperationStatus.NotFound };
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (options.expectedVersion !== currentVersion) {
|
|
346
|
+
results[key] = { key, status: agents_hosting_1.StorageOperationStatus.ConditionNotMet, version: currentVersion };
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
const deleteOptions = (options === null || options === void 0 ? void 0 : options.expectedVersion) === undefined
|
|
352
|
+
? undefined
|
|
353
|
+
: { conditions: { ifMatch: options.expectedVersion } };
|
|
354
|
+
await this.internals._containerClient.deleteBlob((0, blobsTranscriptStore_1.sanitizeBlobKey)(key), deleteOptions);
|
|
355
|
+
results[key] = { key, status: agents_hosting_1.StorageOperationStatus.Succeeded, version: currentVersion };
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
const statusCode = err.statusCode;
|
|
359
|
+
if (statusCode === 412) {
|
|
360
|
+
results[key] = { key, status: agents_hosting_1.StorageOperationStatus.ConditionNotMet, version: currentVersion };
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (statusCode === 404) {
|
|
364
|
+
results[key] = { key, status: agents_hosting_1.StorageOperationStatus.NotFound };
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
throwStorageOperationError('delete', key, err);
|
|
368
|
+
}
|
|
369
|
+
}));
|
|
370
|
+
return results;
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
exports.BlobsStorageV2 = BlobsStorageV2;
|
|
375
|
+
function validateExpectedVersion(expectedVersion) {
|
|
376
|
+
if (expectedVersion === '')
|
|
377
|
+
throw agents_activity_1.ExceptionHelper.generateException(RangeError, errorHelper_1.Errors.StorageV2ExpectedVersionEmpty);
|
|
378
|
+
}
|
|
379
|
+
function validateWriteMode(mode) {
|
|
380
|
+
if (!Object.values(agents_hosting_1.StorageWriteMode).includes(mode)) {
|
|
381
|
+
throw agents_activity_1.ExceptionHelper.generateException(RangeError, errorHelper_1.Errors.StorageV2WriteModeUnsupported, undefined, { mode: String(mode) });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function validateV2Changes(changes) {
|
|
385
|
+
if (changes === null || typeof changes !== 'object' || Array.isArray(changes)) {
|
|
386
|
+
throw agents_activity_1.ExceptionHelper.generateException(ReferenceError, errorHelper_1.Errors.StorageV2ChangesRequired);
|
|
387
|
+
}
|
|
388
|
+
if (Object.keys(changes).some(key => key.trim() === '')) {
|
|
389
|
+
throw agents_activity_1.ExceptionHelper.generateException(ReferenceError, errorHelper_1.Errors.StorageV2KeyRequired);
|
|
390
|
+
}
|
|
391
|
+
if (Object.values(changes).some(value => value === null || typeof value !== 'object' || Array.isArray(value))) {
|
|
392
|
+
throw agents_activity_1.ExceptionHelper.generateException(TypeError, errorHelper_1.Errors.StorageV2ValueRequired);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function validateV2Keys(keys) {
|
|
396
|
+
if (!Array.isArray(keys))
|
|
397
|
+
throw agents_activity_1.ExceptionHelper.generateException(ReferenceError, errorHelper_1.Errors.StorageV2KeysRequired);
|
|
398
|
+
if (keys.some(key => typeof key !== 'string' || key.trim() === '')) {
|
|
399
|
+
throw agents_activity_1.ExceptionHelper.generateException(ReferenceError, errorHelper_1.Errors.StorageV2KeyRequired);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function throwStorageOperationError(operation, key, error) {
|
|
403
|
+
throw agents_activity_1.ExceptionHelper.generateException(Error, errorHelper_1.Errors.StorageV2OperationFailed, error instanceof Error ? error : undefined, { operation, key });
|
|
404
|
+
}
|
|
405
|
+
function isExpired(expiresAt) {
|
|
406
|
+
if (expiresAt === undefined)
|
|
407
|
+
return false;
|
|
408
|
+
const timestamp = Number(expiresAt);
|
|
409
|
+
return Number.isFinite(timestamp) && timestamp <= Date.now();
|
|
410
|
+
}
|
|
186
411
|
//# sourceMappingURL=blobsStorage.js.map
|