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.
Files changed (209) hide show
  1. package/CHANGELOG.md +1021 -3
  2. package/README.md +43 -21
  3. package/dist/api/LambderApiAnswer.d.ts +18 -22
  4. package/dist/api/LambderApiAnswer.js +6 -7
  5. package/dist/api/LambderApiCallContext.d.ts +21 -8
  6. package/dist/api/LambderApiCallContext.js +22 -4
  7. package/dist/api/LambderApiDefinition.d.ts +4 -3
  8. package/dist/api/LambderApiEnvelope.d.ts +14 -9
  9. package/dist/api/LambderApiEnvelope.js +33 -34
  10. package/dist/api/LambderApiGuards.d.ts +78 -51
  11. package/dist/api/LambderApiGuards.js +34 -36
  12. package/dist/api/LambderApiIdempotency.d.ts +74 -61
  13. package/dist/api/LambderApiIdempotency.js +226 -151
  14. package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
  15. package/dist/api/LambderApiOutputValidationError.js +50 -0
  16. package/dist/api/LambderApiPipeline.d.ts +77 -39
  17. package/dist/api/LambderApiPipeline.js +135 -62
  18. package/dist/api/LambderApiRateLimits.d.ts +208 -54
  19. package/dist/api/LambderApiRateLimits.js +197 -108
  20. package/dist/api/LambderApiRequest.d.ts +27 -21
  21. package/dist/api/LambderApiRequest.js +26 -19
  22. package/dist/api/LambderApiSignature.d.ts +12 -15
  23. package/dist/api/LambderApiSignature.js +28 -51
  24. package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
  25. package/dist/api/LambderApiValidationRefusal.js +10 -10
  26. package/dist/build/freshProcessVerifier.d.ts +13 -0
  27. package/dist/build/freshProcessVerifier.js +19 -0
  28. package/dist/build/writeApiSignatures.d.ts +109 -0
  29. package/dist/build/writeApiSignatures.js +222 -0
  30. package/dist/build.d.ts +9 -0
  31. package/dist/build.js +8 -0
  32. package/dist/client/LambderCaller.d.ts +13 -44
  33. package/dist/client/LambderCaller.js +77 -84
  34. package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
  35. package/dist/client/LambderReloadLoopBreaker.js +90 -46
  36. package/dist/client/lambderFetchTransport.d.ts +4 -1
  37. package/dist/client/lambderFetchTransport.js +52 -28
  38. package/dist/client.d.ts +5 -3
  39. package/dist/client.js +2 -1
  40. package/dist/core/Lambder.d.ts +161 -69
  41. package/dist/core/Lambder.js +370 -226
  42. package/dist/core/LambderContext.d.ts +82 -15
  43. package/dist/core/LambderContext.js +107 -20
  44. package/dist/core/LambderCors.d.ts +21 -3
  45. package/dist/core/LambderCors.js +35 -16
  46. package/dist/core/LambderCrashHandling.d.ts +40 -0
  47. package/dist/core/LambderCrashHandling.js +97 -0
  48. package/dist/core/LambderCreateOptions.d.ts +151 -75
  49. package/dist/core/LambderCreateOptions.js +16 -23
  50. package/dist/core/LambderFiles.d.ts +28 -7
  51. package/dist/core/LambderFiles.js +73 -33
  52. package/dist/core/LambderIndexHtml.js +12 -11
  53. package/dist/core/LambderPolicyBuilders.d.ts +17 -5
  54. package/dist/core/LambderPolicyBuilders.js +17 -5
  55. package/dist/core/LambderPublicFiles.d.ts +11 -5
  56. package/dist/core/LambderPublicFiles.js +32 -4
  57. package/dist/core/LambderRequestPath.d.ts +43 -0
  58. package/dist/core/LambderRequestPath.js +63 -0
  59. package/dist/core/LambderResponse.d.ts +26 -5
  60. package/dist/core/LambderResponse.js +157 -70
  61. package/dist/core/LambderResponseBuilder.d.ts +49 -4
  62. package/dist/core/LambderResponseBuilder.js +64 -3
  63. package/dist/core/LambderRouting.d.ts +2 -3
  64. package/dist/core/LambderRouting.js +22 -7
  65. package/dist/core/LambderTemplatingEngine.js +211 -32
  66. package/dist/index.d.ts +15 -8
  67. package/dist/index.js +5 -4
  68. package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
  69. package/dist/invoke/LambderInvokeCaller.js +76 -66
  70. package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
  71. package/dist/invoke/LambderInvokeOutcome.js +9 -22
  72. package/dist/invoke/LambderLambdaEvent.d.ts +44 -10
  73. package/dist/invoke/LambderLambdaEvent.js +80 -37
  74. package/dist/invoke/lambderHandlerTransport.d.ts +12 -10
  75. package/dist/invoke/lambderHandlerTransport.js +16 -19
  76. package/dist/mock/LambderMockApp.d.ts +67 -83
  77. package/dist/mock/LambderMockApp.js +167 -153
  78. package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
  79. package/dist/mock/LambderMockBrowserCookies.js +24 -28
  80. package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
  81. package/dist/mock/LambderMockCallRecorder.js +19 -28
  82. package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
  83. package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
  84. package/dist/mock/LambderMockEntryRegistry.js +24 -29
  85. package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
  86. package/dist/mock/LambderMockFailureInjector.js +3 -6
  87. package/dist/mock/LambderMockTypes.d.ts +78 -108
  88. package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
  89. package/dist/mock/lambderMockInvokeTransport.js +11 -10
  90. package/dist/mock/lambderMockMswHandler.d.ts +33 -29
  91. package/dist/mock/lambderMockMswHandler.js +50 -39
  92. package/dist/mock.d.ts +3 -1
  93. package/dist/mock.js +5 -3
  94. package/dist/session/LambderSessionController.d.ts +108 -89
  95. package/dist/session/LambderSessionController.js +187 -168
  96. package/dist/session/LambderSessionCrypto.d.ts +16 -7
  97. package/dist/session/LambderSessionCrypto.js +26 -12
  98. package/dist/session/LambderSessionManager.d.ts +136 -47
  99. package/dist/session/LambderSessionManager.js +280 -139
  100. package/dist/shared/LambderHtml.d.ts +42 -3
  101. package/dist/shared/LambderHtml.js +127 -7
  102. package/dist/shared/LambderHtmlPositions.d.ts +173 -0
  103. package/dist/shared/LambderHtmlPositions.js +652 -0
  104. package/dist/shared/LambderI18n.d.ts +10 -11
  105. package/dist/shared/LambderI18n.js +33 -21
  106. package/dist/shared/contracts/LambderCache.d.ts +66 -0
  107. package/dist/shared/contracts/LambderCache.js +11 -0
  108. package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
  109. package/dist/shared/contracts/LambderFileSource.js +5 -8
  110. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
  111. package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
  112. package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
  113. package/dist/shared/contracts/LambderRateLimiter.js +4 -5
  114. package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
  115. package/dist/shared/contracts/LambderSessionStore.js +5 -6
  116. package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
  117. package/dist/shared/transport/LambderApiTransport.js +7 -7
  118. package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
  119. package/dist/shared/transport/LambderCookieJar.js +54 -66
  120. package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
  121. package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
  122. package/dist/shared/util/LambderCallAbort.d.ts +5 -5
  123. package/dist/shared/util/LambderCallAbort.js +5 -5
  124. package/dist/shared/util/LambderClientIp.d.ts +27 -11
  125. package/dist/shared/util/LambderClientIp.js +96 -13
  126. package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
  127. package/dist/shared/util/LambderExpiringMap.js +41 -57
  128. package/dist/shared/util/LambderNodeModules.js +6 -7
  129. package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
  130. package/dist/shared/util/LambderOptionChecks.js +4 -4
  131. package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
  132. package/dist/shared/util/LambderResponseBrand.js +5 -5
  133. package/dist/shared/util/LambderTestingDoors.d.ts +29 -0
  134. package/dist/shared/util/LambderTestingDoors.js +29 -0
  135. package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
  136. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  137. package/dist/shared/util/boundKeyField.d.ts +20 -0
  138. package/dist/shared/util/boundKeyField.js +34 -0
  139. package/dist/shared/util/canonicalJson.d.ts +11 -0
  140. package/dist/shared/util/canonicalJson.js +28 -0
  141. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  142. package/dist/shared/util/joinKeyFields.js +22 -0
  143. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  144. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  145. package/dist/shared/wire/LambderApiContract.d.ts +107 -32
  146. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  147. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  148. package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
  149. package/dist/shared/wire/LambderApiRefusal.js +36 -7
  150. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  151. package/dist/shared/wire/LambderApiSignature.js +16 -19
  152. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  153. package/dist/shared/wire/LambderCallOptions.js +9 -11
  154. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  155. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  156. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  157. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  158. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  159. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  160. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  161. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  162. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  163. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  164. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  165. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  166. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  167. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +79 -0
  168. package/dist/shared/wire/LambderOutcomeAssertions.js +112 -0
  169. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  170. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  171. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  172. package/dist/stores/LambderCacheFiller.js +119 -0
  173. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  174. package/dist/stores/LambderCacheKeys.js +54 -0
  175. package/dist/stores/LambderCacheValues.d.ts +45 -0
  176. package/dist/stores/LambderCacheValues.js +74 -0
  177. package/dist/stores/LambderDdbCache.d.ts +121 -56
  178. package/dist/stores/LambderDdbCache.js +528 -225
  179. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  180. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  181. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  182. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  183. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  184. package/dist/stores/LambderDdbSdk.js +79 -33
  185. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  186. package/dist/stores/LambderDdbSessionStore.js +119 -47
  187. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  188. package/dist/stores/LambderHttpFileSource.js +15 -13
  189. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  190. package/dist/stores/LambderMemoryCache.js +113 -0
  191. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  192. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  193. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  194. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  195. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  196. package/dist/stores/LambderMemorySessionStore.js +38 -19
  197. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  198. package/dist/stores/LambderS3FileSource.js +12 -7
  199. package/dist/testing/LambderTestApp.d.ts +176 -0
  200. package/dist/testing/LambderTestApp.js +204 -0
  201. package/dist/testing/LambderTestVisitor.d.ts +153 -0
  202. package/dist/testing/LambderTestVisitor.js +154 -0
  203. package/dist/testing.d.ts +27 -0
  204. package/dist/testing.js +24 -0
  205. package/package.json +20 -3
  206. package/dist/api/LambderApiPolicyEngine.d.ts +0 -36
  207. package/dist/api/LambderApiPolicyEngine.js +0 -77
  208. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  209. package/dist/shared/util/LambderKeyFields.js +0 -34
