lambder 6.0.2 → 7.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 +2316 -0
- package/README.md +60 -33
- package/dist/api/LambderApiAnswer.d.ts +40 -0
- package/dist/api/LambderApiAnswer.js +19 -0
- package/dist/api/LambderApiCallContext.d.ts +38 -0
- package/dist/api/LambderApiCallContext.js +13 -0
- package/dist/api/LambderApiDefinition.d.ts +18 -0
- package/dist/api/LambderApiDefinition.js +1 -0
- package/dist/api/LambderApiEnvelope.d.ts +67 -0
- package/dist/api/LambderApiEnvelope.js +180 -0
- package/dist/api/LambderApiGuards.d.ts +302 -0
- package/dist/api/LambderApiGuards.js +134 -0
- package/dist/api/LambderApiIdempotency.d.ts +122 -0
- package/dist/api/LambderApiIdempotency.js +330 -0
- package/dist/api/LambderApiPipeline.d.ts +134 -0
- package/dist/api/LambderApiPipeline.js +221 -0
- package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
- package/dist/api/LambderApiPolicyEngine.js +77 -0
- package/dist/api/LambderApiRateLimits.d.ts +206 -0
- package/dist/api/LambderApiRateLimits.js +239 -0
- package/dist/api/LambderApiRequest.d.ts +101 -0
- package/dist/api/LambderApiRequest.js +129 -0
- package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
- package/dist/api/LambderApiValidationRefusal.js +40 -0
- package/dist/client/LambderCaller.d.ts +62 -55
- package/dist/client/LambderCaller.js +147 -90
- package/dist/client/lambderFetchTransport.d.ts +9 -0
- package/dist/client/lambderFetchTransport.js +71 -0
- package/dist/client.d.ts +20 -10
- package/dist/client.js +11 -5
- package/dist/core/Lambder.d.ts +117 -253
- package/dist/core/Lambder.js +374 -341
- package/dist/core/LambderContext.d.ts +54 -44
- package/dist/core/LambderContext.js +41 -110
- package/dist/core/LambderCreateOptions.d.ts +285 -0
- package/dist/core/LambderCreateOptions.js +44 -0
- package/dist/core/LambderFiles.d.ts +1 -45
- package/dist/core/LambderFiles.js +18 -38
- package/dist/core/LambderIndexHtml.d.ts +37 -0
- package/dist/core/LambderIndexHtml.js +87 -0
- package/dist/core/LambderPolicyBuilders.d.ts +17 -0
- package/dist/core/LambderPolicyBuilders.js +16 -0
- package/dist/core/LambderPublicFiles.d.ts +5 -2
- package/dist/core/LambderPublicFiles.js +7 -2
- package/dist/core/LambderResolver.d.ts +8 -6
- package/dist/core/LambderResponse.d.ts +29 -11
- package/dist/core/LambderResponse.js +96 -49
- package/dist/core/LambderResponseBuilder.d.ts +18 -14
- package/dist/core/LambderResponseBuilder.js +19 -25
- package/dist/core/LambderRouting.d.ts +18 -7
- package/dist/core/LambderRouting.js +17 -7
- package/dist/core/LambderTemplatingEngine.d.ts +0 -62
- package/dist/core/LambderTemplatingEngine.js +7 -3
- package/dist/index.d.ts +85 -32
- package/dist/index.js +44 -16
- package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
- package/dist/invoke/LambderInvokeCaller.js +140 -335
- package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
- package/dist/invoke/LambderInvokeOutcome.js +129 -0
- package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
- package/dist/invoke/LambderLambdaEvent.js +187 -0
- package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
- package/dist/invoke/lambderHandlerTransport.js +89 -0
- package/dist/mock/LambderMockApp.d.ts +352 -0
- package/dist/mock/LambderMockApp.js +815 -0
- package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
- package/dist/mock/LambderMockBrowserCookies.js +76 -0
- package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
- package/dist/mock/LambderMockCallRecorder.js +183 -0
- package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
- package/dist/mock/LambderMockCreateOptions.js +9 -0
- package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
- package/dist/mock/LambderMockEntryRegistry.js +126 -0
- package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
- package/dist/mock/LambderMockFailureInjector.js +138 -0
- package/dist/mock/LambderMockTypes.d.ts +421 -0
- package/dist/mock/LambderMockTypes.js +8 -0
- package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
- package/dist/mock/lambderMockConsoleLogger.js +35 -0
- package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
- package/dist/mock/lambderMockInvokeTransport.js +52 -0
- package/dist/mock/lambderMockMswHandler.d.ts +99 -0
- package/dist/mock/lambderMockMswHandler.js +126 -0
- package/dist/mock.d.ts +34 -0
- package/dist/mock.js +27 -0
- package/dist/session/LambderSessionController.d.ts +199 -30
- package/dist/session/LambderSessionController.js +396 -82
- package/dist/session/LambderSessionCrypto.d.ts +66 -0
- package/dist/session/LambderSessionCrypto.js +101 -0
- package/dist/session/LambderSessionManager.d.ts +118 -80
- package/dist/session/LambderSessionManager.js +212 -184
- package/dist/shared/LambderI18n.d.ts +6 -6
- package/dist/shared/LambderI18n.js +1 -1
- package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
- package/dist/shared/contracts/LambderFileSource.js +19 -0
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
- package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
- package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
- package/dist/shared/contracts/LambderRateLimiter.js +24 -0
- package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
- package/dist/shared/contracts/LambderSessionStore.js +13 -0
- package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
- package/dist/shared/transport/LambderApiTransport.js +65 -0
- package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
- package/dist/shared/transport/LambderCookieJar.js +246 -0
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
- package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
- package/dist/shared/util/LambderBase64.d.ts +10 -0
- package/dist/shared/util/LambderBase64.js +27 -0
- package/dist/shared/util/LambderCallAbort.d.ts +62 -0
- package/dist/shared/util/LambderCallAbort.js +80 -0
- package/dist/shared/util/LambderClientIp.d.ts +32 -0
- package/dist/shared/util/LambderClientIp.js +56 -0
- package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
- package/dist/shared/util/LambderExpiringMap.js +217 -0
- package/dist/shared/util/LambderKeyFields.d.ts +32 -0
- package/dist/shared/util/LambderKeyFields.js +34 -0
- package/dist/shared/util/LambderNodeModules.d.ts +9 -0
- package/dist/shared/util/LambderNodeModules.js +39 -0
- package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
- package/dist/shared/util/LambderOptionChecks.js +33 -0
- package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
- package/dist/shared/util/LambderResponseBrand.js +18 -0
- package/dist/shared/util/LambderTextDigest.d.ts +17 -0
- package/dist/shared/util/LambderTextDigest.js +34 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
- package/dist/shared/util/LambderTypeUtilities.js +8 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
- package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
- package/dist/shared/wire/LambderApiContract.d.ts +129 -0
- package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
- package/dist/shared/wire/LambderApiOptionValues.js +11 -0
- package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
- package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
- package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
- package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
- package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
- package/dist/shared/wire/LambderCallOptions.js +17 -0
- package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
- package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
- package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
- package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
- package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
- package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
- package/dist/shared/wire/LambderHttpStatus.js +1 -0
- package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
- package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
- package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
- package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
- package/dist/stores/LambderDdbCache.d.ts +12 -9
- package/dist/stores/LambderDdbCache.js +56 -47
- package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
- package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
- package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
- package/dist/stores/LambderDdbRateLimiter.js +47 -45
- package/dist/stores/LambderDdbSdk.d.ts +83 -6
- package/dist/stores/LambderDdbSdk.js +83 -2
- package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
- package/dist/stores/LambderDdbSessionStore.js +161 -0
- package/dist/stores/LambderHttpFileSource.d.ts +1 -1
- package/dist/stores/LambderHttpFileSource.js +10 -1
- package/dist/stores/LambderLocalFileSource.d.ts +15 -0
- package/dist/stores/LambderLocalFileSource.js +28 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
- package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
- package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
- package/dist/stores/LambderMemoryRateLimiter.js +64 -0
- package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
- package/dist/stores/LambderMemorySessionStore.js +74 -0
- package/dist/stores/LambderS3FileSource.d.ts +1 -1
- package/dist/stores/LambderS3FileSource.js +1 -1
- package/package.json +21 -19
- package/dist/client/LambderMSW.d.ts +0 -69
- package/dist/client/LambderMSW.js +0 -121
- package/dist/policies/LambderApiGuards.d.ts +0 -256
- package/dist/policies/LambderApiGuards.js +0 -94
- package/dist/policies/LambderApiIdempotency.d.ts +0 -58
- package/dist/policies/LambderApiIdempotency.js +0 -219
- package/dist/policies/LambderApiPolicies.d.ts +0 -42
- package/dist/policies/LambderApiPolicies.js +0 -52
- package/dist/policies/LambderApiRateLimits.d.ts +0 -132
- package/dist/policies/LambderApiRateLimits.js +0 -119
- package/dist/shared/LambderApiContract.d.ts +0 -57
- package/dist/shared/LambderApiOutcome.d.ts +0 -69
- package/dist/shared/LambderCallOptions.d.ts +0 -71
- package/dist/shared/LambderCallOptions.js +0 -16
- package/dist/shared/node-polyfills.d.ts +0 -4
- package/dist/shared/node-polyfills.js +0 -58
- package/dist/stores/LambderDdbIdempotency.js +0 -229
- package/dist/testing.d.ts +0 -9
- package/dist/testing.js +0 -8
- /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.js +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which of register()'s arguments is the rest entry rather than a slice.
|
|
3
|
+
*
|
|
4
|
+
* Read off the one field restNotMocked() writes, which is also why that field
|
|
5
|
+
* is named as it is: a slice is keyed by endpoint names holding entries, so
|
|
6
|
+
* what tells the two apart has to be a key no endpoint name plausibly is,
|
|
7
|
+
* carrying a value no entry is.
|
|
8
|
+
*/
|
|
9
|
+
const isRestNotMockedEntry = (slice) => typeof slice.restNotMockedReason === "string";
|
|
10
|
+
/**
|
|
11
|
+
* Which mock answers an endpoint: the registered entries, and the overrides
|
|
12
|
+
* standing over them.
|
|
13
|
+
*
|
|
14
|
+
* One of the four pieces of state LambderMockApp holds that nothing else
|
|
15
|
+
* touches; it meets the rest of the runtime at one call, the lookup a request
|
|
16
|
+
* makes. The app keeps the whole caller-facing surface (register,
|
|
17
|
+
* registerPartial, override, restoreOverrides, registeredNames) as
|
|
18
|
+
* delegations, because that surface is contract-typed and the checks that
|
|
19
|
+
* make it safe are compile-time; what lives here is the bookkeeping.
|
|
20
|
+
*
|
|
21
|
+
* Generic over the contract only so the entries keep their type through the
|
|
22
|
+
* map; the registry itself never reads one.
|
|
23
|
+
*/
|
|
24
|
+
export class LambderMockEntryRegistry {
|
|
25
|
+
entries = new Map();
|
|
26
|
+
/**
|
|
27
|
+
* The overrides standing over one endpoint, innermost last. A stack
|
|
28
|
+
* rather than one slot because overrides nest: an override a describe
|
|
29
|
+
* scopes and one an it scopes are both live, and restoring the inner one
|
|
30
|
+
* has to uncover the outer rather than the registry.
|
|
31
|
+
*/
|
|
32
|
+
overrideStacks = new Map();
|
|
33
|
+
/** The reason register() was given a rest entry with; null while it was given none. */
|
|
34
|
+
restReason = null;
|
|
35
|
+
/**
|
|
36
|
+
* What a call to an endpoint no slice registered is refused with, or null
|
|
37
|
+
* when no rest entry was registered. A registration like any other, so
|
|
38
|
+
* reset() keeps it.
|
|
39
|
+
*/
|
|
40
|
+
get restNotMockedReason() {
|
|
41
|
+
return this.restReason;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Adds every entry of every slice, and the rest entry where one is among
|
|
45
|
+
* them. Slices are staged and committed together, so a slice that fails a
|
|
46
|
+
* check leaves nothing behind: a caller that catches the error and retries
|
|
47
|
+
* sees the problem it is fixing rather than a duplicate-name error from
|
|
48
|
+
* its own first attempt. The rest entry is staged with them, for the same
|
|
49
|
+
* reason.
|
|
50
|
+
*/
|
|
51
|
+
addSlices(slices) {
|
|
52
|
+
const staged = new Map();
|
|
53
|
+
let stagedRestReason = null;
|
|
54
|
+
for (const slice of slices) {
|
|
55
|
+
if (isRestNotMockedEntry(slice)) {
|
|
56
|
+
// Two of them answer the same calls with two different
|
|
57
|
+
// reasons, and which one a call would get is registration
|
|
58
|
+
// order, which is exactly what a duplicate name is refused
|
|
59
|
+
// for.
|
|
60
|
+
const registered = this.restReason ?? stagedRestReason;
|
|
61
|
+
if (registered !== null) {
|
|
62
|
+
throw new Error(`LambderMockApp: the rest of the contract is already registered as not mocked ("${registered}"). One restNotMocked entry covers every endpoint the slices leave out.`);
|
|
63
|
+
}
|
|
64
|
+
stagedRestReason = slice.restNotMockedReason;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
for (const [key, entry] of Object.entries(slice)) {
|
|
68
|
+
// The compile-time completeness check reads the slice's KEYS
|
|
69
|
+
// and registration reads entry.name, so the two have to agree
|
|
70
|
+
// or the check is checking something else than what runs. A
|
|
71
|
+
// hand-written slice is where they part: `{ ...userMocks,
|
|
72
|
+
// "user.list": someOtherEntry }` registers the other endpoint
|
|
73
|
+
// and leaves "user.list" unanswered, and the first symptom is
|
|
74
|
+
// an overlap error naming an endpoint nobody wrote twice.
|
|
75
|
+
if (key !== entry.name) {
|
|
76
|
+
throw new Error(`LambderMockApp: slice key "${key}" holds the mock for "${entry.name}". Key every entry by its own endpoint name, or build the slice with mockApp.apiSlice(...).`);
|
|
77
|
+
}
|
|
78
|
+
if (this.entries.has(entry.name) || staged.has(entry.name)) {
|
|
79
|
+
throw new Error(`LambderMockApp: endpoint "${entry.name}" is mocked in more than one slice.`);
|
|
80
|
+
}
|
|
81
|
+
staged.set(entry.name, entry);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const [name, entry] of staged)
|
|
85
|
+
this.entries.set(name, entry);
|
|
86
|
+
if (stagedRestReason !== null)
|
|
87
|
+
this.restReason = stagedRestReason;
|
|
88
|
+
}
|
|
89
|
+
/** The registered entry for a name, before any override; null when there is none. */
|
|
90
|
+
registered(apiName) {
|
|
91
|
+
return this.entries.get(apiName) ?? null;
|
|
92
|
+
}
|
|
93
|
+
/** Stacks an override over a registered entry and hands back its removal. */
|
|
94
|
+
pushOverride(name, entry) {
|
|
95
|
+
const stack = this.overrideStacks.get(name) ?? [];
|
|
96
|
+
stack.push(entry);
|
|
97
|
+
this.overrideStacks.set(name, stack);
|
|
98
|
+
// Removes this override wherever it sits rather than popping the top:
|
|
99
|
+
// scopes do not always unwind innermost first (an outer handle
|
|
100
|
+
// restored by hand while an inner one is still standing), and popping
|
|
101
|
+
// would then take down somebody else's override. Restoring twice is a
|
|
102
|
+
// no-op.
|
|
103
|
+
const restore = () => {
|
|
104
|
+
const current = this.overrideStacks.get(name);
|
|
105
|
+
const at = current?.lastIndexOf(entry) ?? -1;
|
|
106
|
+
if (!current || at === -1)
|
|
107
|
+
return;
|
|
108
|
+
current.splice(at, 1);
|
|
109
|
+
if (!current.length)
|
|
110
|
+
this.overrideStacks.delete(name);
|
|
111
|
+
};
|
|
112
|
+
return { restore };
|
|
113
|
+
}
|
|
114
|
+
/** Puts every overridden handler back, however deeply they were stacked. */
|
|
115
|
+
restoreOverrides() {
|
|
116
|
+
this.overrideStacks.clear();
|
|
117
|
+
}
|
|
118
|
+
/** The registered endpoint names. */
|
|
119
|
+
get names() {
|
|
120
|
+
return [...this.entries.keys()];
|
|
121
|
+
}
|
|
122
|
+
/** What answers this call: the innermost override, else the registered entry. */
|
|
123
|
+
entryFor(apiName) {
|
|
124
|
+
return this.overrideStacks.get(apiName)?.at(-1) ?? this.entries.get(apiName) ?? null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { LambderApiAnswer } from "../api/LambderApiAnswer.js";
|
|
2
|
+
import type { LambderApiRequest } from "../api/LambderApiRequest.js";
|
|
3
|
+
import type { LambderMockFailure, LambderMockFailureReason, LambderMockLatency } from "./LambderMockTypes.js";
|
|
4
|
+
/**
|
|
5
|
+
* A failure the transport reports rather than an answer the caller reads:
|
|
6
|
+
* an injected network failure or timeout. The caller maps a rejected
|
|
7
|
+
* transport to `network`, or `timeout` when its own abort fired.
|
|
8
|
+
*/
|
|
9
|
+
export declare class LambderMockTransportError extends Error {
|
|
10
|
+
readonly reason: "network" | "timeout" | "offline";
|
|
11
|
+
constructor(reason: "network" | "timeout" | "offline");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* What a call fails with and how long it takes to do it: the queued and
|
|
15
|
+
* standing failures per endpoint, the offline switch, and the configured
|
|
16
|
+
* latency.
|
|
17
|
+
*
|
|
18
|
+
* One of the four pieces of state LambderMockApp holds that nothing else
|
|
19
|
+
* touches, so it is its own object. The app keeps the whole surface
|
|
20
|
+
* (failNext, setFailure, setOffline, setLatency) as one-line delegations,
|
|
21
|
+
* which is what a caller reads; what moved is the bookkeeping behind them.
|
|
22
|
+
*
|
|
23
|
+
* Injected refusals are rendered through the real envelope helpers, never
|
|
24
|
+
* hand-written: an injected 429 and an earned one have to be the same bytes,
|
|
25
|
+
* or a test written against the injected shape passes while the real one
|
|
26
|
+
* would fail.
|
|
27
|
+
*/
|
|
28
|
+
export declare class LambderMockFailureInjector {
|
|
29
|
+
private readonly failuresNext;
|
|
30
|
+
private readonly failuresSet;
|
|
31
|
+
private offlineFlag;
|
|
32
|
+
private latency;
|
|
33
|
+
/** What latency was configured at creation, so reset() can put it back. */
|
|
34
|
+
private readonly initialLatency;
|
|
35
|
+
private readonly apiVersion;
|
|
36
|
+
constructor(options: {
|
|
37
|
+
apiVersion: string | null;
|
|
38
|
+
latency: LambderMockLatency;
|
|
39
|
+
});
|
|
40
|
+
/** True while every call should reject at the transport, as with no network at all. */
|
|
41
|
+
get offline(): boolean;
|
|
42
|
+
/** The next call to the endpoint fails this way; several calls queue in order. */
|
|
43
|
+
failNext(apiName: string, failure: LambderMockFailure | LambderMockFailureReason): void;
|
|
44
|
+
/** Every call to the endpoint fails this way until cleared with null. */
|
|
45
|
+
setFailure(apiName: string, failure: LambderMockFailure | LambderMockFailureReason | null): void;
|
|
46
|
+
setOffline(offline: boolean): void;
|
|
47
|
+
setLatency(latency: LambderMockLatency): void;
|
|
48
|
+
/** The failure this call should take, queued one first; null when the call should run. */
|
|
49
|
+
take(apiName: string): LambderMockFailure | null;
|
|
50
|
+
/** Milliseconds this call should take before it is answered. */
|
|
51
|
+
latencyFor(apiName: string): number;
|
|
52
|
+
/** Waits the configured latency, cancelled by the caller's abort. */
|
|
53
|
+
wait(ms: number, signal: AbortSignal | undefined): Promise<void>;
|
|
54
|
+
/** Waits for the caller's own abort, which is what a timeout is; a call with no signal waits for ever. */
|
|
55
|
+
waitForAbort(signal: AbortSignal | undefined): Promise<never>;
|
|
56
|
+
/** The answer an injected failure produces, or a throw for the ones that never reach the caller as answers. */
|
|
57
|
+
answerFor(failure: LambderMockFailure, request: LambderApiRequest): Promise<LambderApiAnswer>;
|
|
58
|
+
/** Clears every injected failure and puts the configured latency back. */
|
|
59
|
+
reset(): void;
|
|
60
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { LambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
|
|
2
|
+
import { crashAnswer, refusalAnswer, sessionExpiredAnswer, versionExpiredAnswer } from "../api/LambderApiEnvelope.js";
|
|
3
|
+
import { rateLimitRefusal } from "../api/LambderApiRateLimits.js";
|
|
4
|
+
/** An injected refusal's message, from a string, a full message, or the reason's own wording. */
|
|
5
|
+
const toRefusalMessage = (message, fallback) => typeof message === "string" ? { type: "warning", content: message }
|
|
6
|
+
: message ?? { type: "warning", content: fallback };
|
|
7
|
+
/**
|
|
8
|
+
* A failure the transport reports rather than an answer the caller reads:
|
|
9
|
+
* an injected network failure or timeout. The caller maps a rejected
|
|
10
|
+
* transport to `network`, or `timeout` when its own abort fired.
|
|
11
|
+
*/
|
|
12
|
+
export class LambderMockTransportError extends Error {
|
|
13
|
+
reason;
|
|
14
|
+
constructor(reason) {
|
|
15
|
+
super(`LambderMockApp: ${reason === "offline" ? "the mock is offline" : `injected ${reason} failure`}`);
|
|
16
|
+
this.name = "LambderMockTransportError";
|
|
17
|
+
this.reason = reason;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* What a call fails with and how long it takes to do it: the queued and
|
|
22
|
+
* standing failures per endpoint, the offline switch, and the configured
|
|
23
|
+
* latency.
|
|
24
|
+
*
|
|
25
|
+
* One of the four pieces of state LambderMockApp holds that nothing else
|
|
26
|
+
* touches, so it is its own object. The app keeps the whole surface
|
|
27
|
+
* (failNext, setFailure, setOffline, setLatency) as one-line delegations,
|
|
28
|
+
* which is what a caller reads; what moved is the bookkeeping behind them.
|
|
29
|
+
*
|
|
30
|
+
* Injected refusals are rendered through the real envelope helpers, never
|
|
31
|
+
* hand-written: an injected 429 and an earned one have to be the same bytes,
|
|
32
|
+
* or a test written against the injected shape passes while the real one
|
|
33
|
+
* would fail.
|
|
34
|
+
*/
|
|
35
|
+
export class LambderMockFailureInjector {
|
|
36
|
+
failuresNext = new Map();
|
|
37
|
+
failuresSet = new Map();
|
|
38
|
+
offlineFlag = false;
|
|
39
|
+
latency;
|
|
40
|
+
/** What latency was configured at creation, so reset() can put it back. */
|
|
41
|
+
initialLatency;
|
|
42
|
+
apiVersion;
|
|
43
|
+
constructor(options) {
|
|
44
|
+
this.apiVersion = options.apiVersion;
|
|
45
|
+
this.latency = options.latency;
|
|
46
|
+
this.initialLatency = options.latency;
|
|
47
|
+
}
|
|
48
|
+
/** True while every call should reject at the transport, as with no network at all. */
|
|
49
|
+
get offline() { return this.offlineFlag; }
|
|
50
|
+
/** The next call to the endpoint fails this way; several calls queue in order. */
|
|
51
|
+
failNext(apiName, failure) {
|
|
52
|
+
const queue = this.failuresNext.get(apiName) ?? [];
|
|
53
|
+
queue.push(typeof failure === "string" ? { reason: failure } : failure);
|
|
54
|
+
this.failuresNext.set(apiName, queue);
|
|
55
|
+
}
|
|
56
|
+
/** Every call to the endpoint fails this way until cleared with null. */
|
|
57
|
+
setFailure(apiName, failure) {
|
|
58
|
+
if (failure === null)
|
|
59
|
+
this.failuresSet.delete(apiName);
|
|
60
|
+
else
|
|
61
|
+
this.failuresSet.set(apiName, typeof failure === "string" ? { reason: failure } : failure);
|
|
62
|
+
}
|
|
63
|
+
setOffline(offline) { this.offlineFlag = offline; }
|
|
64
|
+
setLatency(latency) { this.latency = latency; }
|
|
65
|
+
/** The failure this call should take, queued one first; null when the call should run. */
|
|
66
|
+
take(apiName) {
|
|
67
|
+
const queue = this.failuresNext.get(apiName);
|
|
68
|
+
if (queue?.length) {
|
|
69
|
+
const next = queue.shift();
|
|
70
|
+
if (!queue.length)
|
|
71
|
+
this.failuresNext.delete(apiName);
|
|
72
|
+
return next;
|
|
73
|
+
}
|
|
74
|
+
return this.failuresSet.get(apiName) ?? null;
|
|
75
|
+
}
|
|
76
|
+
/** Milliseconds this call should take before it is answered. */
|
|
77
|
+
latencyFor(apiName) {
|
|
78
|
+
const latency = this.latency;
|
|
79
|
+
if (typeof latency === "number")
|
|
80
|
+
return latency;
|
|
81
|
+
if (typeof latency === "function")
|
|
82
|
+
return latency(apiName);
|
|
83
|
+
return latency.min + Math.random() * Math.max(0, latency.max - latency.min);
|
|
84
|
+
}
|
|
85
|
+
/** Waits the configured latency, cancelled by the caller's abort. */
|
|
86
|
+
wait(ms, signal) {
|
|
87
|
+
if (ms <= 0)
|
|
88
|
+
return Promise.resolve();
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const timer = setTimeout(() => { signal?.removeEventListener("abort", onAbort); resolve(); }, ms);
|
|
91
|
+
const onAbort = () => { clearTimeout(timer); reject(new LambderMockTransportError("network")); };
|
|
92
|
+
if (signal?.aborted) {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
reject(new LambderMockTransportError("network"));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/** Waits for the caller's own abort, which is what a timeout is; a call with no signal waits for ever. */
|
|
101
|
+
waitForAbort(signal) {
|
|
102
|
+
return new Promise((_resolve, reject) => {
|
|
103
|
+
if (!signal)
|
|
104
|
+
return;
|
|
105
|
+
if (signal.aborted) {
|
|
106
|
+
reject(new LambderMockTransportError("timeout"));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
signal.addEventListener("abort", () => reject(new LambderMockTransportError("timeout")), { once: true });
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/** The answer an injected failure produces, or a throw for the ones that never reach the caller as answers. */
|
|
113
|
+
async answerFor(failure, request) {
|
|
114
|
+
switch (failure.reason) {
|
|
115
|
+
case "network": throw new LambderMockTransportError("network");
|
|
116
|
+
case "timeout": return await this.waitForAbort(request.signal);
|
|
117
|
+
case "server": return crashAnswer(this.apiVersion);
|
|
118
|
+
case "refusal": {
|
|
119
|
+
const message = toRefusalMessage(failure.message, "Injected refusal.");
|
|
120
|
+
return refusalAnswer(new LambderApiRefusal(message.content, { errorMessage: message, statusCode: failure.statusCode }), this.apiVersion);
|
|
121
|
+
}
|
|
122
|
+
case "notAuthorized": {
|
|
123
|
+
const message = toRefusalMessage(failure.message, "Injected authorization refusal.");
|
|
124
|
+
return refusalAnswer(new LambderApiRefusal(message.content, { errorMessage: message, notAuthorized: true }), this.apiVersion);
|
|
125
|
+
}
|
|
126
|
+
case "sessionExpired": return sessionExpiredAnswer(this.apiVersion);
|
|
127
|
+
case "versionExpired": return versionExpiredAnswer(this.apiVersion);
|
|
128
|
+
case "rateLimited": return refusalAnswer(rateLimitRefusal("Injected rate limit.", failure.retryAfterSeconds ?? 30, failure.message), this.apiVersion);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Clears every injected failure and puts the configured latency back. */
|
|
132
|
+
reset() {
|
|
133
|
+
this.failuresNext.clear();
|
|
134
|
+
this.failuresSet.clear();
|
|
135
|
+
this.offlineFlag = false;
|
|
136
|
+
this.latency = this.initialLatency;
|
|
137
|
+
}
|
|
138
|
+
}
|