lambder 7.3.1 → 8.1.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/CHANGELOG.md +1047 -3
- package/README.md +46 -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 +68 -62
- package/dist/api/LambderApiIdempotency.js +214 -151
- package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
- package/dist/api/LambderApiOutputValidationError.js +50 -0
- package/dist/api/LambderApiPipeline.d.ts +47 -38
- package/dist/api/LambderApiPipeline.js +122 -63
- package/dist/api/LambderApiRateLimits.d.ts +201 -54
- package/dist/api/LambderApiRateLimits.js +185 -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/ContractTypePrinter.d.ts +85 -0
- package/dist/build/ContractTypePrinter.js +402 -0
- package/dist/build/freshProcessVerifier.d.ts +13 -0
- package/dist/build/freshProcessVerifier.js +19 -0
- package/dist/build/moduleLocation.d.ts +11 -0
- package/dist/build/moduleLocation.js +6 -0
- package/dist/build/writeApiContract.d.ts +78 -0
- package/dist/build/writeApiContract.js +302 -0
- package/dist/build/writeApiSignatures.d.ts +114 -0
- package/dist/build/writeApiSignatures.js +217 -0
- package/dist/build/writeFileAtomically.d.ts +8 -0
- package/dist/build/writeFileAtomically.js +22 -0
- package/dist/build.d.ts +14 -0
- package/dist/build.js +11 -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/LambderUploadRunner.d.ts +96 -0
- package/dist/client/LambderUploadRunner.js +234 -0
- package/dist/client/lambderFetchTransport.d.ts +4 -1
- package/dist/client/lambderFetchTransport.js +52 -28
- package/dist/client.d.ts +9 -3
- package/dist/client.js +6 -1
- package/dist/core/Lambder.d.ts +143 -79
- package/dist/core/Lambder.js +350 -231
- 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 +21 -7
- package/dist/core/LambderFiles.js +62 -34
- 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 +25 -8
- package/dist/index.js +13 -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 +29 -9
- package/dist/invoke/LambderLambdaEvent.js +40 -22
- package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
- package/dist/invoke/lambderHandlerTransport.js +15 -18
- 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 +43 -33
- package/dist/mock/lambderMockMswHandler.js +50 -39
- package/dist/mock/lambderMockUploadMswHandler.d.ts +26 -0
- package/dist/mock/lambderMockUploadMswHandler.js +28 -0
- package/dist/mock.d.ts +4 -1
- package/dist/mock.js +6 -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 +124 -46
- package/dist/session/LambderSessionManager.js +262 -137
- 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/contracts/LambderUploadBucket.d.ts +154 -0
- package/dist/shared/contracts/LambderUploadBucket.js +74 -0
- 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/LambderContentDisposition.d.ts +10 -0
- package/dist/shared/util/LambderContentDisposition.js +13 -0
- 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/LambderTextDigest.d.ts +7 -5
- package/dist/shared/util/LambderTextDigest.js +11 -5
- 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 +98 -53
- package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
- package/dist/shared/wire/LambderApiOutcome.js +48 -23
- package/dist/shared/wire/LambderApiRefusal.d.ts +45 -27
- package/dist/shared/wire/LambderApiRefusal.js +42 -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 +6 -7
- package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
- package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
- package/dist/shared/wire/LambderRequestPayload.js +4 -6
- package/dist/shared/wire/LambderUploadObjectFields.d.ts +10 -0
- package/dist/shared/wire/LambderUploadObjectFields.js +24 -0
- package/dist/shared/wire/LambderUploadRefusal.d.ts +9 -0
- package/dist/shared/wire/LambderUploadRefusal.js +18 -0
- package/dist/shared/wire/LambderUploadSchemas.d.ts +12 -0
- package/dist/shared/wire/LambderUploadSchemas.js +30 -0
- 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 +80 -38
- 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/LambderMemoryUploadBucket.d.ts +99 -0
- package/dist/stores/LambderMemoryUploadBucket.js +219 -0
- package/dist/stores/LambderS3FileSource.d.ts +21 -6
- package/dist/stores/LambderS3FileSource.js +12 -7
- package/dist/stores/LambderS3UploadBucket.d.ts +73 -0
- package/dist/stores/LambderS3UploadBucket.js +144 -0
- package/dist/stores/LambderSdkInstallHint.d.ts +11 -0
- package/dist/stores/LambderSdkInstallHint.js +14 -0
- package/dist/testing/LambderTestApp.d.ts +23 -25
- package/dist/testing/LambderTestApp.js +22 -24
- package/dist/testing/LambderTestVisitor.d.ts +10 -12
- package/dist/testing/LambderTestVisitor.js +15 -15
- package/dist/testing.d.ts +3 -0
- package/dist/testing.js +2 -0
- package/package.json +26 -3
- package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
- package/dist/api/LambderApiPolicyEngine.js +0 -85
- package/dist/shared/util/LambderKeyFields.d.ts +0 -32
- package/dist/shared/util/LambderKeyFields.js +0 -34
|
@@ -1,48 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A Map whose entries
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* second.
|
|
2
|
+
* A Map whose entries expire, which every in-memory store here needs:
|
|
3
|
+
* idempotency records, rate-limit counters and session records all key
|
|
4
|
+
* something by a string and all stop mattering at a known second.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
* only when
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* runs, and an idempotency record for a key that never returns is dead weight
|
|
12
|
-
* for ever. So expiry happens on read AND on an amortized sweep, and no
|
|
13
|
-
* caller has to remember either.
|
|
6
|
+
* Entries expire on read and on an amortized sweep, so no caller has to
|
|
7
|
+
* remember either. Expiring only when the same key is read again would leak
|
|
8
|
+
* in anything long-lived: a counter nobody asks about again, or a record for
|
|
9
|
+
* a key that never returns, would stay for as long as the process runs.
|
|
14
10
|
*
|
|
15
|
-
* Expiry
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* idempotency records, so a flood of short-lived keys could reset a monthly
|
|
25
|
-
* cap. Evicting the soonest-expiring entry costs the caller the least that
|
|
26
|
-
* can be taken, and a flood evicts mostly itself.
|
|
11
|
+
* Expiry bounds how long an entry lives, not how many there are: a workload
|
|
12
|
+
* that never repeats a key (a per-IP counter with a monthly window, an
|
|
13
|
+
* idempotency key per request) accumulates live entries faster than expiry
|
|
14
|
+
* retires them. So the map also holds a ceiling, where it evicts the
|
|
15
|
+
* evictable entries that expire soonest. Insertion order would instead
|
|
16
|
+
* retire the entries with the most life left (long-window counters, day-long
|
|
17
|
+
* idempotency records), letting a flood of short-lived keys reset a monthly
|
|
18
|
+
* cap; soonest-to-expire costs the caller least, and a flood evicts mostly
|
|
19
|
+
* itself.
|
|
27
20
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* chosen, and a caller whose write cannot be made room for is told so
|
|
34
|
-
* (LambderExpiringMapFullError) rather than quietly costing somebody else
|
|
21
|
+
* An idempotency claim lives minutes while the record it settles into lives
|
|
22
|
+
* a day, so at the ceiling it would always be the first victim and two
|
|
23
|
+
* concurrent retries would both execute. An entry written with `evictable:
|
|
24
|
+
* false` is never chosen, and a write that cannot be made room for is
|
|
25
|
+
* refused (LambderExpiringMapFullError) rather than costing somebody else
|
|
35
26
|
* their claim.
|
|
36
27
|
*
|
|
37
|
-
* Times are epoch
|
|
38
|
-
*
|
|
28
|
+
* Times are epoch seconds, matching DynamoDB's TTL attribute, so the memory
|
|
29
|
+
* stores and their DynamoDB counterparts say the same thing.
|
|
39
30
|
*/
|
|
40
31
|
/**
|
|
41
32
|
* Thrown by set() when the map is at its ceiling and every entry it holds is
|
|
42
33
|
* protected from eviction. The write did not happen and nothing was dropped
|
|
43
|
-
* to make room for it: the caller decides what to do about a store
|
|
44
|
-
*
|
|
45
|
-
* something this map will trade away.
|
|
34
|
+
* to make room for it: the caller decides what to do about a store full of
|
|
35
|
+
* live claims, and a claim somebody else holds is never traded away.
|
|
46
36
|
*/
|
|
47
37
|
export declare class LambderExpiringMapFullError extends Error {
|
|
48
38
|
constructor(maxEntries: number);
|
|
@@ -97,22 +87,18 @@ export declare class LambderExpiringMap<TValue> {
|
|
|
97
87
|
* Drops a batch of the evictable entries closest to expiring, taking the
|
|
98
88
|
* map from its ceiling down to a floor one batch below it. Reached only
|
|
99
89
|
* when expiry cannot keep up, which means the keys are not repeating.
|
|
100
|
-
* Evicting costs the caller whatever the entry
|
|
101
|
-
* resets, a stored answer re-executes on retry), so the
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* short-lived keys cannot evict a long-window counter.
|
|
90
|
+
* Evicting costs the caller whatever the entry protected (a counter
|
|
91
|
+
* resets, a stored answer re-executes on retry), so the entries taken are
|
|
92
|
+
* those with the least life left, and a flood of short-lived keys cannot
|
|
93
|
+
* evict a long-window counter.
|
|
105
94
|
*
|
|
106
|
-
* A batch rather than
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* 100,000 entries: 0.56 ms per write one at a time, 0.007 ms per write in
|
|
111
|
-
* batches of one percent.
|
|
95
|
+
* A batch rather than one entry, because one leaves the map exactly at
|
|
96
|
+
* its ceiling and the next write pays for another pass: one pass per
|
|
97
|
+
* write for as long as the flood lasts. At 100,000 entries that is 0.56
|
|
98
|
+
* ms per write, against 0.007 ms with batches of one percent.
|
|
112
99
|
*
|
|
113
|
-
* The pass is linear and the sort
|
|
114
|
-
*
|
|
115
|
-
* pathological.
|
|
100
|
+
* The pass is linear and the sort covers only evictable entries, which is
|
|
101
|
+
* affordable because reaching the ceiling at all is already pathological.
|
|
116
102
|
*/
|
|
117
103
|
private evictBatch;
|
|
118
104
|
private sweep;
|
|
@@ -1,65 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A Map whose entries
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* second.
|
|
2
|
+
* A Map whose entries expire, which every in-memory store here needs:
|
|
3
|
+
* idempotency records, rate-limit counters and session records all key
|
|
4
|
+
* something by a string and all stop mattering at a known second.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
* only when
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* runs, and an idempotency record for a key that never returns is dead weight
|
|
12
|
-
* for ever. So expiry happens on read AND on an amortized sweep, and no
|
|
13
|
-
* caller has to remember either.
|
|
6
|
+
* Entries expire on read and on an amortized sweep, so no caller has to
|
|
7
|
+
* remember either. Expiring only when the same key is read again would leak
|
|
8
|
+
* in anything long-lived: a counter nobody asks about again, or a record for
|
|
9
|
+
* a key that never returns, would stay for as long as the process runs.
|
|
14
10
|
*
|
|
15
|
-
* Expiry
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* idempotency records, so a flood of short-lived keys could reset a monthly
|
|
25
|
-
* cap. Evicting the soonest-expiring entry costs the caller the least that
|
|
26
|
-
* can be taken, and a flood evicts mostly itself.
|
|
11
|
+
* Expiry bounds how long an entry lives, not how many there are: a workload
|
|
12
|
+
* that never repeats a key (a per-IP counter with a monthly window, an
|
|
13
|
+
* idempotency key per request) accumulates live entries faster than expiry
|
|
14
|
+
* retires them. So the map also holds a ceiling, where it evicts the
|
|
15
|
+
* evictable entries that expire soonest. Insertion order would instead
|
|
16
|
+
* retire the entries with the most life left (long-window counters, day-long
|
|
17
|
+
* idempotency records), letting a flood of short-lived keys reset a monthly
|
|
18
|
+
* cap; soonest-to-expire costs the caller least, and a flood evicts mostly
|
|
19
|
+
* itself.
|
|
27
20
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* chosen, and a caller whose write cannot be made room for is told so
|
|
34
|
-
* (LambderExpiringMapFullError) rather than quietly costing somebody else
|
|
21
|
+
* An idempotency claim lives minutes while the record it settles into lives
|
|
22
|
+
* a day, so at the ceiling it would always be the first victim and two
|
|
23
|
+
* concurrent retries would both execute. An entry written with `evictable:
|
|
24
|
+
* false` is never chosen, and a write that cannot be made room for is
|
|
25
|
+
* refused (LambderExpiringMapFullError) rather than costing somebody else
|
|
35
26
|
* their claim.
|
|
36
27
|
*
|
|
37
|
-
* Times are epoch
|
|
38
|
-
*
|
|
28
|
+
* Times are epoch seconds, matching DynamoDB's TTL attribute, so the memory
|
|
29
|
+
* stores and their DynamoDB counterparts say the same thing.
|
|
39
30
|
*/
|
|
40
31
|
import { assertPositiveInteger } from "./LambderOptionChecks.js";
|
|
41
32
|
/** How many writes go by before the map walks itself and drops what has expired. */
|
|
42
33
|
const SWEEP_WRITE_INTERVAL = 256;
|
|
43
34
|
/**
|
|
44
|
-
* Live entries held before
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
35
|
+
* Live entries held before eviction starts. High enough that no ordinary
|
|
36
|
+
* single-process run reaches it, low enough to bound the process: crossing it
|
|
37
|
+
* means a key space that never repeats, where the alternative to evicting is
|
|
38
|
+
* growing until the process dies.
|
|
48
39
|
*/
|
|
49
40
|
const DEFAULT_MAX_ENTRIES = 100_000;
|
|
50
41
|
/**
|
|
51
|
-
* Share of the ceiling one eviction pass reclaims
|
|
52
|
-
*
|
|
53
|
-
* the map pays a full pass per write for as long as the flood lasts; taking a
|
|
54
|
-
* batch amortizes that pass over the writes the headroom absorbs.
|
|
42
|
+
* Share of the ceiling one eviction pass reclaims, so the pass is amortized
|
|
43
|
+
* over the writes the headroom absorbs (see evictBatch).
|
|
55
44
|
*/
|
|
56
45
|
const EVICTION_BATCH_SHARE = 0.01;
|
|
57
46
|
/**
|
|
58
47
|
* Thrown by set() when the map is at its ceiling and every entry it holds is
|
|
59
48
|
* protected from eviction. The write did not happen and nothing was dropped
|
|
60
|
-
* to make room for it: the caller decides what to do about a store
|
|
61
|
-
*
|
|
62
|
-
* something this map will trade away.
|
|
49
|
+
* to make room for it: the caller decides what to do about a store full of
|
|
50
|
+
* live claims, and a claim somebody else holds is never traded away.
|
|
63
51
|
*/
|
|
64
52
|
export class LambderExpiringMapFullError extends Error {
|
|
65
53
|
constructor(maxEntries) {
|
|
@@ -160,22 +148,18 @@ export class LambderExpiringMap {
|
|
|
160
148
|
* Drops a batch of the evictable entries closest to expiring, taking the
|
|
161
149
|
* map from its ceiling down to a floor one batch below it. Reached only
|
|
162
150
|
* when expiry cannot keep up, which means the keys are not repeating.
|
|
163
|
-
* Evicting costs the caller whatever the entry
|
|
164
|
-
* resets, a stored answer re-executes on retry), so the
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* short-lived keys cannot evict a long-window counter.
|
|
151
|
+
* Evicting costs the caller whatever the entry protected (a counter
|
|
152
|
+
* resets, a stored answer re-executes on retry), so the entries taken are
|
|
153
|
+
* those with the least life left, and a flood of short-lived keys cannot
|
|
154
|
+
* evict a long-window counter.
|
|
168
155
|
*
|
|
169
|
-
* A batch rather than
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* 100,000 entries: 0.56 ms per write one at a time, 0.007 ms per write in
|
|
174
|
-
* batches of one percent.
|
|
156
|
+
* A batch rather than one entry, because one leaves the map exactly at
|
|
157
|
+
* its ceiling and the next write pays for another pass: one pass per
|
|
158
|
+
* write for as long as the flood lasts. At 100,000 entries that is 0.56
|
|
159
|
+
* ms per write, against 0.007 ms with batches of one percent.
|
|
175
160
|
*
|
|
176
|
-
* The pass is linear and the sort
|
|
177
|
-
*
|
|
178
|
-
* pathological.
|
|
161
|
+
* The pass is linear and the sort covers only evictable entries, which is
|
|
162
|
+
* affordable because reaching the ceiling at all is already pathological.
|
|
179
163
|
*/
|
|
180
164
|
evictBatch(justWritten) {
|
|
181
165
|
const floorSize = Math.max(0, this.maxEntries - this.evictionBatchSize);
|
|
@@ -8,14 +8,13 @@
|
|
|
8
8
|
*
|
|
9
9
|
* A failed import is only half of "no such thing". The other half is a
|
|
10
10
|
* bundler: package.json maps fs, path, zlib and crypto to `false` for the
|
|
11
|
-
* browser, and webpack, Vite and esbuild
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* module exports; a module that cannot answer it is not the module.
|
|
11
|
+
* browser, and webpack, Vite and esbuild resolve such an import to a stub
|
|
12
|
+
* object rather than rejecting it, so a truthiness test would call the stub
|
|
13
|
+
* usable and the caller would die on its first real call. `expect` names a
|
|
14
|
+
* function the genuine module exports; a module without it is not the module.
|
|
16
15
|
*
|
|
17
|
-
* The answer
|
|
18
|
-
*
|
|
16
|
+
* The answer, absence included, is memoized, so the probe runs once per
|
|
17
|
+
* module.
|
|
19
18
|
*/
|
|
20
19
|
const loadNodeModule = (load, expect) => {
|
|
21
20
|
let pending = null;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The checks every option that names a count, a size or a duration goes
|
|
3
|
-
* through at creation, so a bad value
|
|
4
|
-
* everywhere
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* through at creation, so a bad value meets one predicate and one wording
|
|
4
|
+
* everywhere. `name` is the option as the reader wrote it
|
|
5
|
+
* (`maxResponseBytes`, `session.ttlSeconds`), so the error says which one to
|
|
6
|
+
* fix.
|
|
7
7
|
*/
|
|
8
8
|
/** A safe integer of one or more; returns it so the check reads as an assignment. */
|
|
9
9
|
export declare const assertPositiveInteger: (value: unknown, name: string) => number;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The checks every option that names a count, a size or a duration goes
|
|
3
|
-
* through at creation, so a bad value
|
|
4
|
-
* everywhere
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* through at creation, so a bad value meets one predicate and one wording
|
|
4
|
+
* everywhere. `name` is the option as the reader wrote it
|
|
5
|
+
* (`maxResponseBytes`, `session.ttlSeconds`), so the error says which one to
|
|
6
|
+
* fix.
|
|
7
7
|
*/
|
|
8
8
|
const describe = (value) => typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
9
9
|
/** A safe integer of one or more; returns it so the check reads as an assignment. */
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Marks an object as a response without anyone having to import the class to
|
|
3
3
|
* ask. Layers that must recognise one but must not depend on core at runtime
|
|
4
4
|
* (the guards engine, which runs in the browser too) test for this key.
|
|
5
|
-
* Symbol.for keeps it true across realms and
|
|
6
|
-
* package. Defined
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Symbol.for keeps it true across realms and duplicate copies of the
|
|
6
|
+
* package. Defined in shared so the class that carries the brand and the
|
|
7
|
+
* engine that checks it import one constant: a hand-typed copy of the
|
|
8
|
+
* symbol's name would keep compiling after a rename while the runtime check
|
|
9
|
+
* silently stopped matching.
|
|
10
10
|
*/
|
|
11
11
|
export declare const LAMBDER_RESPONSE_BRAND: unique symbol;
|
|
12
12
|
/**
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Marks an object as a response without anyone having to import the class to
|
|
3
3
|
* ask. Layers that must recognise one but must not depend on core at runtime
|
|
4
4
|
* (the guards engine, which runs in the browser too) test for this key.
|
|
5
|
-
* Symbol.for keeps it true across realms and
|
|
6
|
-
* package. Defined
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Symbol.for keeps it true across realms and duplicate copies of the
|
|
6
|
+
* package. Defined in shared so the class that carries the brand and the
|
|
7
|
+
* engine that checks it import one constant: a hand-typed copy of the
|
|
8
|
+
* symbol's name would keep compiling after a rename while the runtime check
|
|
9
|
+
* silently stopped matching.
|
|
10
10
|
*/
|
|
11
11
|
export const LAMBDER_RESPONSE_BRAND = Symbol.for("lambder.response");
|
|
12
12
|
/**
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SHA-256
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* SHA-256 through WebCrypto: the one digest every layer shares. The session
|
|
3
|
+
* crypto hashes bearer secrets with it and the rate-limit engine folds an
|
|
4
|
+
* over-long tracker key with it, so both key spaces are built from the same
|
|
5
|
+
* primitive on every runtime (browsers on a secure context, Node 20+, edge
|
|
6
|
+
* runtimes); an upload's checksum is the same digest over the file's bytes.
|
|
7
7
|
*/
|
|
8
8
|
/** Lowercase hex of a byte array, two characters per byte. */
|
|
9
9
|
export declare const bytesToHexString: (bytes: Uint8Array) => string;
|
|
@@ -15,3 +15,5 @@ export declare const bytesToHexString: (bytes: Uint8Array) => string;
|
|
|
15
15
|
export declare const resolveWebCrypto: () => Promise<Crypto>;
|
|
16
16
|
/** The SHA-256 digest of `text` (UTF-8), as 64 lowercase hex characters. */
|
|
17
17
|
export declare const sha256HexOf: (text: string) => Promise<string>;
|
|
18
|
+
/** The SHA-256 digest of `bytes`, as base64: the form object storage checks an upload's checksum in. */
|
|
19
|
+
export declare const sha256Base64Of: (bytes: Uint8Array) => Promise<string>;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { bytesToBase64 } from "./LambderBase64.js";
|
|
1
2
|
import { getCrypto } from "./LambderNodeModules.js";
|
|
2
3
|
/**
|
|
3
|
-
* SHA-256
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* SHA-256 through WebCrypto: the one digest every layer shares. The session
|
|
5
|
+
* crypto hashes bearer secrets with it and the rate-limit engine folds an
|
|
6
|
+
* over-long tracker key with it, so both key spaces are built from the same
|
|
7
|
+
* primitive on every runtime (browsers on a secure context, Node 20+, edge
|
|
8
|
+
* runtimes); an upload's checksum is the same digest over the file's bytes.
|
|
8
9
|
*/
|
|
9
10
|
/** Lowercase hex of a byte array, two characters per byte. */
|
|
10
11
|
export const bytesToHexString = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
@@ -32,3 +33,8 @@ export const sha256HexOf = async (text) => {
|
|
|
32
33
|
const digest = await webCrypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
33
34
|
return bytesToHexString(new Uint8Array(digest));
|
|
34
35
|
};
|
|
36
|
+
/** The SHA-256 digest of `bytes`, as base64: the form object storage checks an upload's checksum in. */
|
|
37
|
+
export const sha256Base64Of = async (bytes) => {
|
|
38
|
+
const webCrypto = await resolveWebCrypto();
|
|
39
|
+
return bytesToBase64(new Uint8Array(await webCrypto.subtle.digest("SHA-256", bytes)));
|
|
40
|
+
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The small type utilities more than one module needs.
|
|
3
3
|
*
|
|
4
|
-
* Nothing here is Lambder's own vocabulary: these are
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Nothing here is Lambder's own vocabulary: these are shapes TypeScript does
|
|
5
|
+
* not ship, written once so they cannot drift in name or meaning across the
|
|
6
|
+
* modules that use them.
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
9
|
* A value a caller may hand back either synchronously or as a promise. Every
|
|
@@ -18,15 +18,14 @@ export type MaybePromise<T> = T | Promise<T>;
|
|
|
18
18
|
*
|
|
19
19
|
* An all-optional map is inhabited by `{}`, which would let `guards: {}`
|
|
20
20
|
* satisfy requireSessionApiGuards / requirePublicApiGuards at the type level
|
|
21
|
-
* while declaring no guard
|
|
21
|
+
* while declaring no guard: the option is present, so the required-field
|
|
22
22
|
* check passes, and it normalizes to zero entries, so nothing runs. Requiring
|
|
23
23
|
* the chosen key also rejects `{ theGuard: undefined }`, which an optional
|
|
24
|
-
* property accepts and which would
|
|
24
|
+
* property accepts and which would reach the guard's handler with an
|
|
25
25
|
* undefined param.
|
|
26
26
|
*
|
|
27
|
-
* Option-neutral
|
|
28
|
-
*
|
|
29
|
-
* did not.
|
|
27
|
+
* Option-neutral: the rate-limit option's map form uses it too, so
|
|
28
|
+
* `rateLimit: {}` and `guards: {}` are refused alike.
|
|
30
29
|
*/
|
|
31
30
|
export type LambderNonEmptyOptionMap<TMap> = {
|
|
32
31
|
[K in keyof TMap]-?: Required<Pick<TMap, K>> & Omit<TMap, K>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The small type utilities more than one module needs.
|
|
3
3
|
*
|
|
4
|
-
* Nothing here is Lambder's own vocabulary: these are
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Nothing here is Lambder's own vocabulary: these are shapes TypeScript does
|
|
5
|
+
* not ship, written once so they cannot drift in name or meaning across the
|
|
6
|
+
* modules that use them.
|
|
7
7
|
*/
|
|
8
8
|
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounding the caller-supplied field of a tracker or scope key.
|
|
3
|
+
*
|
|
4
|
+
* The rate-limit engine and the idempotency engine each build a store key
|
|
5
|
+
* around a field only the caller controls: a custom rate-limit key or a
|
|
6
|
+
* session key, a callerIdentity or a session key. A store has a key limit of
|
|
7
|
+
* its own (a DynamoDB partition key stops at 2048 bytes) and refuses a key
|
|
8
|
+
* past it by throwing, and both engines fail open on a store throw by
|
|
9
|
+
* default: a long enough field (a 3,000-character email, a device token)
|
|
10
|
+
* would turn the rate limit or the idempotency off for that caller, in
|
|
11
|
+
* silence, while the table held the field in plain text. So the field is
|
|
12
|
+
* bounded before any store sees it, in one implementation the two engines
|
|
13
|
+
* share, so they cannot drift apart.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The field bounded: `<kind>:<value>` while the value fits, `<kind>:h:<sha256
|
|
17
|
+
* hex>` once it does not. The digest keeps distinct callers on distinct
|
|
18
|
+
* counters and scopes, and a value that fits stays readable in the table.
|
|
19
|
+
*/
|
|
20
|
+
export declare const boundKeyField: (kind: string, value: string) => Promise<string>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounding the caller-supplied field of a tracker or scope key.
|
|
3
|
+
*
|
|
4
|
+
* The rate-limit engine and the idempotency engine each build a store key
|
|
5
|
+
* around a field only the caller controls: a custom rate-limit key or a
|
|
6
|
+
* session key, a callerIdentity or a session key. A store has a key limit of
|
|
7
|
+
* its own (a DynamoDB partition key stops at 2048 bytes) and refuses a key
|
|
8
|
+
* past it by throwing, and both engines fail open on a store throw by
|
|
9
|
+
* default: a long enough field (a 3,000-character email, a device token)
|
|
10
|
+
* would turn the rate limit or the idempotency off for that caller, in
|
|
11
|
+
* silence, while the table held the field in plain text. So the field is
|
|
12
|
+
* bounded before any store sees it, in one implementation the two engines
|
|
13
|
+
* share, so they cannot drift apart.
|
|
14
|
+
*/
|
|
15
|
+
import { joinKeyFields } from "./joinKeyFields.js";
|
|
16
|
+
import { sha256HexOf } from "./LambderTextDigest.js";
|
|
17
|
+
/**
|
|
18
|
+
* The ceiling, in UTF-8 bytes, on the field as it is written into the key;
|
|
19
|
+
* past it, the field is replaced by its digest. Measured escaped, since
|
|
20
|
+
* joinKeyFields doubles every separator and escape character: a field of
|
|
21
|
+
* 1,000 separators is 2,000 bytes in the key. 1024 sits well inside every
|
|
22
|
+
* store's key limit, with the store's prefix and the engine's other fields
|
|
23
|
+
* (API and policy names, a posted idempotency key of at most 200 characters)
|
|
24
|
+
* joined around it.
|
|
25
|
+
*/
|
|
26
|
+
const MAX_KEY_FIELD_BYTES = 1024;
|
|
27
|
+
/**
|
|
28
|
+
* The field bounded: `<kind>:<value>` while the value fits, `<kind>:h:<sha256
|
|
29
|
+
* hex>` once it does not. The digest keeps distinct callers on distinct
|
|
30
|
+
* counters and scopes, and a value that fits stays readable in the table.
|
|
31
|
+
*/
|
|
32
|
+
export const boundKeyField = async (kind, value) => new TextEncoder().encode(joinKeyFields(value)).length > MAX_KEY_FIELD_BYTES
|
|
33
|
+
? `${kind}:h:${await sha256HexOf(value)}`
|
|
34
|
+
: `${kind}:${value}`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON with object keys sorted at every level, so two values that are the
|
|
3
|
+
* same data hash the same whatever order their keys were built in. Arrays
|
|
4
|
+
* keep their order: a tuple's positions and an enum's values are part of the
|
|
5
|
+
* data. Undefined entries are dropped, as JSON.stringify would drop them.
|
|
6
|
+
*
|
|
7
|
+
* What the API signature digests a schema's description with, and what the
|
|
8
|
+
* idempotency engine fingerprints a request with: two hashes that must not
|
|
9
|
+
* depend on the order a client or a builder happened to write keys in.
|
|
10
|
+
*/
|
|
11
|
+
export declare const canonicalJson: (value: unknown) => string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON with object keys sorted at every level, so two values that are the
|
|
3
|
+
* same data hash the same whatever order their keys were built in. Arrays
|
|
4
|
+
* keep their order: a tuple's positions and an enum's values are part of the
|
|
5
|
+
* data. Undefined entries are dropped, as JSON.stringify would drop them.
|
|
6
|
+
*
|
|
7
|
+
* What the API signature digests a schema's description with, and what the
|
|
8
|
+
* idempotency engine fingerprints a request with: two hashes that must not
|
|
9
|
+
* depend on the order a client or a builder happened to write keys in.
|
|
10
|
+
*/
|
|
11
|
+
export const canonicalJson = (value) => JSON.stringify(sortKeys(value));
|
|
12
|
+
const sortKeys = (value) => {
|
|
13
|
+
if (Array.isArray(value))
|
|
14
|
+
return value.map(sortKeys);
|
|
15
|
+
if (value === null || typeof value !== "object")
|
|
16
|
+
return value;
|
|
17
|
+
const source = value;
|
|
18
|
+
// No prototype, so a "__proto__" key (JSON.parse makes it an own key) is
|
|
19
|
+
// kept as data. On a plain object the assignment would set the prototype
|
|
20
|
+
// instead, and two payloads differing only under "__proto__" would share
|
|
21
|
+
// one fingerprint.
|
|
22
|
+
const sorted = Object.create(null);
|
|
23
|
+
for (const key of Object.keys(source).sort()) {
|
|
24
|
+
if (source[key] !== undefined)
|
|
25
|
+
sorted[key] = sortKeys(source[key]);
|
|
26
|
+
}
|
|
27
|
+
return sorted;
|
|
28
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Joining the fields of a tracker or scope key.
|
|
3
|
+
*
|
|
4
|
+
* The rate limiter and the idempotency engine each join fields with a
|
|
5
|
+
* separator, and at least one field is caller data (a policy's rate-limit
|
|
6
|
+
* key, a posted idempotency key). A plain join lets two field lists produce
|
|
7
|
+
* one string, so two callers would share a counter or one would read
|
|
8
|
+
* another's stored answer. The caller's separator is escaped, not refused: a
|
|
9
|
+
* limit that rejects a legal key is a bug of its own.
|
|
10
|
+
*
|
|
11
|
+
* The join is one-way (a key is looked up or compared, never taken apart),
|
|
12
|
+
* unlike LambderDdbCache's reversible sort-key escape, which listSortKeys
|
|
13
|
+
* must decode to exactly what was written; neither stands in for the other.
|
|
14
|
+
* Both engines share this one implementation so they cannot drift apart.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The fields joined into one key, each escaped, so no two distinct field
|
|
18
|
+
* lists can produce the same string.
|
|
19
|
+
*/
|
|
20
|
+
export declare const joinKeyFields: (...fields: string[]) => string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Joining the fields of a tracker or scope key.
|
|
3
|
+
*
|
|
4
|
+
* The rate limiter and the idempotency engine each join fields with a
|
|
5
|
+
* separator, and at least one field is caller data (a policy's rate-limit
|
|
6
|
+
* key, a posted idempotency key). A plain join lets two field lists produce
|
|
7
|
+
* one string, so two callers would share a counter or one would read
|
|
8
|
+
* another's stored answer. The caller's separator is escaped, not refused: a
|
|
9
|
+
* limit that rejects a legal key is a bug of its own.
|
|
10
|
+
*
|
|
11
|
+
* The join is one-way (a key is looked up or compared, never taken apart),
|
|
12
|
+
* unlike LambderDdbCache's reversible sort-key escape, which listSortKeys
|
|
13
|
+
* must decode to exactly what was written; neither stands in for the other.
|
|
14
|
+
* Both engines share this one implementation so they cannot drift apart.
|
|
15
|
+
*/
|
|
16
|
+
/** One field, with the separator and its own escape character escaped. */
|
|
17
|
+
const escapeKeyField = (value) => value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
18
|
+
/**
|
|
19
|
+
* The fields joined into one key, each escaped, so no two distinct field
|
|
20
|
+
* lists can produce the same string.
|
|
21
|
+
*/
|
|
22
|
+
export const joinKeyFields = (...fields) => fields.map(escapeKeyField).join("|");
|