lambder 7.2.5 → 8.0.2
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/CHANGELOG.md +1021 -3
- package/README.md +43 -21
- package/dist/api/LambderApiAnswer.d.ts +18 -22
- package/dist/api/LambderApiAnswer.js +6 -7
- package/dist/api/LambderApiCallContext.d.ts +21 -8
- package/dist/api/LambderApiCallContext.js +22 -4
- package/dist/api/LambderApiDefinition.d.ts +4 -3
- package/dist/api/LambderApiEnvelope.d.ts +14 -9
- package/dist/api/LambderApiEnvelope.js +33 -34
- package/dist/api/LambderApiGuards.d.ts +78 -51
- package/dist/api/LambderApiGuards.js +34 -36
- package/dist/api/LambderApiIdempotency.d.ts +74 -61
- package/dist/api/LambderApiIdempotency.js +226 -151
- package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
- package/dist/api/LambderApiOutputValidationError.js +50 -0
- package/dist/api/LambderApiPipeline.d.ts +77 -39
- package/dist/api/LambderApiPipeline.js +135 -62
- package/dist/api/LambderApiRateLimits.d.ts +208 -54
- package/dist/api/LambderApiRateLimits.js +197 -108
- package/dist/api/LambderApiRequest.d.ts +27 -21
- package/dist/api/LambderApiRequest.js +26 -19
- package/dist/api/LambderApiSignature.d.ts +12 -15
- package/dist/api/LambderApiSignature.js +28 -51
- package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
- package/dist/api/LambderApiValidationRefusal.js +10 -10
- package/dist/build/freshProcessVerifier.d.ts +13 -0
- package/dist/build/freshProcessVerifier.js +19 -0
- package/dist/build/writeApiSignatures.d.ts +109 -0
- package/dist/build/writeApiSignatures.js +222 -0
- package/dist/build.d.ts +9 -0
- package/dist/build.js +8 -0
- package/dist/client/LambderCaller.d.ts +13 -44
- package/dist/client/LambderCaller.js +77 -84
- package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
- package/dist/client/LambderReloadLoopBreaker.js +90 -46
- package/dist/client/lambderFetchTransport.d.ts +4 -1
- package/dist/client/lambderFetchTransport.js +52 -28
- package/dist/client.d.ts +5 -3
- package/dist/client.js +2 -1
- package/dist/core/Lambder.d.ts +161 -69
- package/dist/core/Lambder.js +370 -226
- package/dist/core/LambderContext.d.ts +82 -15
- package/dist/core/LambderContext.js +107 -20
- package/dist/core/LambderCors.d.ts +21 -3
- package/dist/core/LambderCors.js +35 -16
- package/dist/core/LambderCrashHandling.d.ts +40 -0
- package/dist/core/LambderCrashHandling.js +97 -0
- package/dist/core/LambderCreateOptions.d.ts +151 -75
- package/dist/core/LambderCreateOptions.js +16 -23
- package/dist/core/LambderFiles.d.ts +28 -7
- package/dist/core/LambderFiles.js +73 -33
- package/dist/core/LambderIndexHtml.js +12 -11
- package/dist/core/LambderPolicyBuilders.d.ts +17 -5
- package/dist/core/LambderPolicyBuilders.js +17 -5
- package/dist/core/LambderPublicFiles.d.ts +11 -5
- package/dist/core/LambderPublicFiles.js +32 -4
- package/dist/core/LambderRequestPath.d.ts +43 -0
- package/dist/core/LambderRequestPath.js +63 -0
- package/dist/core/LambderResponse.d.ts +26 -5
- package/dist/core/LambderResponse.js +157 -70
- package/dist/core/LambderResponseBuilder.d.ts +49 -4
- package/dist/core/LambderResponseBuilder.js +64 -3
- package/dist/core/LambderRouting.d.ts +2 -3
- package/dist/core/LambderRouting.js +22 -7
- package/dist/core/LambderTemplatingEngine.js +211 -32
- package/dist/index.d.ts +15 -8
- package/dist/index.js +5 -4
- package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
- package/dist/invoke/LambderInvokeCaller.js +76 -66
- package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
- package/dist/invoke/LambderInvokeOutcome.js +9 -22
- package/dist/invoke/LambderLambdaEvent.d.ts +44 -10
- package/dist/invoke/LambderLambdaEvent.js +80 -37
- package/dist/invoke/lambderHandlerTransport.d.ts +12 -10
- package/dist/invoke/lambderHandlerTransport.js +16 -19
- package/dist/mock/LambderMockApp.d.ts +67 -83
- package/dist/mock/LambderMockApp.js +167 -153
- package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
- package/dist/mock/LambderMockBrowserCookies.js +24 -28
- package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
- package/dist/mock/LambderMockCallRecorder.js +19 -28
- package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
- package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
- package/dist/mock/LambderMockEntryRegistry.js +24 -29
- package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
- package/dist/mock/LambderMockFailureInjector.js +3 -6
- package/dist/mock/LambderMockTypes.d.ts +78 -108
- package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
- package/dist/mock/lambderMockInvokeTransport.js +11 -10
- package/dist/mock/lambderMockMswHandler.d.ts +33 -29
- package/dist/mock/lambderMockMswHandler.js +50 -39
- package/dist/mock.d.ts +3 -1
- package/dist/mock.js +5 -3
- package/dist/session/LambderSessionController.d.ts +108 -89
- package/dist/session/LambderSessionController.js +187 -168
- package/dist/session/LambderSessionCrypto.d.ts +16 -7
- package/dist/session/LambderSessionCrypto.js +26 -12
- package/dist/session/LambderSessionManager.d.ts +136 -47
- package/dist/session/LambderSessionManager.js +280 -139
- package/dist/shared/LambderHtml.d.ts +42 -3
- package/dist/shared/LambderHtml.js +127 -7
- package/dist/shared/LambderHtmlPositions.d.ts +173 -0
- package/dist/shared/LambderHtmlPositions.js +652 -0
- package/dist/shared/LambderI18n.d.ts +10 -11
- package/dist/shared/LambderI18n.js +33 -21
- package/dist/shared/contracts/LambderCache.d.ts +66 -0
- package/dist/shared/contracts/LambderCache.js +11 -0
- package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
- package/dist/shared/contracts/LambderFileSource.js +5 -8
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
- package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
- package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
- package/dist/shared/contracts/LambderRateLimiter.js +4 -5
- package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
- package/dist/shared/contracts/LambderSessionStore.js +5 -6
- package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
- package/dist/shared/transport/LambderApiTransport.js +7 -7
- package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
- package/dist/shared/transport/LambderCookieJar.js +54 -66
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
- package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
- package/dist/shared/util/LambderCallAbort.d.ts +5 -5
- package/dist/shared/util/LambderCallAbort.js +5 -5
- package/dist/shared/util/LambderClientIp.d.ts +27 -11
- package/dist/shared/util/LambderClientIp.js +96 -13
- package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
- package/dist/shared/util/LambderExpiringMap.js +41 -57
- package/dist/shared/util/LambderNodeModules.js +6 -7
- package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
- package/dist/shared/util/LambderOptionChecks.js +4 -4
- package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
- package/dist/shared/util/LambderResponseBrand.js +5 -5
- package/dist/shared/util/LambderTestingDoors.d.ts +29 -0
- package/dist/shared/util/LambderTestingDoors.js +29 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
- package/dist/shared/util/LambderTypeUtilities.js +3 -3
- package/dist/shared/util/boundKeyField.d.ts +20 -0
- package/dist/shared/util/boundKeyField.js +34 -0
- package/dist/shared/util/canonicalJson.d.ts +11 -0
- package/dist/shared/util/canonicalJson.js +28 -0
- package/dist/shared/util/joinKeyFields.d.ts +20 -0
- package/dist/shared/util/joinKeyFields.js +22 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
- package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
- package/dist/shared/wire/LambderApiContract.d.ts +107 -32
- package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
- package/dist/shared/wire/LambderApiOutcome.js +48 -23
- package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
- package/dist/shared/wire/LambderApiRefusal.js +36 -7
- package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
- package/dist/shared/wire/LambderApiSignature.js +16 -19
- package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
- package/dist/shared/wire/LambderCallOptions.js +9 -11
- package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
- package/dist/shared/wire/LambderCompressionCodec.js +31 -36
- package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
- package/dist/shared/wire/LambderCompressionOption.js +9 -9
- package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
- package/dist/shared/wire/LambderCrashDetail.js +12 -15
- package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
- package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
- package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
- package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
- package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
- package/dist/shared/wire/LambderInvokeApiId.js +27 -0
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +79 -0
- package/dist/shared/wire/LambderOutcomeAssertions.js +112 -0
- package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
- package/dist/shared/wire/LambderRequestPayload.js +4 -6
- package/dist/stores/LambderCacheFiller.d.ts +48 -0
- package/dist/stores/LambderCacheFiller.js +119 -0
- package/dist/stores/LambderCacheKeys.d.ts +26 -0
- package/dist/stores/LambderCacheKeys.js +54 -0
- package/dist/stores/LambderCacheValues.d.ts +45 -0
- package/dist/stores/LambderCacheValues.js +74 -0
- package/dist/stores/LambderDdbCache.d.ts +121 -56
- package/dist/stores/LambderDdbCache.js +528 -225
- package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
- package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
- package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
- package/dist/stores/LambderDdbRateLimiter.js +151 -39
- package/dist/stores/LambderDdbSdk.d.ts +43 -31
- package/dist/stores/LambderDdbSdk.js +79 -33
- package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
- package/dist/stores/LambderDdbSessionStore.js +119 -47
- package/dist/stores/LambderHttpFileSource.d.ts +15 -6
- package/dist/stores/LambderHttpFileSource.js +15 -13
- package/dist/stores/LambderMemoryCache.d.ts +49 -0
- package/dist/stores/LambderMemoryCache.js +113 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
- package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
- package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
- package/dist/stores/LambderMemoryRateLimiter.js +14 -13
- package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
- package/dist/stores/LambderMemorySessionStore.js +38 -19
- package/dist/stores/LambderS3FileSource.d.ts +21 -6
- package/dist/stores/LambderS3FileSource.js +12 -7
- package/dist/testing/LambderTestApp.d.ts +176 -0
- package/dist/testing/LambderTestApp.js +204 -0
- package/dist/testing/LambderTestVisitor.d.ts +153 -0
- package/dist/testing/LambderTestVisitor.js +154 -0
- package/dist/testing.d.ts +27 -0
- package/dist/testing.js +24 -0
- package/package.json +20 -3
- package/dist/api/LambderApiPolicyEngine.d.ts +0 -36
- package/dist/api/LambderApiPolicyEngine.js +0 -77
- package/dist/shared/util/LambderKeyFields.d.ts +0 -32
- package/dist/shared/util/LambderKeyFields.js +0 -34
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createDynamoDocumentClientLoader, isConditionalCheckFailure } from "./LambderDdbSdk.js";
|
|
2
2
|
import { compressText, restoreText } from "../shared/wire/LambderCompressionCodec.js";
|
|
3
3
|
import { resolveCompressionOption, } from "../shared/wire/LambderCompressionOption.js";
|
|
4
|
+
/** The fields besides data an update may write, as the manager names them. */
|
|
5
|
+
const UPDATABLE_FIELDS = ["dataExpiresAt", "lastAccessedAt", "expiresAt"];
|
|
4
6
|
/**
|
|
5
7
|
* Session compression defaults: every record compressed (see
|
|
6
8
|
* LambderCompressionOption for the option's shape and toggle semantics).
|
|
@@ -19,6 +21,10 @@ const SESSION_COMPRESSION_DEFAULTS = { minBytes: 0, quality: 5 };
|
|
|
19
21
|
* Table shape: a string hash key (the salted sessionKey hash, every session
|
|
20
22
|
* of one subject shares it) and a string range key (the bearer secret's
|
|
21
23
|
* hash), plus a TTL on `expiresAt` to let DynamoDB sweep expired sessions.
|
|
24
|
+
*
|
|
25
|
+
* Every write is conditional: a create on the item not existing, an update
|
|
26
|
+
* on it existing, so no write already in flight can bring back a session a
|
|
27
|
+
* logout deleted.
|
|
22
28
|
*/
|
|
23
29
|
export class LambderDdbSessionStore {
|
|
24
30
|
/** DynamoDB keeps records after this process is gone. */
|
|
@@ -45,33 +51,38 @@ export class LambderDdbSessionStore {
|
|
|
45
51
|
keyOf(sessionKeyHash, secretHash) {
|
|
46
52
|
return { [this.partitionKey]: sessionKeyHash, [this.sortKey]: secretHash };
|
|
47
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* session.data as the attributes that hold it: `dataBr` and `dataBytes`
|
|
56
|
+
* when compressed, a plain `data` otherwise. Either way it goes through
|
|
57
|
+
* its JSON first, so a plain record holds exactly what a compressed one
|
|
58
|
+
* restores to: an `undefined` inside the data is dropped rather than
|
|
59
|
+
* handed to the document client, which refuses one and would fail the
|
|
60
|
+
* write (a login answering 500) only when compression is off.
|
|
61
|
+
*/
|
|
62
|
+
async dataAttributes(data) {
|
|
63
|
+
const json = JSON.stringify(data);
|
|
64
|
+
const raw = Buffer.from(json, "utf8");
|
|
65
|
+
if (this.compression && raw.byteLength >= this.compression.minBytes) {
|
|
66
|
+
return { dataBr: await compressText(raw, "br", this.compression.quality), dataBytes: raw.byteLength };
|
|
67
|
+
}
|
|
68
|
+
return { data: JSON.parse(json) };
|
|
69
|
+
}
|
|
48
70
|
/** The item for a record: the two hashes under the table's key names, the data plain or compressed. */
|
|
49
71
|
async toItem(record) {
|
|
50
72
|
const { sessionKeyHash, secretHash, data, ...rest } = record;
|
|
51
|
-
|
|
52
|
-
const raw = this.compression && Buffer.from(JSON.stringify(data), "utf8");
|
|
53
|
-
if (this.compression && raw && raw.byteLength >= this.compression.minBytes) {
|
|
54
|
-
item.dataBr = await compressText(raw, "br", this.compression.quality);
|
|
55
|
-
item.dataBytes = raw.byteLength;
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
item.data = data;
|
|
59
|
-
}
|
|
60
|
-
return item;
|
|
73
|
+
return { ...this.keyOf(sessionKeyHash, secretHash), ...rest, ...await this.dataAttributes(data) };
|
|
61
74
|
}
|
|
62
75
|
/**
|
|
63
76
|
* The record for an item. A compressed record decodes back into `data`;
|
|
64
|
-
* one whose data cannot be decoded is
|
|
65
|
-
*
|
|
77
|
+
* one whose data cannot be decoded is malformed and reads as no session,
|
|
78
|
+
* like a record missing its csrfTokenHash.
|
|
66
79
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* clear, on every request, until the TTL retires it. Ending it lets them
|
|
74
|
-
* log in again.
|
|
80
|
+
* This is where read failures and malformed records separate. A read
|
|
81
|
+
* failure is infrastructure and must surface as a 500: signing somebody
|
|
82
|
+
* out over a transient DynamoDB error is worse than an error page. A
|
|
83
|
+
* record that will not decode will not decode on the next request either,
|
|
84
|
+
* so a 500 there would be a session the visitor can neither use nor clear
|
|
85
|
+
* until the TTL retires it. Ending it lets them log in again.
|
|
75
86
|
*/
|
|
76
87
|
async fromItem(item) {
|
|
77
88
|
const { [this.partitionKey]: sessionKeyHash, [this.sortKey]: secretHash, dataBr, dataBytes, data, ...rest } = item;
|
|
@@ -89,15 +100,19 @@ export class LambderDdbSessionStore {
|
|
|
89
100
|
// everything past this line trusts them: the manager compares the two
|
|
90
101
|
// hashes in constant time (a non-string would throw there rather than
|
|
91
102
|
// answer false) and reads expiresAt as a number to decide whether the
|
|
92
|
-
// session is over. An item missing them is not this store's record
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
103
|
+
// session is over. An item missing them is not this store's record
|
|
104
|
+
// (written by an earlier major, by hand, or by another app sharing the
|
|
105
|
+
// table), and it reads as no session for the same reason an
|
|
106
|
+
// undecodable one does: it will not become valid later. Not logged: a
|
|
107
|
+
// visitor whose cookie names such an item sends it on every request
|
|
108
|
+
// until the cookie expires, and the item itself goes with its TTL.
|
|
109
|
+
// dataVersion is load-bearing the same way: every conditioned write
|
|
110
|
+
// names the one read, and a record without it would fail that write
|
|
111
|
+
// rather than answer "stale".
|
|
96
112
|
if (typeof sessionKeyHash !== "string" || typeof secretHash !== "string"
|
|
97
113
|
|| typeof rest.csrfTokenHash !== "string" || typeof rest.sessionKey !== "string"
|
|
98
114
|
|| typeof rest.createdAt !== "number" || typeof rest.expiresAt !== "number"
|
|
99
|
-
|| typeof rest.ttlInSeconds !== "number") {
|
|
100
|
-
console.warn(`LambderDdbSessionStore: an item in "${this.tableName}" is missing the fields a session record has, so it reads as no session.`);
|
|
115
|
+
|| typeof rest.ttlInSeconds !== "number" || typeof rest.dataVersion !== "number") {
|
|
101
116
|
return null;
|
|
102
117
|
}
|
|
103
118
|
// The one cast: session.data is whatever the app put there, and JSON
|
|
@@ -112,14 +127,86 @@ export class LambderDdbSessionStore {
|
|
|
112
127
|
return null;
|
|
113
128
|
return await this.fromItem(response.Item);
|
|
114
129
|
}
|
|
115
|
-
async
|
|
130
|
+
async create(record) {
|
|
116
131
|
const item = await this.toItem(record);
|
|
117
132
|
const { client, sdk } = await this.ready();
|
|
118
|
-
await client.send(new sdk.PutCommand({
|
|
133
|
+
await client.send(new sdk.PutCommand({
|
|
134
|
+
TableName: this.tableName,
|
|
135
|
+
Item: item,
|
|
136
|
+
ConditionExpression: "attribute_not_exists(#sk)",
|
|
137
|
+
ExpressionAttributeNames: { "#sk": this.sortKey },
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
async update(sessionKeyHash, secretHash, changes, condition) {
|
|
141
|
+
const names = { "#sk": this.sortKey };
|
|
142
|
+
const values = {};
|
|
143
|
+
const set = [];
|
|
144
|
+
const remove = [];
|
|
145
|
+
const add = [];
|
|
146
|
+
if ("data" in changes) {
|
|
147
|
+
// The data is held in one of two forms, so writing one removes the other.
|
|
148
|
+
const attributes = await this.dataAttributes(changes.data);
|
|
149
|
+
for (const attribute of ["data", "dataBr", "dataBytes"]) {
|
|
150
|
+
names[`#${attribute}`] = attribute;
|
|
151
|
+
if (attribute in attributes) {
|
|
152
|
+
values[`:${attribute}`] = attributes[attribute];
|
|
153
|
+
set.push(`#${attribute} = :${attribute}`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
remove.push(`#${attribute}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const field of UPDATABLE_FIELDS) {
|
|
161
|
+
if (changes[field] === undefined)
|
|
162
|
+
continue;
|
|
163
|
+
names[`#${field}`] = field;
|
|
164
|
+
values[`:${field}`] = changes[field];
|
|
165
|
+
set.push(`#${field} = :${field}`);
|
|
166
|
+
}
|
|
167
|
+
// A write of the data or its deadline moves the version in the same
|
|
168
|
+
// write, whatever value it writes (see LambderSessionStore.update).
|
|
169
|
+
if ("data" in changes || changes.dataExpiresAt !== undefined) {
|
|
170
|
+
names["#dataVersion"] = "dataVersion";
|
|
171
|
+
values[":dataVersionStep"] = 1;
|
|
172
|
+
add.push("#dataVersion :dataVersionStep");
|
|
173
|
+
}
|
|
174
|
+
let conditionExpression = "attribute_exists(#sk)";
|
|
175
|
+
if (condition) {
|
|
176
|
+
names["#dataVersion"] = "dataVersion";
|
|
177
|
+
values[":readDataVersion"] = condition.dataVersion;
|
|
178
|
+
conditionExpression += " AND #dataVersion = :readDataVersion";
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const { client, sdk } = await this.ready();
|
|
182
|
+
await client.send(new sdk.UpdateCommand({
|
|
183
|
+
TableName: this.tableName,
|
|
184
|
+
Key: this.keyOf(sessionKeyHash, secretHash),
|
|
185
|
+
UpdateExpression: [
|
|
186
|
+
set.length ? `SET ${set.join(", ")}` : "",
|
|
187
|
+
add.length ? `ADD ${add.join(", ")}` : "",
|
|
188
|
+
remove.length ? `REMOVE ${remove.join(", ")}` : "",
|
|
189
|
+
].filter(Boolean).join(" "),
|
|
190
|
+
ConditionExpression: conditionExpression,
|
|
191
|
+
ExpressionAttributeNames: names,
|
|
192
|
+
...(Object.keys(values).length ? { ExpressionAttributeValues: values } : {}),
|
|
193
|
+
// Which half of the condition failed is told by the item the
|
|
194
|
+
// refusal hands back, in the same call: none means the record
|
|
195
|
+
// is gone, one means its dataVersion moved.
|
|
196
|
+
ReturnValuesOnConditionCheckFailure: "ALL_OLD",
|
|
197
|
+
}));
|
|
198
|
+
return "updated";
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
if (!isConditionalCheckFailure(err))
|
|
202
|
+
throw err;
|
|
203
|
+
return err.Item ? "stale" : "missing";
|
|
204
|
+
}
|
|
119
205
|
}
|
|
120
206
|
async delete(sessionKeyHash, secretHash) {
|
|
121
207
|
const { client, sdk } = await this.ready();
|
|
122
|
-
await client.send(new sdk.DeleteCommand({ TableName: this.tableName, Key: this.keyOf(sessionKeyHash, secretHash) }));
|
|
208
|
+
const response = await client.send(new sdk.DeleteCommand({ TableName: this.tableName, Key: this.keyOf(sessionKeyHash, secretHash), ReturnValues: "ALL_OLD" }));
|
|
209
|
+
return response.Attributes ? await this.fromItem(response.Attributes) : null;
|
|
123
210
|
}
|
|
124
211
|
async listSecretHashes(sessionKeyHash) {
|
|
125
212
|
const params = {
|
|
@@ -128,6 +215,9 @@ export class LambderDdbSessionStore {
|
|
|
128
215
|
ProjectionExpression: "#sk",
|
|
129
216
|
ExpressionAttributeNames: { "#pk": this.partitionKey, "#sk": this.sortKey },
|
|
130
217
|
ExpressionAttributeValues: { ":pv": sessionKeyHash },
|
|
218
|
+
// Consistent: "log out everywhere" has to find a session created
|
|
219
|
+
// a moment before it, and an eventually consistent read may not.
|
|
220
|
+
ConsistentRead: true,
|
|
131
221
|
};
|
|
132
222
|
const hashes = [];
|
|
133
223
|
for (;;) {
|
|
@@ -140,22 +230,4 @@ export class LambderDdbSessionStore {
|
|
|
140
230
|
params.ExclusiveStartKey = LastEvaluatedKey;
|
|
141
231
|
}
|
|
142
232
|
}
|
|
143
|
-
async markDataExpired(sessionKeyHash, secretHash, at) {
|
|
144
|
-
try {
|
|
145
|
-
const { client, sdk } = await this.ready();
|
|
146
|
-
await client.send(new sdk.UpdateCommand({
|
|
147
|
-
TableName: this.tableName,
|
|
148
|
-
Key: this.keyOf(sessionKeyHash, secretHash),
|
|
149
|
-
UpdateExpression: "SET #dataExpiresAt = :at",
|
|
150
|
-
ConditionExpression: "attribute_exists(#sk)",
|
|
151
|
-
ExpressionAttributeNames: { "#dataExpiresAt": "dataExpiresAt", "#sk": this.sortKey },
|
|
152
|
-
ExpressionAttributeValues: { ":at": at },
|
|
153
|
-
}));
|
|
154
|
-
}
|
|
155
|
-
catch (err) {
|
|
156
|
-
// Deleted between the query and the update: nothing left to expire.
|
|
157
|
-
if (!isConditionalCheckFailure(err))
|
|
158
|
-
throw err;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
233
|
}
|
|
@@ -9,6 +9,14 @@ export type LambderHttpFileSourceOptions = {
|
|
|
9
9
|
headers?: Record<string, string>;
|
|
10
10
|
/** How long one read may take before it fails. Default: 10000. */
|
|
11
11
|
timeoutMs?: number;
|
|
12
|
+
/**
|
|
13
|
+
* The statuses that mean "no such file", read as null so the request
|
|
14
|
+
* falls through. Default: [403, 404, 410]. A private S3 bucket behind
|
|
15
|
+
* CloudFront answers a missing key 403, since its reader may not list the
|
|
16
|
+
* bucket; an origin whose 403 only ever means a refused credential passes
|
|
17
|
+
* [404, 410] so that failure surfaces as an error.
|
|
18
|
+
*/
|
|
19
|
+
notFoundStatuses?: readonly number[];
|
|
12
20
|
};
|
|
13
21
|
/**
|
|
14
22
|
* Files over HTTP(S) from any origin that serves them by path: a CDN, a
|
|
@@ -16,16 +24,17 @@ export type LambderHttpFileSourceOptions = {
|
|
|
16
24
|
* endpoint) or another server. Reads with the runtime's fetch, so it needs
|
|
17
25
|
* no SDK, and no credentials for a public origin; reads come out of the
|
|
18
26
|
* origin's edge cache. Each path segment is percent-encoded, so a relative
|
|
19
|
-
* path names the same object it would as an S3 key. A
|
|
20
|
-
* null and the request falls through; any other
|
|
21
|
-
* error or a timeout
|
|
22
|
-
* used unless it is a generic octet-stream, in which case
|
|
23
|
-
*
|
|
27
|
+
* path names the same object it would as an S3 key. A missing file (see
|
|
28
|
+
* `notFoundStatuses`) reads as null and the request falls through; any other
|
|
29
|
+
* failed status, a network error or a timeout throws. The response's
|
|
30
|
+
* Content-Type is used unless it is a generic octet-stream, in which case
|
|
31
|
+
* the extension decides.
|
|
24
32
|
*/
|
|
25
33
|
export declare class LambderHttpFileSource implements LambderFileSource {
|
|
26
34
|
private readonly baseUrl;
|
|
27
35
|
private readonly headers;
|
|
28
36
|
private readonly timeoutMs;
|
|
29
|
-
|
|
37
|
+
private readonly notFoundStatuses;
|
|
38
|
+
constructor({ baseUrl, headers, timeoutMs, notFoundStatuses }: LambderHttpFileSourceOptions);
|
|
30
39
|
read(relativePath: string): Promise<LambderFile | null>;
|
|
31
40
|
}
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import { remoteStoreFile } from "../shared/contracts/LambderFileSource.js";
|
|
2
2
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
3
|
+
const DEFAULT_NOT_FOUND_STATUSES = [403, 404, 410];
|
|
3
4
|
/**
|
|
4
5
|
* Files over HTTP(S) from any origin that serves them by path: a CDN, a
|
|
5
6
|
* public bucket's own domain (a Cloudflare R2 custom domain, an S3 website
|
|
6
7
|
* endpoint) or another server. Reads with the runtime's fetch, so it needs
|
|
7
8
|
* no SDK, and no credentials for a public origin; reads come out of the
|
|
8
9
|
* origin's edge cache. Each path segment is percent-encoded, so a relative
|
|
9
|
-
* path names the same object it would as an S3 key. A
|
|
10
|
-
* null and the request falls through; any other
|
|
11
|
-
* error or a timeout
|
|
12
|
-
* used unless it is a generic octet-stream, in which case
|
|
13
|
-
*
|
|
10
|
+
* path names the same object it would as an S3 key. A missing file (see
|
|
11
|
+
* `notFoundStatuses`) reads as null and the request falls through; any other
|
|
12
|
+
* failed status, a network error or a timeout throws. The response's
|
|
13
|
+
* Content-Type is used unless it is a generic octet-stream, in which case
|
|
14
|
+
* the extension decides.
|
|
14
15
|
*/
|
|
15
16
|
export class LambderHttpFileSource {
|
|
16
17
|
baseUrl;
|
|
17
18
|
headers;
|
|
18
19
|
timeoutMs;
|
|
19
|
-
|
|
20
|
+
notFoundStatuses;
|
|
21
|
+
constructor({ baseUrl, headers = {}, timeoutMs = DEFAULT_TIMEOUT_MS, notFoundStatuses = DEFAULT_NOT_FOUND_STATUSES }) {
|
|
20
22
|
let url;
|
|
21
23
|
try {
|
|
22
24
|
url = new URL(baseUrl);
|
|
@@ -31,22 +33,22 @@ export class LambderHttpFileSource {
|
|
|
31
33
|
this.baseUrl = url;
|
|
32
34
|
this.headers = headers;
|
|
33
35
|
this.timeoutMs = timeoutMs;
|
|
36
|
+
this.notFoundStatuses = notFoundStatuses;
|
|
34
37
|
}
|
|
35
38
|
async read(relativePath) {
|
|
36
39
|
const url = new URL(relativePath.split("/").map(encodeURIComponent).join("/"), this.baseUrl);
|
|
37
|
-
// The reader's path rule already refuses
|
|
38
|
-
//
|
|
40
|
+
// The reader's path rule already refuses anything that could make this
|
|
41
|
+
// reference leave the configured folder (a leading slash makes it
|
|
39
42
|
// root-relative, two make it protocol-relative and pick the host).
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// this app's own domain.
|
|
43
|
+
// Re-checked rather than trusted: the value is the request path, and
|
|
44
|
+
// escaping would mean a credentialed fetch of an attacker-named
|
|
45
|
+
// origin, served back from this app's own domain.
|
|
44
46
|
if (!url.href.startsWith(this.baseUrl.href))
|
|
45
47
|
return null;
|
|
46
48
|
const response = await fetch(url, { headers: this.headers, signal: AbortSignal.timeout(this.timeoutMs) });
|
|
47
49
|
if (!response.ok) {
|
|
48
50
|
await response.body?.cancel();
|
|
49
|
-
if (
|
|
51
|
+
if (this.notFoundStatuses.includes(response.status))
|
|
50
52
|
return null;
|
|
51
53
|
throw new Error(`LambderHttpFileSource: ${response.status} ${response.statusText} reading ${url}`);
|
|
52
54
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { LambderCache, LambderCacheKey, LambderCacheListOptions, LambderCacheSetOptions } from "../shared/contracts/LambderCache.js";
|
|
2
|
+
export interface LambderMemoryCacheOptions {
|
|
3
|
+
/** Default: one year, as LambderDdbCache's. */
|
|
4
|
+
defaultTtlSeconds?: number;
|
|
5
|
+
/** Largest value accepted, in UTF-8 bytes of its JSON. Default: 32 MiB, as LambderDdbCache's. */
|
|
6
|
+
maxValueBytes?: number;
|
|
7
|
+
/** Ceiling on entries held at once; past it the ones closest to expiring go first. Default: 100,000. */
|
|
8
|
+
maxEntries?: number;
|
|
9
|
+
/** The clock entries expire against, so a test can cross a TTL without waiting. */
|
|
10
|
+
now?: () => number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* LambderDdbCache's twin, held in memory: the same LambderCache interface and
|
|
14
|
+
* the same rules, so code written against the interface can be tested
|
|
15
|
+
* without a table. It refuses the keys and values the DynamoDB cache
|
|
16
|
+
* refuses, stores a value's JSON text and hands back a fresh parse of it,
|
|
17
|
+
* expires entries on the same TTL, and lists sort keys in the same order.
|
|
18
|
+
*
|
|
19
|
+
* getOrSet runs through the LambderCacheFiller both caches hold: concurrent
|
|
20
|
+
* calls for one key share a load, every call (the filling one included)
|
|
21
|
+
* answers the stored JSON parsed, a loader's undefined comes back uncached,
|
|
22
|
+
* and a set, delete or deletePartition of the key while the loader runs
|
|
23
|
+
* keeps the fill from storing over it, so a test that passes here passes
|
|
24
|
+
* against the table.
|
|
25
|
+
*
|
|
26
|
+
* It has none of the table's machinery: no compression, no chunks, no fill
|
|
27
|
+
* lease across containers. It is one process's cache, bounded by
|
|
28
|
+
* `maxEntries`, and a single-process server could use it as that.
|
|
29
|
+
*/
|
|
30
|
+
export declare class LambderMemoryCache implements LambderCache {
|
|
31
|
+
private readonly entries;
|
|
32
|
+
/** getOrSet's single-flight and fail-open, shared with LambderDdbCache (see LambderCacheFiller). */
|
|
33
|
+
private readonly filler;
|
|
34
|
+
private readonly defaultTtlSeconds;
|
|
35
|
+
private readonly maxValueBytes;
|
|
36
|
+
private readonly now;
|
|
37
|
+
constructor(options?: LambderMemoryCacheOptions);
|
|
38
|
+
get<T>(key: LambderCacheKey): Promise<T | undefined>;
|
|
39
|
+
has(key: LambderCacheKey): Promise<boolean>;
|
|
40
|
+
set<T>(key: LambderCacheKey, value: T, options?: LambderCacheSetOptions): Promise<void>;
|
|
41
|
+
delete(key: LambderCacheKey): Promise<boolean>;
|
|
42
|
+
deletePartition(partition: string): Promise<number>;
|
|
43
|
+
listSortKeys(partition: string, options?: LambderCacheListOptions): Promise<string[]>;
|
|
44
|
+
getOrSet<T>(key: LambderCacheKey, loader: () => Promise<T>, options?: LambderCacheSetOptions): Promise<T>;
|
|
45
|
+
/** Forgets every entry; a fill in flight meanwhile stores nothing. */
|
|
46
|
+
reset(): void;
|
|
47
|
+
/** Stores the value and hands back what was stored, the parse of its JSON. */
|
|
48
|
+
private setByAddress;
|
|
49
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { LambderExpiringMap } from "../shared/util/LambderExpiringMap.js";
|
|
2
|
+
import { assertPositiveInteger } from "../shared/util/LambderOptionChecks.js";
|
|
3
|
+
import { cacheMemoryKeyOf, encodeCacheSortKey, normalizeCacheKey, normalizeCachePartition } from "./LambderCacheKeys.js";
|
|
4
|
+
import { DEFAULT_MAX_VALUE_BYTES, DEFAULT_TTL_SECONDS, resolveCacheTtlSeconds, resolveGetOrSetOptions, serializeCacheValue, } from "./LambderCacheValues.js";
|
|
5
|
+
import { LambderCacheFiller } from "./LambderCacheFiller.js";
|
|
6
|
+
/** DynamoDB orders string range keys by their UTF-8 bytes, which is not JavaScript's UTF-16 order for every character. */
|
|
7
|
+
const compareUtf8 = (first, second) => {
|
|
8
|
+
const a = new TextEncoder().encode(first);
|
|
9
|
+
const b = new TextEncoder().encode(second);
|
|
10
|
+
const length = Math.min(a.length, b.length);
|
|
11
|
+
for (let index = 0; index < length; index += 1) {
|
|
12
|
+
if (a[index] !== b[index])
|
|
13
|
+
return a[index] - b[index];
|
|
14
|
+
}
|
|
15
|
+
return a.length - b.length;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* LambderDdbCache's twin, held in memory: the same LambderCache interface and
|
|
19
|
+
* the same rules, so code written against the interface can be tested
|
|
20
|
+
* without a table. It refuses the keys and values the DynamoDB cache
|
|
21
|
+
* refuses, stores a value's JSON text and hands back a fresh parse of it,
|
|
22
|
+
* expires entries on the same TTL, and lists sort keys in the same order.
|
|
23
|
+
*
|
|
24
|
+
* getOrSet runs through the LambderCacheFiller both caches hold: concurrent
|
|
25
|
+
* calls for one key share a load, every call (the filling one included)
|
|
26
|
+
* answers the stored JSON parsed, a loader's undefined comes back uncached,
|
|
27
|
+
* and a set, delete or deletePartition of the key while the loader runs
|
|
28
|
+
* keeps the fill from storing over it, so a test that passes here passes
|
|
29
|
+
* against the table.
|
|
30
|
+
*
|
|
31
|
+
* It has none of the table's machinery: no compression, no chunks, no fill
|
|
32
|
+
* lease across containers. It is one process's cache, bounded by
|
|
33
|
+
* `maxEntries`, and a single-process server could use it as that.
|
|
34
|
+
*/
|
|
35
|
+
export class LambderMemoryCache {
|
|
36
|
+
entries;
|
|
37
|
+
/** getOrSet's single-flight and fail-open, shared with LambderDdbCache (see LambderCacheFiller). */
|
|
38
|
+
filler = new LambderCacheFiller("Memory cache failed open");
|
|
39
|
+
defaultTtlSeconds;
|
|
40
|
+
maxValueBytes;
|
|
41
|
+
now;
|
|
42
|
+
constructor(options = {}) {
|
|
43
|
+
this.now = options.now ?? (() => Date.now());
|
|
44
|
+
this.defaultTtlSeconds = assertPositiveInteger(options.defaultTtlSeconds ?? DEFAULT_TTL_SECONDS, "defaultTtlSeconds");
|
|
45
|
+
this.maxValueBytes = assertPositiveInteger(options.maxValueBytes ?? DEFAULT_MAX_VALUE_BYTES, "maxValueBytes");
|
|
46
|
+
this.entries = new LambderExpiringMap({ now: this.now, maxEntries: options.maxEntries });
|
|
47
|
+
}
|
|
48
|
+
async get(key) {
|
|
49
|
+
const entry = this.entries.get(normalizeCacheKey(key).memoryKey);
|
|
50
|
+
return entry ? JSON.parse(entry.json) : undefined;
|
|
51
|
+
}
|
|
52
|
+
async has(key) {
|
|
53
|
+
return this.entries.get(normalizeCacheKey(key).memoryKey) !== undefined;
|
|
54
|
+
}
|
|
55
|
+
async set(key, value, options = {}) {
|
|
56
|
+
const address = normalizeCacheKey(key);
|
|
57
|
+
const ttlSeconds = resolveCacheTtlSeconds(options.ttlSeconds, this.defaultTtlSeconds);
|
|
58
|
+
this.filler.supersedeFill(address.memoryKey);
|
|
59
|
+
this.setByAddress(address, value, ttlSeconds);
|
|
60
|
+
}
|
|
61
|
+
async delete(key) {
|
|
62
|
+
const address = normalizeCacheKey(key);
|
|
63
|
+
this.filler.supersedeFill(address.memoryKey);
|
|
64
|
+
const existed = this.entries.get(address.memoryKey) !== undefined;
|
|
65
|
+
this.entries.delete(address.memoryKey);
|
|
66
|
+
return existed;
|
|
67
|
+
}
|
|
68
|
+
async deletePartition(partition) {
|
|
69
|
+
const normalized = normalizeCachePartition(partition);
|
|
70
|
+
this.filler.supersedeFillsWithPrefix(cacheMemoryKeyOf(normalized, ""));
|
|
71
|
+
let removed = 0;
|
|
72
|
+
for (const { address } of this.entries.values()) {
|
|
73
|
+
if (address.partition !== normalized)
|
|
74
|
+
continue;
|
|
75
|
+
this.entries.delete(address.memoryKey);
|
|
76
|
+
removed += 1;
|
|
77
|
+
}
|
|
78
|
+
return removed;
|
|
79
|
+
}
|
|
80
|
+
async listSortKeys(partition, options = {}) {
|
|
81
|
+
const normalized = normalizeCachePartition(partition);
|
|
82
|
+
const prefix = options.prefix ?? "";
|
|
83
|
+
const limit = options.limit === undefined ? undefined : assertPositiveInteger(options.limit, "limit");
|
|
84
|
+
const sortKeys = this.entries.values()
|
|
85
|
+
.flatMap(({ address }) => address.partition === normalized && address.sortKey !== null && address.sortKey.startsWith(prefix) ? [address.sortKey] : [])
|
|
86
|
+
// Ordered as the table orders the items it lists: by the whole
|
|
87
|
+
// item sort key, which carries a "#" after the encoded key. So
|
|
88
|
+
// "New York City" sorts before "New York" there (" " is below
|
|
89
|
+
// "#"), and here too.
|
|
90
|
+
.sort((first, second) => compareUtf8(`${encodeCacheSortKey(first)}#`, `${encodeCacheSortKey(second)}#`));
|
|
91
|
+
return limit === undefined ? sortKeys : sortKeys.slice(0, limit);
|
|
92
|
+
}
|
|
93
|
+
async getOrSet(key, loader, options = {}) {
|
|
94
|
+
const address = normalizeCacheKey(key);
|
|
95
|
+
const { ttlSeconds } = resolveGetOrSetOptions(options, this.defaultTtlSeconds);
|
|
96
|
+
return this.filler.getOrSet(address.memoryKey, loader, async (load) => {
|
|
97
|
+
const existing = this.entries.get(address.memoryKey);
|
|
98
|
+
return existing ? JSON.parse(existing.json) : await load(async (value) => this.setByAddress(address, value, ttlSeconds));
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** Forgets every entry; a fill in flight meanwhile stores nothing. */
|
|
102
|
+
reset() {
|
|
103
|
+
this.filler.supersedeFillsWithPrefix("");
|
|
104
|
+
this.entries.clear();
|
|
105
|
+
}
|
|
106
|
+
/** Stores the value and hands back what was stored, the parse of its JSON. */
|
|
107
|
+
setByAddress(address, value, ttlSeconds) {
|
|
108
|
+
const { json } = serializeCacheValue(value, this.maxValueBytes);
|
|
109
|
+
const expiresAt = Math.floor(this.now() / 1000) + ttlSeconds;
|
|
110
|
+
this.entries.set(address.memoryKey, { json, address }, expiresAt);
|
|
111
|
+
return JSON.parse(json);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { LambderIdempotencyStore, LambderIdempotencyDoneRecord, LambderIdem
|
|
|
2
2
|
type MemoryIdempotencyRecord = {
|
|
3
3
|
state: "pending";
|
|
4
4
|
ownerToken: string;
|
|
5
|
+
fingerprint: string;
|
|
5
6
|
} | ({
|
|
6
7
|
state: "done";
|
|
7
8
|
ownerToken: string;
|
|
@@ -17,14 +18,12 @@ type MemoryIdempotencyRecord = {
|
|
|
17
18
|
* path can be exercised; unbounded by default. `now` is injectable so a test
|
|
18
19
|
* can expire a claim without waiting.
|
|
19
20
|
*
|
|
20
|
-
* `maxEntries`
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* exists to prevent; a claim that cannot be made room for is reported as
|
|
27
|
-
* "pending" instead, so the duplicate is refused rather than run.
|
|
21
|
+
* `maxEntries` (100,000 by default) is the one way this store differs from a
|
|
22
|
+
* table: past the ceiling, SETTLED records are dropped, soonest expiry first,
|
|
23
|
+
* and a retry whose record was dropped executes again instead of replaying.
|
|
24
|
+
* Pending claims are never dropped, since losing one lets two concurrent
|
|
25
|
+
* retries both execute; a claim that finds no room is reported as "pending"
|
|
26
|
+
* instead, so the duplicate is refused rather than run.
|
|
28
27
|
*/
|
|
29
28
|
export declare class LambderMemoryIdempotencyStore implements LambderIdempotencyStore {
|
|
30
29
|
private readonly records;
|
|
@@ -42,17 +41,19 @@ export declare class LambderMemoryIdempotencyStore implements LambderIdempotency
|
|
|
42
41
|
/** A settled record as the engine reads it: a copy, so a caller writing onto what it got back cannot rewrite the record. */
|
|
43
42
|
private static answerOf;
|
|
44
43
|
peek(scopeKey: string): Promise<LambderIdempotencyDoneRecord | null>;
|
|
45
|
-
begin(scopeKey: string, { pendingTtlSeconds }: {
|
|
44
|
+
begin(scopeKey: string, { pendingTtlSeconds, fingerprint }: {
|
|
46
45
|
pendingTtlSeconds: number;
|
|
46
|
+
fingerprint: string;
|
|
47
47
|
}): Promise<LambderIdempotencyBeginResult>;
|
|
48
|
-
complete(scopeKey: string, ownerToken: string, { statusCode, headers, body, ttlSeconds }: LambderIdempotencyDoneRecord & {
|
|
48
|
+
complete(scopeKey: string, ownerToken: string, { statusCode, headers, body, fingerprint, ttlSeconds }: LambderIdempotencyDoneRecord & {
|
|
49
49
|
ttlSeconds: number;
|
|
50
50
|
}): Promise<"stored" | "too-large" | "lost">;
|
|
51
|
+
/** Releases a pending claim for its owner; a settled record stays, as LambderIdempotencyStore requires. */
|
|
51
52
|
abandon(scopeKey: string, ownerToken: string): Promise<void>;
|
|
52
53
|
/**
|
|
53
54
|
* The record under a scope, for assertions; null when absent or expired.
|
|
54
|
-
* A copy, like every
|
|
55
|
-
*
|
|
55
|
+
* A copy, like every read here, so an assertion cannot edit the stored
|
|
56
|
+
* record.
|
|
56
57
|
*/
|
|
57
58
|
recordOf(scopeKey: string): MemoryIdempotencyRecord | null;
|
|
58
59
|
/** Number of live records held. */
|