@cfworker/cosmos 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +6 -3
- package/dist/client.js +21 -35
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/response.d.ts +2 -1
- package/dist/response.js +0 -3
- package/dist/retry.d.ts +1 -0
- package/dist/retry.js +4 -3
- package/dist/session.d.ts +25 -0
- package/dist/session.js +48 -0
- package/dist/signer.d.ts +1 -0
- package/dist/signer.js +1 -2
- package/package.json +10 -11
- package/src/client.ts +26 -35
- package/src/index.ts +1 -0
- package/src/response.ts +1 -1
- package/src/retry.ts +5 -0
- package/src/session.ts +74 -0
- package/tsconfig.json +8 -3
- package/LICENSE.md +0 -21
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
/// <reference types="@cfworker/worker-types" />
|
|
1
3
|
import { FeedResponse, ItemResponse } from './response.js';
|
|
2
4
|
import { RetryPolicy } from './retry.js';
|
|
5
|
+
import { SessionContainer } from './session.js';
|
|
3
6
|
import { Collection, ConsistencyLevel, Database, Document, IndexingDirective, IndexingPolicy, OfferType, PartitionKeyDefinition, Resource } from './types.js';
|
|
4
7
|
export interface CosmosClientConfig {
|
|
5
8
|
endpoint: string;
|
|
@@ -8,7 +11,7 @@ export interface CosmosClientConfig {
|
|
|
8
11
|
consistencyLevel?: ConsistencyLevel;
|
|
9
12
|
dbId?: string;
|
|
10
13
|
collId?: string;
|
|
11
|
-
|
|
14
|
+
sessions?: SessionContainer;
|
|
12
15
|
fetch?: typeof fetch;
|
|
13
16
|
}
|
|
14
17
|
export declare class CosmosClient {
|
|
@@ -19,7 +22,7 @@ export declare class CosmosClient {
|
|
|
19
22
|
private readonly dbId;
|
|
20
23
|
private readonly collId;
|
|
21
24
|
private readonly systemFetch;
|
|
22
|
-
|
|
25
|
+
readonly sessions: SessionContainer;
|
|
23
26
|
requestCharges: number;
|
|
24
27
|
retries: {
|
|
25
28
|
count: number;
|
|
@@ -44,7 +47,7 @@ export declare class CosmosClient {
|
|
|
44
47
|
private fetch;
|
|
45
48
|
private setHeaders;
|
|
46
49
|
}
|
|
47
|
-
export declare type DocumentInit = BufferSource | ReadableStream
|
|
50
|
+
export declare type DocumentInit = BufferSource | ReadableStream | string | Resource;
|
|
48
51
|
interface CommonArgs {
|
|
49
52
|
activityId?: string;
|
|
50
53
|
consistencyLevel?: ConsistencyLevel;
|
package/dist/client.js
CHANGED
|
@@ -1,27 +1,20 @@
|
|
|
1
1
|
import { FeedResponse, ItemResponse } from './response.js';
|
|
2
2
|
import { defaultRetryPolicy } from './retry.js';
|
|
3
|
+
import { DefaultSessionContainer } from './session.js';
|
|
3
4
|
import { getSigner } from './signer.js';
|
|
4
5
|
import { assertArg, escapeNonASCII, uri } from './util.js';
|
|
5
6
|
export class CosmosClient {
|
|
6
|
-
endpoint;
|
|
7
|
-
signer;
|
|
8
|
-
retryPolicy;
|
|
9
|
-
consistencyLevel;
|
|
10
|
-
dbId;
|
|
11
|
-
collId;
|
|
12
|
-
systemFetch;
|
|
13
|
-
sessionToken = '';
|
|
14
|
-
requestCharges = 0;
|
|
15
|
-
retries = { count: 0, delayMs: 0 };
|
|
16
7
|
constructor(config) {
|
|
8
|
+
this.requestCharges = 0;
|
|
9
|
+
this.retries = { count: 0, delayMs: 0 };
|
|
17
10
|
this.endpoint = config.endpoint;
|
|
18
11
|
this.signer = getSigner(config.masterKey);
|
|
19
|
-
this.retryPolicy = config.retryPolicy
|
|
20
|
-
this.consistencyLevel = config.consistencyLevel
|
|
12
|
+
this.retryPolicy = config.retryPolicy ?? defaultRetryPolicy;
|
|
13
|
+
this.consistencyLevel = config.consistencyLevel ?? 'Session';
|
|
21
14
|
this.dbId = config.dbId;
|
|
22
15
|
this.collId = config.collId;
|
|
23
|
-
this.
|
|
24
|
-
this.
|
|
16
|
+
this.sessions = config.sessions ?? new DefaultSessionContainer();
|
|
17
|
+
this.systemFetch = config.fetch ?? fetch.bind(self);
|
|
25
18
|
}
|
|
26
19
|
async getDatabases(args = {}) {
|
|
27
20
|
const url = this.endpoint + `/dbs`;
|
|
@@ -178,34 +171,30 @@ export class CosmosClient {
|
|
|
178
171
|
return fn.call(this, Object.assign({}, args, { continuation }));
|
|
179
172
|
};
|
|
180
173
|
}
|
|
181
|
-
async fetchWithRetry(request) {
|
|
174
|
+
async fetchWithRetry(request, context) {
|
|
182
175
|
const retryRequest = request.clone();
|
|
183
176
|
const response = await this.fetch(request);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
};
|
|
190
|
-
retryContext.attempts++;
|
|
191
|
-
retryContext.request = retryRequest;
|
|
192
|
-
retryContext.response = response;
|
|
193
|
-
const { retry, delayMs } = await this.retryPolicy.shouldRetry(retryContext);
|
|
177
|
+
context ??= { attempts: 0, cumulativeWaitMs: 0, request, response };
|
|
178
|
+
context.attempts++;
|
|
179
|
+
context.request = retryRequest;
|
|
180
|
+
context.response = response;
|
|
181
|
+
const { retry, delayMs } = await this.retryPolicy.shouldRetry(context);
|
|
194
182
|
if (!retry) {
|
|
195
183
|
return response;
|
|
196
184
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
185
|
+
if (delayMs !== 0) {
|
|
186
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
187
|
+
context.cumulativeWaitMs += delayMs;
|
|
188
|
+
this.retries.delayMs += delayMs;
|
|
189
|
+
}
|
|
200
190
|
this.retries.count++;
|
|
201
|
-
this.
|
|
202
|
-
return this.fetchWithRetry(retryContext.request);
|
|
191
|
+
return this.fetchWithRetry(retryRequest, context);
|
|
203
192
|
}
|
|
204
193
|
async fetch(request) {
|
|
194
|
+
this.sessions.setRequestSession(request);
|
|
205
195
|
await this.signer.sign(request);
|
|
206
196
|
const response = await this.systemFetch(request);
|
|
207
|
-
this.
|
|
208
|
-
response.headers.get('x-ms-session-token') || this.sessionToken;
|
|
197
|
+
this.sessions.readResponseSession(response);
|
|
209
198
|
const requestCharge = +(response.headers.get('x-ms-request-charge') || 0);
|
|
210
199
|
this.requestCharges += requestCharge;
|
|
211
200
|
return response;
|
|
@@ -219,9 +208,6 @@ export class CosmosClient {
|
|
|
219
208
|
}
|
|
220
209
|
const consistencyLevel = args.consistencyLevel || this.consistencyLevel;
|
|
221
210
|
headers.set('x-ms-consistency-level', consistencyLevel);
|
|
222
|
-
if (consistencyLevel === 'Session' && this.sessionToken) {
|
|
223
|
-
headers.set('x-ms-session-token', this.sessionToken);
|
|
224
|
-
}
|
|
225
211
|
if (args.continuation) {
|
|
226
212
|
headers.set('x-ms-continuation', args.continuation);
|
|
227
213
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/response.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
1
2
|
import { PersistedResource } from './types.js';
|
|
2
3
|
export declare abstract class CosmosResponse {
|
|
3
4
|
protected readonly response: Response;
|
|
4
5
|
constructor(response: Response);
|
|
5
6
|
get status(): number;
|
|
6
7
|
get headers(): Headers;
|
|
7
|
-
get body(): ReadableStream
|
|
8
|
+
get body(): ReadableStream | null;
|
|
8
9
|
get activityId(): string | null;
|
|
9
10
|
get etag(): string;
|
|
10
11
|
get requestCharge(): number;
|
package/dist/response.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export class CosmosResponse {
|
|
2
|
-
response;
|
|
3
2
|
constructor(response) {
|
|
4
3
|
this.response = response;
|
|
5
4
|
}
|
|
@@ -34,8 +33,6 @@ export class ItemResponse extends CosmosResponse {
|
|
|
34
33
|
}
|
|
35
34
|
}
|
|
36
35
|
export class FeedResponse extends CosmosResponse {
|
|
37
|
-
next;
|
|
38
|
-
itemsProperty;
|
|
39
36
|
constructor(response, next, itemsProperty) {
|
|
40
37
|
super(response);
|
|
41
38
|
this.next = next;
|
package/dist/retry.d.ts
CHANGED
package/dist/retry.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { readSessionNotAvailable } from './session';
|
|
1
2
|
export class DefaultRetryPolicy {
|
|
2
|
-
maxAttempts;
|
|
3
|
-
maxCumulativeWaitTimeMs;
|
|
4
|
-
defaultRetryDelayMs;
|
|
5
3
|
constructor(maxAttempts = 10, maxCumulativeWaitTimeMs = 30000, defaultRetryDelayMs = 5000) {
|
|
6
4
|
this.maxAttempts = maxAttempts;
|
|
7
5
|
this.maxCumulativeWaitTimeMs = maxCumulativeWaitTimeMs;
|
|
8
6
|
this.defaultRetryDelayMs = defaultRetryDelayMs;
|
|
9
7
|
}
|
|
10
8
|
async shouldRetry(context) {
|
|
9
|
+
if (readSessionNotAvailable(context.response)) {
|
|
10
|
+
return { retry: true, delayMs: 0 };
|
|
11
|
+
}
|
|
11
12
|
if (context.response.status !== 429 ||
|
|
12
13
|
context.attempts >= this.maxAttempts ||
|
|
13
14
|
context.cumulativeWaitMs >= this.maxCumulativeWaitTimeMs) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
export declare function readSessionNotAvailable(response: Response): boolean;
|
|
3
|
+
export declare function getCollectionId({ url, headers }: {
|
|
4
|
+
url: string;
|
|
5
|
+
headers: Headers;
|
|
6
|
+
}): {
|
|
7
|
+
dbId: string;
|
|
8
|
+
collId: string;
|
|
9
|
+
} | null;
|
|
10
|
+
export interface SessionTokens {
|
|
11
|
+
[dbId: string]: {
|
|
12
|
+
[collId: string]: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface SessionContainer {
|
|
16
|
+
tokens: SessionTokens;
|
|
17
|
+
setRequestSession(request: Request): void;
|
|
18
|
+
readResponseSession(response: Response): void;
|
|
19
|
+
}
|
|
20
|
+
export declare class DefaultSessionContainer implements SessionContainer {
|
|
21
|
+
readonly tokens: SessionTokens;
|
|
22
|
+
constructor(tokens?: SessionTokens);
|
|
23
|
+
setRequestSession(request: Request): void;
|
|
24
|
+
readResponseSession(response: Response): void;
|
|
25
|
+
}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const readSessionNotAvailableSubstatus = '1002';
|
|
2
|
+
export function readSessionNotAvailable(response) {
|
|
3
|
+
return (response.status === 404 &&
|
|
4
|
+
response.headers.get('x-ms-substatus') === readSessionNotAvailableSubstatus);
|
|
5
|
+
}
|
|
6
|
+
export function getCollectionId({ url, headers }) {
|
|
7
|
+
const path = headers.get('x-ms-alt-content-path') ?? new URL(url).pathname.substr(1);
|
|
8
|
+
const [, dbId, colls, collId] = path.split('/');
|
|
9
|
+
if (colls !== 'colls' || !collId) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return { dbId, collId };
|
|
13
|
+
}
|
|
14
|
+
export class DefaultSessionContainer {
|
|
15
|
+
constructor(tokens = {}) {
|
|
16
|
+
this.tokens = tokens;
|
|
17
|
+
}
|
|
18
|
+
setRequestSession(request) {
|
|
19
|
+
if (request.headers.get('x-ms-consistency-level') !== 'Session') {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const ids = getCollectionId(request);
|
|
23
|
+
if (!ids) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const token = this.tokens?.[ids.dbId]?.[ids.collId];
|
|
27
|
+
if (token) {
|
|
28
|
+
request.headers.set('x-ms-session-token', token);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
readResponseSession(response) {
|
|
32
|
+
const ids = getCollectionId(response);
|
|
33
|
+
if (!ids) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (readSessionNotAvailable(response)) {
|
|
37
|
+
if (this.tokens[ids.dbId]) {
|
|
38
|
+
delete this.tokens[ids.dbId][ids.collId];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const token = response.headers.get('x-ms-session-token');
|
|
42
|
+
if (!token) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
this.tokens[ids.dbId] ??= {};
|
|
46
|
+
this.tokens[ids.dbId][ids.collId] = token;
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/signer.d.ts
CHANGED
package/dist/signer.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { decode as base64ToArrayBuffer, encode as arrayBufferToBase64 } from 'base64-arraybuffer';
|
|
2
2
|
export class Signer {
|
|
3
|
-
masterkey;
|
|
4
|
-
encoder = new TextEncoder();
|
|
5
3
|
constructor(masterkey) {
|
|
4
|
+
this.encoder = new TextEncoder();
|
|
6
5
|
const promiseLike = crypto.subtle.importKey('raw', base64ToArrayBuffer(masterkey), { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign']);
|
|
7
6
|
this.masterkey = Promise.resolve(promiseLike);
|
|
8
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cfworker/cosmos",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Azure Cosmos DB client for Cloudflare Workers and service workers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"azure",
|
|
@@ -37,17 +37,16 @@
|
|
|
37
37
|
"test": "cfworker test test/**/*.spec.ts"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@cfworker/worker-types": "^1.
|
|
41
|
-
"@cloudflare/workers-types": "^2.
|
|
42
|
-
"base64-arraybuffer": "^0.
|
|
40
|
+
"@cfworker/worker-types": "^1.11.0",
|
|
41
|
+
"@cloudflare/workers-types": "^3.2.0",
|
|
42
|
+
"base64-arraybuffer": "^1.0.1"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@cfworker/dev": "^1.
|
|
46
|
-
"@types/chai": "^4.2.
|
|
47
|
-
"@types/mocha": "^
|
|
45
|
+
"@cfworker/dev": "^1.11.0",
|
|
46
|
+
"@types/chai": "^4.2.22",
|
|
47
|
+
"@types/mocha": "^9.0.0",
|
|
48
48
|
"chai": "^4.3.4",
|
|
49
|
-
"mocha": "^
|
|
50
|
-
"typescript": "^4.
|
|
51
|
-
}
|
|
52
|
-
"gitHead": "d333ff940566a7acc384ab791d8b73bed2a9a211"
|
|
49
|
+
"mocha": "^9.1.3",
|
|
50
|
+
"typescript": "^4.5.2"
|
|
51
|
+
}
|
|
53
52
|
}
|
package/src/client.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FeedResponse, ItemResponse } from './response.js';
|
|
2
2
|
import { defaultRetryPolicy, RetryContext, RetryPolicy } from './retry.js';
|
|
3
|
+
import { DefaultSessionContainer, SessionContainer } from './session.js';
|
|
3
4
|
import { getSigner, Signer } from './signer.js';
|
|
4
5
|
import {
|
|
5
6
|
Collection,
|
|
@@ -49,9 +50,9 @@ export interface CosmosClientConfig {
|
|
|
49
50
|
collId?: string;
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
|
-
*
|
|
53
|
+
* Reads, writes, and stores session tokens for requests made with ConsistencyLevel is "Session".
|
|
53
54
|
*/
|
|
54
|
-
|
|
55
|
+
sessions?: SessionContainer;
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* System fetch function
|
|
@@ -67,19 +68,19 @@ export class CosmosClient {
|
|
|
67
68
|
private readonly dbId: string | undefined;
|
|
68
69
|
private readonly collId: string | undefined;
|
|
69
70
|
private readonly systemFetch: typeof fetch;
|
|
70
|
-
public
|
|
71
|
+
public readonly sessions: SessionContainer;
|
|
71
72
|
public requestCharges = 0;
|
|
72
73
|
public retries = { count: 0, delayMs: 0 };
|
|
73
74
|
|
|
74
75
|
constructor(config: CosmosClientConfig) {
|
|
75
76
|
this.endpoint = config.endpoint;
|
|
76
77
|
this.signer = getSigner(config.masterKey);
|
|
77
|
-
this.retryPolicy = config.retryPolicy
|
|
78
|
-
this.consistencyLevel = config.consistencyLevel
|
|
78
|
+
this.retryPolicy = config.retryPolicy ?? defaultRetryPolicy;
|
|
79
|
+
this.consistencyLevel = config.consistencyLevel ?? 'Session';
|
|
79
80
|
this.dbId = config.dbId;
|
|
80
81
|
this.collId = config.collId;
|
|
81
|
-
this.
|
|
82
|
-
this.
|
|
82
|
+
this.sessions = config.sessions ?? new DefaultSessionContainer();
|
|
83
|
+
this.systemFetch = config.fetch ?? fetch.bind(self);
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
public async getDatabases(
|
|
@@ -305,40 +306,37 @@ export class CosmosClient {
|
|
|
305
306
|
};
|
|
306
307
|
}
|
|
307
308
|
|
|
308
|
-
private async fetchWithRetry(
|
|
309
|
+
private async fetchWithRetry(
|
|
310
|
+
request: Request,
|
|
311
|
+
context?: RetryContext
|
|
312
|
+
): Promise<Response> {
|
|
309
313
|
const retryRequest = request.clone();
|
|
310
314
|
|
|
311
315
|
const response = await this.fetch(request);
|
|
312
316
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
response
|
|
318
|
-
};
|
|
319
|
-
retryContext.attempts++;
|
|
320
|
-
retryContext.request = retryRequest;
|
|
321
|
-
retryContext.response = response;
|
|
322
|
-
|
|
323
|
-
const { retry, delayMs } = await this.retryPolicy.shouldRetry(retryContext);
|
|
317
|
+
context ??= { attempts: 0, cumulativeWaitMs: 0, request, response };
|
|
318
|
+
context.attempts++;
|
|
319
|
+
context.request = retryRequest;
|
|
320
|
+
context.response = response;
|
|
324
321
|
|
|
322
|
+
const { retry, delayMs } = await this.retryPolicy.shouldRetry(context);
|
|
325
323
|
if (!retry) {
|
|
326
324
|
return response;
|
|
327
325
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
326
|
+
if (delayMs !== 0) {
|
|
327
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
328
|
+
context.cumulativeWaitMs += delayMs;
|
|
329
|
+
this.retries.delayMs += delayMs;
|
|
330
|
+
}
|
|
332
331
|
this.retries.count++;
|
|
333
|
-
this.
|
|
334
|
-
return this.fetchWithRetry(retryContext.request);
|
|
332
|
+
return this.fetchWithRetry(retryRequest, context);
|
|
335
333
|
}
|
|
336
334
|
|
|
337
335
|
private async fetch(request: Request): Promise<Response> {
|
|
336
|
+
this.sessions.setRequestSession(request);
|
|
338
337
|
await this.signer.sign(request);
|
|
339
338
|
const response = await this.systemFetch(request);
|
|
340
|
-
this.
|
|
341
|
-
response.headers.get('x-ms-session-token') || this.sessionToken;
|
|
339
|
+
this.sessions.readResponseSession(response);
|
|
342
340
|
const requestCharge = +(response.headers.get('x-ms-request-charge') || 0);
|
|
343
341
|
this.requestCharges += requestCharge;
|
|
344
342
|
return response;
|
|
@@ -353,9 +351,6 @@ export class CosmosClient {
|
|
|
353
351
|
}
|
|
354
352
|
const consistencyLevel = args.consistencyLevel || this.consistencyLevel;
|
|
355
353
|
headers.set('x-ms-consistency-level', consistencyLevel);
|
|
356
|
-
if (consistencyLevel === 'Session' && this.sessionToken) {
|
|
357
|
-
headers.set('x-ms-session-token', this.sessionToken);
|
|
358
|
-
}
|
|
359
354
|
if (args.continuation) {
|
|
360
355
|
headers.set('x-ms-continuation', args.continuation);
|
|
361
356
|
}
|
|
@@ -409,11 +404,7 @@ export class CosmosClient {
|
|
|
409
404
|
}
|
|
410
405
|
}
|
|
411
406
|
|
|
412
|
-
export type DocumentInit =
|
|
413
|
-
| BufferSource
|
|
414
|
-
| ReadableStream<Uint8Array>
|
|
415
|
-
| string
|
|
416
|
-
| Resource;
|
|
407
|
+
export type DocumentInit = BufferSource | ReadableStream | string | Resource;
|
|
417
408
|
|
|
418
409
|
function toBodyInit(obj: DocumentInit): BodyInit {
|
|
419
410
|
if (
|
package/src/index.ts
CHANGED
package/src/response.ts
CHANGED
package/src/retry.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readSessionNotAvailable } from './session';
|
|
2
|
+
|
|
1
3
|
export interface RetryContext {
|
|
2
4
|
request: Request;
|
|
3
5
|
response: Response;
|
|
@@ -22,6 +24,9 @@ export class DefaultRetryPolicy implements RetryPolicy {
|
|
|
22
24
|
) {}
|
|
23
25
|
|
|
24
26
|
async shouldRetry(context: RetryContext): Promise<RetryInstruction> {
|
|
27
|
+
if (readSessionNotAvailable(context.response)) {
|
|
28
|
+
return { retry: true, delayMs: 0 };
|
|
29
|
+
}
|
|
25
30
|
if (
|
|
26
31
|
context.response.status !== 429 ||
|
|
27
32
|
context.attempts >= this.maxAttempts ||
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const readSessionNotAvailableSubstatus = '1002';
|
|
2
|
+
|
|
3
|
+
export function readSessionNotAvailable(response: Response): boolean {
|
|
4
|
+
return (
|
|
5
|
+
response.status === 404 &&
|
|
6
|
+
response.headers.get('x-ms-substatus') === readSessionNotAvailableSubstatus
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get the database and collection id from the x-ms-alt-content-path header or url.
|
|
12
|
+
* @param requestOrResponse
|
|
13
|
+
*/
|
|
14
|
+
export function getCollectionId({
|
|
15
|
+
url,
|
|
16
|
+
headers
|
|
17
|
+
}: {
|
|
18
|
+
url: string;
|
|
19
|
+
headers: Headers;
|
|
20
|
+
}): { dbId: string; collId: string } | null {
|
|
21
|
+
const path =
|
|
22
|
+
headers.get('x-ms-alt-content-path') ?? new URL(url).pathname.substr(1);
|
|
23
|
+
const [, dbId, colls, collId] = path.split('/');
|
|
24
|
+
if (colls !== 'colls' || !collId) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return { dbId, collId };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SessionTokens {
|
|
31
|
+
[dbId: string]: { [collId: string]: string };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SessionContainer {
|
|
35
|
+
tokens: SessionTokens;
|
|
36
|
+
setRequestSession(request: Request): void;
|
|
37
|
+
readResponseSession(response: Response): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class DefaultSessionContainer implements SessionContainer {
|
|
41
|
+
constructor(public readonly tokens: SessionTokens = {}) {}
|
|
42
|
+
|
|
43
|
+
setRequestSession(request: Request) {
|
|
44
|
+
if (request.headers.get('x-ms-consistency-level') !== 'Session') {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const ids = getCollectionId(request);
|
|
48
|
+
if (!ids) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const token = this.tokens?.[ids.dbId]?.[ids.collId];
|
|
52
|
+
if (token) {
|
|
53
|
+
request.headers.set('x-ms-session-token', token);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
readResponseSession(response: Response) {
|
|
58
|
+
const ids = getCollectionId(response);
|
|
59
|
+
if (!ids) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (readSessionNotAvailable(response)) {
|
|
63
|
+
if (this.tokens[ids.dbId]) {
|
|
64
|
+
delete this.tokens[ids.dbId][ids.collId];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const token = response.headers.get('x-ms-session-token');
|
|
68
|
+
if (!token) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.tokens[ids.dbId] ??= {};
|
|
72
|
+
this.tokens[ids.dbId][ids.collId] = token;
|
|
73
|
+
}
|
|
74
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extends": "../../tsconfig-base",
|
|
3
3
|
"compilerOptions": {
|
|
4
|
-
"target": "
|
|
5
|
-
"lib": ["esnext"
|
|
6
|
-
"types": [
|
|
4
|
+
"target": "ES2021",
|
|
5
|
+
"lib": ["esnext"],
|
|
6
|
+
"types": [
|
|
7
|
+
"chai",
|
|
8
|
+
"mocha",
|
|
9
|
+
"@cfworker/worker-types",
|
|
10
|
+
"@cloudflare/workers-types"
|
|
11
|
+
],
|
|
7
12
|
"outDir": "dist",
|
|
8
13
|
"rootDir": "./src"
|
|
9
14
|
},
|
package/LICENSE.md
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2020 Jeremy Danyow
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|