@@ -0,0 +1,146 @@
1
+ import { LAMBDER_REFUSAL_CODES } from "./LambderApiRefusal.js";
2
+ /** The outcome of an attempt that ended before anything was sent: it tried nothing. */
3
+ export const IDEMPOTENT_ATTEMPT_NOT_SENT = { ok: false, reason: "notSent" };
4
+ /**
5
+ * Refusals that answer this request itself, so the same request would get
6
+ * the same answer again and what the person sends next is a new operation.
7
+ */
8
+ const REFUSAL_REASONS = new Set(["validation", "notAuthorized", "errorMessage"]);
9
+ /** Answers that never reached the operation: the same request may pass later. */
10
+ const UNTRIED_REASONS = new Set(["sessionExpired", "versionExpired", "payloadTooLarge", "notSent"]);
11
+ /**
12
+ * Generate an idempotency key for one logical operation. Create it when the
13
+ * operation begins (a form opens, a draft starts), send the same key on every
14
+ * attempt of that operation, and generate a new one after a confirmed
15
+ * success; createIdempotencyKeyScope() does that bookkeeping itself. Uses
16
+ * crypto.randomUUID when available, else a v4 UUID from getRandomValues,
17
+ * because randomUUID only exists in secure contexts (plain-http LAN device
18
+ * testing lacks it).
19
+ *
20
+ * A runtime with neither throws rather than using Math.random: the key
21
+ * scopes the replay record for a logged-out client, so a guessable one hands
22
+ * that client's stored response to whoever guesses it.
23
+ */
24
+ export const createIdempotencyKey = () => {
25
+ const cryptoObj = globalThis.crypto;
26
+ if (cryptoObj?.randomUUID)
27
+ return cryptoObj.randomUUID();
28
+ if (!cryptoObj?.getRandomValues)
29
+ throw new Error("createIdempotencyKey needs crypto.getRandomValues: an idempotency key must be unguessable, and this runtime offers no random source that is.");
30
+ const bytes = new Uint8Array(16);
31
+ cryptoObj.getRandomValues(bytes);
32
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
33
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
34
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
35
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
36
+ };
37
+ /** What a scope's attempts are started through: the class hands it out once, below, so it is reachable from this module alone. */
38
+ let beginAttemptOf;
39
+ /**
40
+ * One logical operation's rotating idempotency key, from
41
+ * createIdempotencyKeyScope(). Every attempt of the operation
42
+ * sends the current key, and the scope moves to a new key once an answer
43
+ * settles the operation:
44
+ *
45
+ * - A success settles it, and so does a key refused as reused for another
46
+ * request, since that key can never carry this one.
47
+ * - A refusal of this request (a rejected input, not authorized, an
48
+ * errorMessage) settles it, unless another attempt under the same key is
49
+ * still in flight or went unanswered. That attempt may run or have run the
50
+ * operation, and guards, validation and rate limits refuse before the
51
+ * replay record is claimed, so the refusal of a retry or a double-tap says
52
+ * nothing about it. Keeping the key lets the next attempt replay the
53
+ * original's answer rather than run again.
54
+ * - A rate limit, an expired session, a stale version, and an attempt that
55
+ * ended before anything was sent keep the key.
56
+ * - Anything that is not an answer (a network failure, a timeout, a 5xx, a
57
+ * crash) keeps the key and marks it as possibly used, and so does a
58
+ * duplicate of an original still in flight, unless the scope has another
59
+ * attempt of its own still waiting for its answer (a double-tap): that
60
+ * attempt is the original, and its answer settles the key, a refusal
61
+ * included.
62
+ *
63
+ * An answer to a key the scope has already moved past changes nothing: a
64
+ * slow original answering after the person moved on must not rotate away
65
+ * the key their current attempt is using.
66
+ */
67
+ export class LambderIdempotencyKeyScope {
68
+ #key = createIdempotencyKey();
69
+ #possiblyUsed = false;
70
+ /** Attempts under the current key that have not settled yet. */
71
+ #inFlight = 0;
72
+ static {
73
+ beginAttemptOf = (scope) => scope.#beginAttempt();
74
+ }
75
+ /** The key for the operation currently in progress. */
76
+ get current() { return this.#key; }
77
+ /** Moves on to a new operation by hand, for a caller that settles operations itself. Returns the new key. */
78
+ rotate() {
79
+ this.#key = createIdempotencyKey();
80
+ this.#possiblyUsed = false;
81
+ this.#inFlight = 0;
82
+ return this.#key;
83
+ }
84
+ /**
85
+ * Starts one attempt under the current key; the callers do this for
86
+ * every call handed the scope. Private, and reached through
87
+ * beginIdempotentAttempt: an attempt started and never settled holds the
88
+ * in-flight count up, so a refusal would stop moving the key.
89
+ */
90
+ #beginAttempt() {
91
+ const key = this.#key;
92
+ this.#inFlight += 1;
93
+ let settled = false;
94
+ return { key, settle: (outcome) => {
95
+ if (settled)
96
+ return;
97
+ settled = true;
98
+ if (key !== this.#key)
99
+ return;
100
+ this.#inFlight -= 1;
101
+ const code = outcome.errorMessage?.code;
102
+ if (outcome.ok || code === LAMBDER_REFUSAL_CODES.idempotencyKeyReused) {
103
+ this.rotate();
104
+ return;
105
+ }
106
+ // A rate limit refuses before the claim, whatever code a policy's
107
+ // own message carries, so its status is what names it.
108
+ if (UNTRIED_REASONS.has(outcome.reason ?? "") || outcome.status === 429 || code === LAMBDER_REFUSAL_CODES.rateLimited)
109
+ return;
110
+ if (REFUSAL_REASONS.has(outcome.reason ?? "") && code !== LAMBDER_REFUSAL_CODES.duplicateInFlight) {
111
+ if (!this.#possiblyUsed && this.#inFlight === 0)
112
+ this.rotate();
113
+ return;
114
+ }
115
+ // A duplicate while another attempt of this scope still waits for
116
+ // its answer: unless an earlier attempt went unanswered (which
117
+ // marked the key already), the running original is that attempt,
118
+ // and its own answer settles the key. Marked here, the key would
119
+ // outlive that answer when it is a refusal, and the corrected
120
+ // request after it would be refused as a reused key.
121
+ if (code === LAMBDER_REFUSAL_CODES.duplicateInFlight && this.#inFlight > 0)
122
+ return;
123
+ // No answer, or an original this scope has no attempt waiting
124
+ // for: the operation may have run under this key.
125
+ this.#possiblyUsed = true;
126
+ } };
127
+ }
128
+ }
129
+ /**
130
+ * A self-rotating idempotency key for a component or form that performs the
131
+ * same logical operation repeatedly. Pass the scope itself as the call's
132
+ * `idempotencyKey`, on LambderCaller or LambderInvokeCaller: every attempt of
133
+ * one operation (a retry after a dropped connection, a double-tap) sends its
134
+ * current key, so the server collapses them, and the caller rotates it once
135
+ * an answer settles the operation (a success, or a refusal of this request),
136
+ * so the next attempt, a corrected form included, is a new operation.
137
+ * LambderIdempotencyKeyScope says which answers settle it.
138
+ *
139
+ * ```typescript
140
+ * const submitKey = createIdempotencyKeyScope();
141
+ * await caller.api("order.create", payload, { idempotencyKey: submitKey });
142
+ * ```
143
+ */
144
+ export const createIdempotencyKeyScope = () => new LambderIdempotencyKeyScope();
145
+ /** The attempt a call makes with its idempotencyKey option: a scope's current key, or a plain key with nothing to settle. */
146
+ export const beginIdempotentAttempt = (option) => option instanceof LambderIdempotencyKeyScope ? beginAttemptOf(option) : { key: option, settle: () => { } };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The `requestContext.apiId` of an event LambderInvokeCaller synthesizes, and
3
+ * what the server reads to tell a direct invoke from a gateway's request.
4
+ *
5
+ * A gateway writes its own id there: API Gateway and a Function URL both
6
+ * generate theirs as lowercase letters and digits, so no request through one
7
+ * arrives carrying this hyphenated value, whatever headers it sends. Only a
8
+ * direct invoke delivers an event whose sender wrote the id, and a direct
9
+ * invoke is authorized by IAM. Declared in shared because the invoke caller
10
+ * writes it and the server reads it, and the two must agree byte for byte.
11
+ */
12
+ export declare const LAMBDER_INVOKE_API_ID = "lambder-invoke";
13
+ /**
14
+ * The `requestContext.apiId` of a browser-shaped event Lambder synthesizes
15
+ * (lambder/testing, lambderHandlerTransport): a request standing for one a
16
+ * gateway delivered, not an invoke. Hyphenated like LAMBDER_INVOKE_API_ID,
17
+ * so no gateway's event carries it either.
18
+ *
19
+ * The server reads it, like the invoke id, as "Lambder's own event builder
20
+ * wrote this": that builder always delivers a 2.0 event's path decoded, so
21
+ * the path is not decoded a second time whatever host the request names, a
22
+ * Function URL's included. A direct invoker that writes it gains nothing: it
23
+ * writes the whole event, the path included, either way. Declared beside the
24
+ * invoke id for the same reason: the builder writes it and the server reads
25
+ * it.
26
+ */
27
+ export declare const LAMBDER_LOCAL_API_ID = "lambder-local";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The `requestContext.apiId` of an event LambderInvokeCaller synthesizes, and
3
+ * what the server reads to tell a direct invoke from a gateway's request.
4
+ *
5
+ * A gateway writes its own id there: API Gateway and a Function URL both
6
+ * generate theirs as lowercase letters and digits, so no request through one
7
+ * arrives carrying this hyphenated value, whatever headers it sends. Only a
8
+ * direct invoke delivers an event whose sender wrote the id, and a direct
9
+ * invoke is authorized by IAM. Declared in shared because the invoke caller
10
+ * writes it and the server reads it, and the two must agree byte for byte.
11
+ */
12
+ export const LAMBDER_INVOKE_API_ID = "lambder-invoke";
13
+ /**
14
+ * The `requestContext.apiId` of a browser-shaped event Lambder synthesizes
15
+ * (lambder/testing, lambderHandlerTransport): a request standing for one a
16
+ * gateway delivered, not an invoke. Hyphenated like LAMBDER_INVOKE_API_ID,
17
+ * so no gateway's event carries it either.
18
+ *
19
+ * The server reads it, like the invoke id, as "Lambder's own event builder
20
+ * wrote this": that builder always delivers a 2.0 event's path decoded, so
21
+ * the path is not decoded a second time whatever host the request names, a
22
+ * Function URL's included. A direct invoker that writes it gains nothing: it
23
+ * writes the whole event, the path included, either way. Declared beside the
24
+ * invoke id for the same reason: the builder writes it and the server reads
25
+ * it.
26
+ */
27
+ export const LAMBDER_LOCAL_API_ID = "lambder-local";
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Two assertions over a call's outcome, for tests.
3
+ *
4
+ * An outcome is a discriminated union, so a test that expects a refusal must
5
+ * narrow before it can read what the refusal carries: check `ok`, branch on
6
+ * it, check `reason`. Written by hand, the failing case prints "expected
7
+ * false to be true" and says nothing about what came back, the one thing
8
+ * worth knowing when a call that should have been refused went through, or
9
+ * crashed instead.
10
+ *
11
+ * These narrow through an `asserts` signature, so the lines after one read
12
+ * the arm it proved, and they throw a plain Error naming what the outcome
13
+ * was. No test runner is imported: the same two functions serve vitest, jest
14
+ * and node:test, from `lambder/testing` over a real server and from
15
+ * `lambder/mock` over a mock one. Pure and dependency-free, like the outcome
16
+ * vocabulary they read.
17
+ *
18
+ * Typed structurally over `ok` and `reason` rather than over
19
+ * LambderApiOutcome, so a LambderInvokeOutcome, whose failure side names
20
+ * other reasons, is narrowed by the same functions and a misspelled reason is
21
+ * a compile error against whichever union was passed.
22
+ */
23
+ /** What both callers' outcomes have in common: the discriminant, and a reason on the failure side. */
24
+ type LambderOutcomeShape = {
25
+ ok: true;
26
+ } | {
27
+ ok: false;
28
+ reason: string;
29
+ };
30
+ /** Every reason the failure side of an outcome union can carry. */
31
+ type LambderFailureReasonOf<TOutcome> = TOutcome extends {
32
+ ok: false;
33
+ reason: infer TReason;
34
+ } ? TReason : never;
35
+ /**
36
+ * The failure arms that can carry one of the given reasons, each narrowed to
37
+ * it. Per arm rather than through Extract: one arm may carry several reasons
38
+ * (`network`, `timeout`, `server` and `unknown` share theirs), and Extract
39
+ * would drop that arm for any single one of them.
40
+ */
41
+ type LambderFailureWithReason<TOutcome, TReason> = TOutcome extends {
42
+ ok: false;
43
+ reason: infer TArmReason;
44
+ } ? [TReason & TArmReason] extends [never] ? never : TOutcome & {
45
+ reason: TReason & TArmReason;
46
+ } : never;
47
+ /** What else a failure is expected to carry, beside its reason. */
48
+ export type LambderExpectedFailure = {
49
+ /** The refusal's machine-readable code (`errorMessage.code`), e.g. a LAMBDER_REFUSAL_CODES value or the app's own. */
50
+ code?: string;
51
+ /** The HTTP status the answer came with. */
52
+ status?: number;
53
+ };
54
+ /**
55
+ * Asserts that a call succeeded, and narrows the outcome to its success arm,
56
+ * so `outcome.payload` reads directly on the next line.
57
+ *
58
+ * ```typescript
59
+ * const outcome = await visitor.apiOutcome("order.create", { sku });
60
+ * assertApiSuccess(outcome);
61
+ * expect(outcome.payload?.orderId).toBeDefined();
62
+ * ```
63
+ */
64
+ export declare function assertApiSuccess<TOutcome extends LambderOutcomeShape>(outcome: TOutcome): asserts outcome is Extract<TOutcome, {
65
+ ok: true;
66
+ }>;
67
+ /**
68
+ * Asserts that a call failed, with the given reason when one is named, and
69
+ * narrows the outcome to the arms that reason can be, so what it carries
70
+ * (`zodError` after "validation", `response` after an envelope reason,
71
+ * `error` after the rest) reads directly on the next line.
72
+ *
73
+ * ```typescript
74
+ * assertApiFailure(await member.apiOutcome("org.delete", { id }), "notAuthorized");
75
+ * assertApiFailure(await guest.apiOutcome("signup", form), "errorMessage", { code: LAMBDER_REFUSAL_CODES.rateLimited, status: 429 });
76
+ * ```
77
+ */
78
+ export declare function assertApiFailure<TOutcome extends LambderOutcomeShape, TReason extends LambderFailureReasonOf<TOutcome> = LambderFailureReasonOf<TOutcome>>(outcome: TOutcome, reason?: TReason, expected?: LambderExpectedFailure): asserts outcome is LambderFailureWithReason<TOutcome, TReason>;
79
+ export {};
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Two assertions over a call's outcome, for tests.
3
+ *
4
+ * An outcome is a discriminated union, so a test that expects a refusal must
5
+ * narrow before it can read what the refusal carries: check `ok`, branch on
6
+ * it, check `reason`. Written by hand, the failing case prints "expected
7
+ * false to be true" and says nothing about what came back, the one thing
8
+ * worth knowing when a call that should have been refused went through, or
9
+ * crashed instead.
10
+ *
11
+ * These narrow through an `asserts` signature, so the lines after one read
12
+ * the arm it proved, and they throw a plain Error naming what the outcome
13
+ * was. No test runner is imported: the same two functions serve vitest, jest
14
+ * and node:test, from `lambder/testing` over a real server and from
15
+ * `lambder/mock` over a mock one. Pure and dependency-free, like the outcome
16
+ * vocabulary they read.
17
+ *
18
+ * Typed structurally over `ok` and `reason` rather than over
19
+ * LambderApiOutcome, so a LambderInvokeOutcome, whose failure side names
20
+ * other reasons, is narrowed by the same functions and a misspelled reason is
21
+ * a compile error against whichever union was passed.
22
+ */
23
+ const MAX_DESCRIBED_VALUE_LENGTH = 300;
24
+ /** A value as it can be printed in an assertion message: JSON, cut short, never throwing over a cyclic one. */
25
+ const describeValue = (value) => {
26
+ let text;
27
+ try {
28
+ text = JSON.stringify(value) ?? String(value);
29
+ }
30
+ catch {
31
+ text = String(value);
32
+ }
33
+ return text.length > MAX_DESCRIBED_VALUE_LENGTH ? `${text.slice(0, MAX_DESCRIBED_VALUE_LENGTH)}...` : text;
34
+ };
35
+ /**
36
+ * The error a failure carries, which becomes the cause of the assertion's
37
+ * own: a test runner prints the chain, so an app that crashed under
38
+ * `lambder/testing` shows the handler's stack under the failed assertion.
39
+ */
40
+ const errorOf = (outcome) => {
41
+ const error = outcome.error;
42
+ return error instanceof Error ? error : undefined;
43
+ };
44
+ /** One line saying what an outcome was, for the message of an assertion it failed. */
45
+ const describeOutcome = (outcome) => {
46
+ if (outcome.ok)
47
+ return `a success carrying ${describeValue(outcome.payload)}`;
48
+ const failure = outcome;
49
+ const details = [];
50
+ if (failure.status !== undefined)
51
+ details.push(`status ${failure.status}`);
52
+ if (failure.errorMessage !== undefined)
53
+ details.push(`errorMessage ${describeValue(failure.errorMessage)}`);
54
+ if (failure.zodError !== undefined)
55
+ details.push(`zodError ${describeValue(failure.zodError.message)}`);
56
+ // The error's own message, and its cause when it has one: an in-process
57
+ // transport reports a handler that threw as a failure whose cause is
58
+ // what actually threw, and that is the line a test author needs.
59
+ if (failure.error instanceof Error) {
60
+ const cause = failure.error.cause instanceof Error ? ` (cause: ${failure.error.cause.message})` : "";
61
+ details.push(`error "${failure.error.message}"${cause}`);
62
+ }
63
+ return `a failure with reason "${failure.reason}"${details.length ? `, ${details.join(", ")}` : ""}`;
64
+ };
65
+ /**
66
+ * Asserts that a call succeeded, and narrows the outcome to its success arm,
67
+ * so `outcome.payload` reads directly on the next line.
68
+ *
69
+ * ```typescript
70
+ * const outcome = await visitor.apiOutcome("order.create", { sku });
71
+ * assertApiSuccess(outcome);
72
+ * expect(outcome.payload?.orderId).toBeDefined();
73
+ * ```
74
+ */
75
+ export function assertApiSuccess(outcome) {
76
+ if (!outcome.ok)
77
+ throw new Error(`Expected the call to succeed, but it was ${describeOutcome(outcome)}.`, { cause: errorOf(outcome) });
78
+ }
79
+ /**
80
+ * Asserts that a call failed, with the given reason when one is named, and
81
+ * narrows the outcome to the arms that reason can be, so what it carries
82
+ * (`zodError` after "validation", `response` after an envelope reason,
83
+ * `error` after the rest) reads directly on the next line.
84
+ *
85
+ * ```typescript
86
+ * assertApiFailure(await member.apiOutcome("org.delete", { id }), "notAuthorized");
87
+ * assertApiFailure(await guest.apiOutcome("signup", form), "errorMessage", { code: LAMBDER_REFUSAL_CODES.rateLimited, status: 429 });
88
+ * ```
89
+ */
90
+ export function assertApiFailure(outcome, reason, expected = {}) {
91
+ const wanted = [
92
+ reason !== undefined ? `reason "${String(reason)}"` : null,
93
+ expected.code !== undefined ? `code "${expected.code}"` : null,
94
+ expected.status !== undefined ? `status ${expected.status}` : null,
95
+ ].filter((part) => part !== null).join(", ");
96
+ const refuse = () => {
97
+ throw new Error(`Expected the call to fail${wanted ? ` with ${wanted}` : ""}, but it was ${describeOutcome(outcome)}.`, { cause: errorOf(outcome) });
98
+ };
99
+ if (outcome.ok)
100
+ return refuse();
101
+ if (reason !== undefined && outcome.reason !== reason)
102
+ return refuse();
103
+ if (expected.code !== undefined) {
104
+ // Only the structured errorMessage carries a code; a plain string has none to match.
105
+ const errorMessage = outcome.errorMessage;
106
+ const code = errorMessage && typeof errorMessage === "object" ? errorMessage.code : undefined;
107
+ if (code !== expected.code)
108
+ return refuse();
109
+ }
110
+ if (expected.status !== undefined && outcome.status !== expected.status)
111
+ return refuse();
112
+ }
@@ -2,27 +2,27 @@
2
2
  * Request payload compression: the wire format both sides speak.
3
3
  *
4
4
  * When a LambderCaller call's payload clears the configured size, the caller
5
- * sends the payload's JSON as `payloadGz` (gzip bytes, base64) beside
5
+ * sends the payload's JSON as `payloadGz` (gzip bytes, base64) plus
6
6
  * `payloadBytes` (its UTF-8 byte length) in place of `payload`, and the
7
7
  * server restores it before anything reads the payload. A Node caller
8
8
  * (LambderInvokeCaller) sends `payloadBr` instead, Brotli under the same
9
- * rules; the server accepts either. Everything else in the envelope
10
- * (apiName, version, token, siteHost, guardInputs, idempotencyKey) stays
11
- * plain text, so routing, logging and request mocking are unaffected.
9
+ * rules; the server accepts either. The rest of the envelope (apiName,
10
+ * version, token, siteHost, guardInputs, idempotencyKey) stays plain text,
11
+ * so routing, logging and request mocking are unaffected.
12
12
  *
13
- * Base64 inside the JSON envelope, rather than a binary body with
14
- * Content-Encoding: API Gateway hands a binary request body to Lambda
15
- * base64-encoded anyway, so binary saves nothing against Lambda's ~6MB
16
- * invoke payload cap while adding a content-type negotiation that gateways,
17
- * CDNs and mock servers each treat differently. Base64's 4/3 overhead
18
- * applies to bytes that already shrank several times over.
13
+ * Base64 inside the JSON envelope rather than a binary body with
14
+ * Content-Encoding: API Gateway hands a binary body to Lambda base64-encoded
15
+ * anyway, so binary saves nothing against Lambda's ~6MB invoke cap while
16
+ * adding content-type negotiation that gateways, CDNs and mock servers each
17
+ * treat differently. Base64's 4/3 overhead applies to bytes that already
18
+ * shrank several times over.
19
19
  *
20
20
  * gzip rather than Brotli because the browser's CompressionStream offers
21
- * gzip and deflate only; responses, compressed by Node, do prefer Brotli.
21
+ * only gzip and deflate; responses, compressed by Node, prefer Brotli.
22
22
  *
23
- * `payloadBytes` is not bookkeeping: it bounds the server's decompression
24
- * and the restored length must match it exactly, the same guarantee
25
- * LambderCompressionCodec gives stored records, so a malicious or truncated
23
+ * `payloadBytes` is not bookkeeping: it bounds the server's decompression,
24
+ * and the restored length must match it exactly (the guarantee
25
+ * LambderCompressionCodec gives stored records), so a malicious or truncated
26
26
  * body fails instead of expanding without limit.
27
27
  */
28
28
  import type { LambderCompressionOption, LambderCompressionSettings } from "./LambderCompressionOption.js";
@@ -76,9 +76,8 @@ export declare const DEFAULT_MAX_RESTORED_PAYLOAD_BYTES = 20000000;
76
76
  * `compressRequest: true` means "whatever the size", which is a threshold of
77
77
  * zero rather than a separate path.
78
78
  *
79
- * Both callers decide this, and the three-line ternary they each wrote is the
80
- * one place a caller can get the override backwards, so it is written once
81
- * beside the compressors it feeds.
79
+ * Both callers decide this, and the override is easy to get backwards, so it
80
+ * is written once, beside the compressors it feeds.
82
81
  */
83
82
  export declare const resolveRequestCompressionMinBytes: (compressRequest: boolean | undefined, settings: {
84
83
  minBytes: number;
@@ -87,11 +86,10 @@ export declare const resolveRequestCompressionMinBytes: (compressRequest: boolea
87
86
  export declare const isRequestCompressionAvailable: () => boolean;
88
87
  /**
89
88
  * Gzip one payload's JSON for sending, or null when the plain JSON should go
90
- * instead (see compressPayloadWith for the two rules). The second null
89
+ * instead (see compressPayloadWith for the two rules). The second rule
91
90
  * matters for the payloads most likely to be large: a base64 image gzips to
92
91
  * nearly its own size, and base64 then inflates the result past the
93
- * original. Sending that would cost CPU on both ends for a request that got
94
- * bigger, so the compressed form is only ever sent when it is smaller.
92
+ * original, so sending it would cost CPU on both ends for a bigger request.
95
93
  */
96
94
  export declare const compressPayloadGzip: (json: string, minBytes: number) => Promise<LambderCompressedGzipPayload | null>;
97
95
  /** Request Brotli when `requestCompression: true` on LambderInvokeCaller: the HTTP request threshold, at the quality every other Lambder site uses. */
@@ -51,9 +51,8 @@ const compressPayloadWith = async (json, minBytes, field, compress) => {
51
51
  * `compressRequest: true` means "whatever the size", which is a threshold of
52
52
  * zero rather than a separate path.
53
53
  *
54
- * Both callers decide this, and the three-line ternary they each wrote is the
55
- * one place a caller can get the override backwards, so it is written once
56
- * beside the compressors it feeds.
54
+ * Both callers decide this, and the override is easy to get backwards, so it
55
+ * is written once, beside the compressors it feeds.
57
56
  */
58
57
  export const resolveRequestCompressionMinBytes = (compressRequest, settings) => compressRequest === true ? 0
59
58
  : compressRequest === false ? null
@@ -62,11 +61,10 @@ export const resolveRequestCompressionMinBytes = (compressRequest, settings) =>
62
61
  export const isRequestCompressionAvailable = () => typeof CompressionStream !== "undefined" && typeof btoa !== "undefined";
63
62
  /**
64
63
  * Gzip one payload's JSON for sending, or null when the plain JSON should go
65
- * instead (see compressPayloadWith for the two rules). The second null
64
+ * instead (see compressPayloadWith for the two rules). The second rule
66
65
  * matters for the payloads most likely to be large: a base64 image gzips to
67
66
  * nearly its own size, and base64 then inflates the result past the
68
- * original. Sending that would cost CPU on both ends for a request that got
69
- * bigger, so the compressed form is only ever sent when it is smaller.
67
+ * original, so sending it would cost CPU on both ends for a bigger request.
70
68
  */
71
69
  export const compressPayloadGzip = (json, minBytes) => compressPayloadWith(json, minBytes, COMPRESSED_PAYLOAD_GZ_FIELD, async (bytes) => {
72
70
  const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
@@ -0,0 +1,48 @@
1
+ /**
2
+ * How a cache's fill runs the loader and keeps its value: `store` writes the
3
+ * value where the cache keeps it and answers it as stored. Handed to the
4
+ * cache at the point it fills, so the store can carry what only that point
5
+ * knows (LambderDdbCache's fill lease).
6
+ */
7
+ export type LambderCacheLoad<T> = (store: (value: T) => Promise<T>) => Promise<T>;
8
+ /**
9
+ * getOrSet's contract, shared by every cache here: concurrent calls for one
10
+ * key in one process share a single load, each handed a parse of its own as
11
+ * every read is, a loader's failure is the caller's, and the cache's own
12
+ * failure (a read, a lease, a write) never is.
13
+ * A cache failure after the loader answered hands that value back uncached,
14
+ * and one before the loader ran calls the loader directly, so the loader runs
15
+ * at most once per call whatever breaks. A loader's undefined has nothing to
16
+ * cache and comes back as it is, so the next call loads again (a loader
17
+ * answers null to cache "not found"); anything else is answered as the JSON
18
+ * it was stored as, the filling call included, so it has the shape every
19
+ * later hit has.
20
+ *
21
+ * A write of the key (set, delete, deletePartition) while a fill is loading
22
+ * wins over the fill: the loader may have read its source before whatever
23
+ * that write records, so storing its value afterwards would put back what the
24
+ * write replaced. The write takes the fill out of the in-flight map, a fill
25
+ * stores only while the map still holds it, and a call arriving after the
26
+ * write starts a load of its own instead of joining the old one. The old
27
+ * fill's callers get its value uncached.
28
+ *
29
+ * LambderDdbCache and LambderMemoryCache each supply only their read-then-fill
30
+ * and hold one filler, so the two cannot drift on any of the above.
31
+ */
32
+ export declare class LambderCacheFiller {
33
+ private readonly inFlight;
34
+ /** Opens the log line of a fail-open, naming the cache: "DynamoDB cache failed open in geo". */
35
+ private readonly failOpenLabel;
36
+ constructor(failOpenLabel: string);
37
+ /**
38
+ * The value for `memoryKey`: `readOrFill` is the cache's own read, and
39
+ * its fill when the read finds nothing, handed `load` to call (at most
40
+ * once) at that point with the store that keeps the loader's value.
41
+ */
42
+ getOrSet<T>(memoryKey: string, loader: () => Promise<T>, readOrFill: (load: LambderCacheLoad<T>) => Promise<T>): Promise<T>;
43
+ /** Called by a write of `memoryKey` before it writes: the fill in flight for it stores nothing (see the class comment). */
44
+ supersedeFill(memoryKey: string): void;
45
+ /** supersedeFill for every key starting with `memoryKeyPrefix`: one partition's keys, or all of them for "". */
46
+ supersedeFillsWithPrefix(memoryKeyPrefix: string): void;
47
+ private failOpen;
48
+ }
@@ -0,0 +1,119 @@
1
+ import { asStoredJson } from "./LambderCacheValues.js";
2
+ /**
3
+ * getOrSet's contract, shared by every cache here: concurrent calls for one
4
+ * key in one process share a single load, each handed a parse of its own as
5
+ * every read is, a loader's failure is the caller's, and the cache's own
6
+ * failure (a read, a lease, a write) never is.
7
+ * A cache failure after the loader answered hands that value back uncached,
8
+ * and one before the loader ran calls the loader directly, so the loader runs
9
+ * at most once per call whatever breaks. A loader's undefined has nothing to
10
+ * cache and comes back as it is, so the next call loads again (a loader
11
+ * answers null to cache "not found"); anything else is answered as the JSON
12
+ * it was stored as, the filling call included, so it has the shape every
13
+ * later hit has.
14
+ *
15
+ * A write of the key (set, delete, deletePartition) while a fill is loading
16
+ * wins over the fill: the loader may have read its source before whatever
17
+ * that write records, so storing its value afterwards would put back what the
18
+ * write replaced. The write takes the fill out of the in-flight map, a fill
19
+ * stores only while the map still holds it, and a call arriving after the
20
+ * write starts a load of its own instead of joining the old one. The old
21
+ * fill's callers get its value uncached.
22
+ *
23
+ * LambderDdbCache and LambderMemoryCache each supply only their read-then-fill
24
+ * and hold one filler, so the two cannot drift on any of the above.
25
+ */
26
+ export class LambderCacheFiller {
27
+ inFlight = new Map();
28
+ /** Opens the log line of a fail-open, naming the cache: "DynamoDB cache failed open in geo". */
29
+ failOpenLabel;
30
+ constructor(failOpenLabel) {
31
+ this.failOpenLabel = failOpenLabel;
32
+ }
33
+ /**
34
+ * The value for `memoryKey`: `readOrFill` is the cache's own read, and
35
+ * its fill when the read finds nothing, handed `load` to call (at most
36
+ * once) at that point with the store that keeps the loader's value.
37
+ */
38
+ getOrSet(memoryKey, loader, readOrFill) {
39
+ const current = this.inFlight.get(memoryKey);
40
+ if (current) {
41
+ // A parse of its own, as a read hands back: the first call's
42
+ // object is that caller's to change, and a change must not show
43
+ // in anyone else's answer.
44
+ current.joiners += 1;
45
+ return current.answer.then((value) => current.answerJson === undefined ? value : JSON.parse(current.answerJson));
46
+ }
47
+ const leave = () => {
48
+ // A write may have handed the key to a newer fill meanwhile.
49
+ if (this.inFlight.get(memoryKey) === shared)
50
+ this.inFlight.delete(memoryKey);
51
+ };
52
+ const shared = {
53
+ joiners: 0,
54
+ answer: this.failOpen(loader, readOrFill, () => this.inFlight.get(memoryKey) !== shared).then((value) => {
55
+ // Out of the map in the same step the text is taken, and
56
+ // before any caller holds the value: no call joins after
57
+ // this, and none has changed the value yet.
58
+ leave();
59
+ if (shared.joiners > 0) {
60
+ // A value JSON cannot hold (a bigint the loader
61
+ // answered on a fail-open) goes to every caller as it is.
62
+ try {
63
+ shared.answerJson = JSON.stringify(value);
64
+ }
65
+ catch { }
66
+ }
67
+ return value;
68
+ }, (error) => {
69
+ leave();
70
+ throw error;
71
+ }),
72
+ };
73
+ this.inFlight.set(memoryKey, shared);
74
+ return shared.answer;
75
+ }
76
+ /** Called by a write of `memoryKey` before it writes: the fill in flight for it stores nothing (see the class comment). */
77
+ supersedeFill(memoryKey) {
78
+ this.inFlight.delete(memoryKey);
79
+ }
80
+ /** supersedeFill for every key starting with `memoryKeyPrefix`: one partition's keys, or all of them for "". */
81
+ supersedeFillsWithPrefix(memoryKeyPrefix) {
82
+ for (const memoryKey of [...this.inFlight.keys()]) {
83
+ if (memoryKey.startsWith(memoryKeyPrefix))
84
+ this.inFlight.delete(memoryKey);
85
+ }
86
+ }
87
+ async failOpen(loader, readOrFill, superseded) {
88
+ let loaderStarted = false;
89
+ let loaderCompleted = false;
90
+ let loaderValue;
91
+ const trackedLoader = async () => {
92
+ loaderStarted = true;
93
+ loaderValue = await loader();
94
+ loaderCompleted = true;
95
+ return loaderValue;
96
+ };
97
+ const load = async (store) => {
98
+ const value = await trackedLoader();
99
+ if (value === undefined)
100
+ return value;
101
+ if (superseded())
102
+ return asStoredJson(value);
103
+ return await store(value);
104
+ };
105
+ try {
106
+ return await readOrFill(load);
107
+ }
108
+ catch (error) {
109
+ if (loaderStarted && !loaderCompleted)
110
+ throw error;
111
+ // The key stays out of the line: it is the app's data (an email
112
+ // address, a user id), and the other engines keep theirs out too.
113
+ console.error(`${this.failOpenLabel}; the loader's value is handed back uncached.`, error);
114
+ if (loaderCompleted)
115
+ return asStoredJson(loaderValue);
116
+ return asStoredJson(await trackedLoader());
117
+ }
118
+ }
119
+ }