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
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type
|
|
3
|
-
import
|
|
4
|
-
import { type
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type
|
|
1
|
+
import type { LambderAppRefusalMessage } from '../shared/wire/LambderApiRefusal.js';
|
|
2
|
+
import { type LambderRequestCompressionOption } from '../shared/wire/LambderRequestPayload.js';
|
|
3
|
+
import type { LambderApiContractShape } from '../shared/wire/LambderApiContract.js';
|
|
4
|
+
import { type LambderApiOutcome, type LambderValidationError } from '../shared/wire/LambderApiOutcome.js';
|
|
5
|
+
import { type LambderCallArgs, type LambderContractOutputOf, type LambderGuardInputsProviderOption, type LambderSharedCallOptions } from '../shared/wire/LambderCallOptions.js';
|
|
6
|
+
import { type LambderApiTransport } from '../shared/transport/LambderApiTransport.js';
|
|
7
|
+
export type { LambderApiOutcome, LambderApiFailureReason, LambderValidationError } from '../shared/wire/LambderApiOutcome.js';
|
|
8
|
+
export type { LambderProvidedGuardInputs, LambderGuardInputsProvider } from '../shared/wire/LambderCallOptions.js';
|
|
9
|
+
/** A handler told that something happened, with nothing to hand it. */
|
|
10
|
+
type NotifyHandler = () => void | Promise<void>;
|
|
11
|
+
/** One call in flight: pushed when it starts, removed when it settles, so the list is the in-flight list rather than a log of every call ever made. */
|
|
8
12
|
type FetchTracker = {
|
|
9
13
|
apiName: string;
|
|
10
|
-
done: boolean;
|
|
11
|
-
fetchEndCalled: boolean;
|
|
12
14
|
};
|
|
13
15
|
type EventHandlerFetchParams = {
|
|
14
16
|
apiName: string;
|
|
15
17
|
payload?: any;
|
|
16
|
-
headers?: Record<string,
|
|
18
|
+
headers?: Record<string, string>;
|
|
17
19
|
};
|
|
18
20
|
type FetchStartEventHandler = (params: {
|
|
19
21
|
fetchParams: EventHandlerFetchParams;
|
|
@@ -26,7 +28,9 @@ type FetchEndEventHandler = (params: {
|
|
|
26
28
|
}) => void | Promise<void>;
|
|
27
29
|
type ErrorHandler = (err: Error) => void | Promise<void>;
|
|
28
30
|
type ValidationErrorHandler = (zodError: LambderValidationError) => (void | false) | Promise<(void | false)>;
|
|
29
|
-
type MessageHandler = (message:
|
|
31
|
+
type MessageHandler = (message: LambderAppRefusalMessage | string) => void | Promise<void>;
|
|
32
|
+
/** The logListHandler option: an answer's logList, success or failure, when it has entries. The invoke caller's onLogList, for a browser. */
|
|
33
|
+
export type LambderLogListHandler = (apiName: string, logList: unknown[]) => void | Promise<void>;
|
|
30
34
|
/** One logical operation's rotating idempotency key: see LambderCaller.createIdempotencyKeyScope(). */
|
|
31
35
|
export type LambderIdempotencyKeyScope = {
|
|
32
36
|
/** The key for the operation currently in progress. */
|
|
@@ -34,45 +38,19 @@ export type LambderIdempotencyKeyScope = {
|
|
|
34
38
|
/** Call after a confirmed success: the next operation is a new intent. Returns the new key. */
|
|
35
39
|
rotate(): string;
|
|
36
40
|
};
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Overrides the constructor's requestCompression for this call: `false`
|
|
46
|
-
* sends the payload plainly (a hot path where the CPU matters more than
|
|
47
|
-
* the bytes), `true` compresses it regardless of the size threshold.
|
|
48
|
-
* Either way a payload is only sent compressed when that is smaller.
|
|
49
|
-
*/
|
|
50
|
-
compressRequest?: boolean;
|
|
51
|
-
/**
|
|
52
|
-
* Values for the API's guardInput-mode guards, keyed by guard name; sent
|
|
53
|
-
* beside the payload and consumed by the guards before validation. The
|
|
54
|
-
* typed contract makes this REQUIRED for APIs that declare such guards,
|
|
55
|
-
* except the guards a guardInputsProvider covers (these merge on top of
|
|
56
|
-
* the provider's values).
|
|
57
|
-
*/
|
|
58
|
-
guardInputs?: Record<string, unknown>;
|
|
59
|
-
/**
|
|
60
|
-
* Replay-protection key for APIs declared idempotent on the server.
|
|
61
|
-
* Generate once per logical operation with createIdempotencyKey() and
|
|
62
|
-
* send the same key on retries: duplicates of an in-flight request
|
|
63
|
-
* refuse, and repeats of a completed one replay its stored response
|
|
64
|
-
* instead of re-executing. Must be UNGUESSABLE random (it scopes the
|
|
65
|
-
* replay record for logged-out clients) and at least 16 characters; the
|
|
66
|
-
* server refuses shorter keys with a 400.
|
|
67
|
-
*/
|
|
68
|
-
idempotencyKey?: string;
|
|
69
|
-
versionExpiredHandler?: VoidFunction;
|
|
70
|
-
sessionExpiredHandler?: VoidFunction;
|
|
41
|
+
/**
|
|
42
|
+
* Per-call options: the request extras both callers share (see
|
|
43
|
+
* LambderSharedCallOptions) plus an override for every constructor handler.
|
|
44
|
+
*/
|
|
45
|
+
export type LambderCallOptions = LambderSharedCallOptions & {
|
|
46
|
+
versionExpiredHandler?: NotifyHandler;
|
|
47
|
+
sessionExpiredHandler?: NotifyHandler;
|
|
71
48
|
messageHandler?: MessageHandler;
|
|
72
49
|
errorMessageHandler?: MessageHandler;
|
|
73
50
|
apiInputValidationErrorHandler?: ValidationErrorHandler;
|
|
74
|
-
notAuthorizedHandler?:
|
|
51
|
+
notAuthorizedHandler?: NotifyHandler;
|
|
75
52
|
errorHandler?: ErrorHandler;
|
|
53
|
+
logListHandler?: LambderLogListHandler;
|
|
76
54
|
fetchStartedHandler?: FetchStartEventHandler;
|
|
77
55
|
fetchEndedHandler?: FetchEndEventHandler;
|
|
78
56
|
};
|
|
@@ -82,12 +60,14 @@ type LambderCallerBaseOptions = {
|
|
|
82
60
|
isCorsEnabled: boolean;
|
|
83
61
|
/** Default per-request timeout in ms (none unless set; API Gateway caps around 29s, so ~30000 is a sensible value). Overridable per call. */
|
|
84
62
|
timeoutMs?: number;
|
|
85
|
-
versionExpiredHandler?:
|
|
86
|
-
sessionExpiredHandler?:
|
|
63
|
+
versionExpiredHandler?: NotifyHandler;
|
|
64
|
+
sessionExpiredHandler?: NotifyHandler;
|
|
87
65
|
messageHandler?: MessageHandler;
|
|
88
66
|
errorMessageHandler?: MessageHandler;
|
|
89
|
-
notAuthorizedHandler?:
|
|
67
|
+
notAuthorizedHandler?: NotifyHandler;
|
|
90
68
|
errorHandler?: ErrorHandler;
|
|
69
|
+
/** Receives each answer's logList, with the API name. Default: console.log with a `[lambder]` prefix, one line per entry. */
|
|
70
|
+
logListHandler?: LambderLogListHandler;
|
|
91
71
|
fetchStartedHandler?: FetchStartEventHandler;
|
|
92
72
|
fetchEndedHandler?: FetchEndEventHandler;
|
|
93
73
|
apiInputValidationErrorHandler?: ValidationErrorHandler;
|
|
@@ -103,6 +83,13 @@ type LambderCallerBaseOptions = {
|
|
|
103
83
|
* Lambda's ~6MB invoke cap, which applies to the compressed bytes.
|
|
104
84
|
*/
|
|
105
85
|
requestCompression?: LambderRequestCompressionOption;
|
|
86
|
+
/**
|
|
87
|
+
* How a call reaches the server. Default: fetch to apiPath
|
|
88
|
+
* (lambderFetchTransport, with CORS per isCorsEnabled). A mock runtime,
|
|
89
|
+
* an in-process Lambder handler, or a cookie-jar decorator over either
|
|
90
|
+
* are the other transports that ship; see LambderApiTransport.
|
|
91
|
+
*/
|
|
92
|
+
transport?: LambderApiTransport;
|
|
106
93
|
};
|
|
107
94
|
/** Constructor options: the base options plus guardInputsProvider, mandatory once TProvided names guards. */
|
|
108
95
|
export type LambderCallerOptions<TContract, TProvided extends string = never> = LambderCallerBaseOptions & LambderGuardInputsProviderOption<TContract, TProvided>;
|
|
@@ -110,13 +97,15 @@ export type LambderCallerOptions<TContract, TProvided extends string = never> =
|
|
|
110
97
|
* @typeParam TContract - The API contract, for typed names, payloads and guard inputs.
|
|
111
98
|
* @typeParam TProvidedGuards - Guard names guardInputsProvider covers; those APIs' options argument becomes optional.
|
|
112
99
|
*/
|
|
113
|
-
export default class LambderCaller<TContract extends
|
|
100
|
+
export default class LambderCaller<TContract extends LambderApiContractShape = any, TProvidedGuards extends string = never> {
|
|
114
101
|
private isCorsEnabled;
|
|
115
102
|
private apiPath;
|
|
116
103
|
private apiVersion?;
|
|
117
104
|
private timeoutMs?;
|
|
105
|
+
/** The calls currently in flight, in the order they started. */
|
|
118
106
|
fetchTrackerList: FetchTracker[];
|
|
119
|
-
|
|
107
|
+
/** Whether any call is in flight. Derived, so it cannot drift from the list the way a separate flag did. */
|
|
108
|
+
get isLoading(): boolean;
|
|
120
109
|
private versionExpiredHandler?;
|
|
121
110
|
private sessionExpiredHandler?;
|
|
122
111
|
private messageHandler?;
|
|
@@ -124,6 +113,7 @@ export default class LambderCaller<TContract extends ApiContractShape = any, TPr
|
|
|
124
113
|
private notAuthorizedHandler?;
|
|
125
114
|
private errorHandler?;
|
|
126
115
|
private apiInputValidationErrorHandler?;
|
|
116
|
+
private logListHandler?;
|
|
127
117
|
private fetchStartedHandler?;
|
|
128
118
|
private fetchEndedHandler?;
|
|
129
119
|
private guardInputsProvider?;
|
|
@@ -131,8 +121,11 @@ export default class LambderCaller<TContract extends ApiContractShape = any, TPr
|
|
|
131
121
|
private sessionCsrfCookieKey;
|
|
132
122
|
private sessionCookieDomain?;
|
|
133
123
|
private requestCompression;
|
|
124
|
+
private transport;
|
|
134
125
|
constructor(options: LambderCallerOptions<TContract, TProvidedGuards>);
|
|
135
126
|
setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string): void;
|
|
127
|
+
/** Replaces how calls reach the server: a mock runtime, an in-process handler, a decorated transport. */
|
|
128
|
+
setTransport(transport: LambderApiTransport): this;
|
|
136
129
|
/**
|
|
137
130
|
* A self-rotating idempotency key for a component or form that performs
|
|
138
131
|
* the same logical operation repeatedly. `current` is the key for the
|
|
@@ -155,6 +148,11 @@ export default class LambderCaller<TContract extends ApiContractShape = any, TPr
|
|
|
155
148
|
* confirmed success. Uses crypto.randomUUID when available and falls back
|
|
156
149
|
* to a v4 UUID from getRandomValues, because randomUUID only exists in
|
|
157
150
|
* secure contexts (plain-http LAN device testing lacks it).
|
|
151
|
+
*
|
|
152
|
+
* A runtime with neither throws rather than reaching for Math.random: the
|
|
153
|
+
* key must be UNGUESSABLE, since it is what scopes the replay record for a
|
|
154
|
+
* logged-out client, and a guessable one hands that client's stored
|
|
155
|
+
* response to whoever guesses it.
|
|
158
156
|
*/
|
|
159
157
|
static createIdempotencyKey(): string;
|
|
160
158
|
private clearSessionCookies;
|
|
@@ -162,9 +160,18 @@ export default class LambderCaller<TContract extends ApiContractShape = any, TPr
|
|
|
162
160
|
private dispatch;
|
|
163
161
|
/**
|
|
164
162
|
* Full-fidelity call: resolves to a discriminated LambderApiOutcome
|
|
165
|
-
* instead of collapsing every failure to
|
|
163
|
+
* instead of collapsing every failure to undefined. Never throws.
|
|
164
|
+
*
|
|
165
|
+
* The output is computed from the contract in the return type rather than
|
|
166
|
+
* taken as a type parameter, so a call site cannot replace it by
|
|
167
|
+
* annotating what it assigns to.
|
|
168
|
+
*/
|
|
169
|
+
apiOutcome<TApiName extends keyof TContract & string = string>(apiName: TApiName, ...rest: LambderCallArgs<TContract, TApiName, TProvidedGuards, LambderCallOptions>): Promise<LambderApiOutcome<LambderContractOutputOf<TContract, TApiName>>>;
|
|
170
|
+
/**
|
|
171
|
+
* The payload on success, `undefined` on every failure except a
|
|
172
|
+
* structured refusal, which hands back whatever payload the envelope
|
|
173
|
+
* carried (usually null). Neither is distinguishable from a legitimately
|
|
174
|
+
* null or undefined payload: use apiOutcome() when that matters.
|
|
166
175
|
*/
|
|
167
|
-
|
|
168
|
-
/** Payload on success, null/undefined otherwise (indistinguishable from a null payload; prefer apiOutcome() when that matters). */
|
|
169
|
-
api<TApiName extends keyof TContract & string = string, TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any>(apiName: TApiName, payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any, ...rest: LambderCallOptionsArg<TContract, TApiName, TProvidedGuards, LambderCallOptions>): Promise<TOutput | null | undefined>;
|
|
176
|
+
api<TApiName extends keyof TContract & string = string>(apiName: TApiName, ...rest: LambderCallArgs<TContract, TApiName, TProvidedGuards, LambderCallOptions>): Promise<LambderContractOutputOf<TContract, TApiName> | null | undefined>;
|
|
170
177
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import Cookies from 'js-cookie';
|
|
2
|
-
import { compressPayloadGzip, isRequestCompressionAvailable, DEFAULT_REQUEST_COMPRESSION_SETTINGS, } from '../shared/LambderRequestPayload.js';
|
|
3
|
-
import { resolveCompressionOption } from '../shared/LambderCompressionOption.js';
|
|
4
|
-
import { resolveApiOutcome } from '../shared/LambderApiOutcome.js';
|
|
5
|
-
import { mergeGuardInputs, } from '../shared/LambderCallOptions.js';
|
|
2
|
+
import { compressPayloadGzip, isRequestCompressionAvailable, resolveRequestCompressionMinBytes, DEFAULT_REQUEST_COMPRESSION_SETTINGS, } from '../shared/wire/LambderRequestPayload.js';
|
|
3
|
+
import { resolveCompressionOption } from '../shared/wire/LambderCompressionOption.js';
|
|
4
|
+
import { resolveApiOutcome } from '../shared/wire/LambderApiOutcome.js';
|
|
5
|
+
import { mergeGuardInputs, } from '../shared/wire/LambderCallOptions.js';
|
|
6
|
+
import { createCallAbort } from '../shared/util/LambderCallAbort.js';
|
|
7
|
+
import { coerceToError } from '../shared/wire/LambderCrashDetail.js';
|
|
8
|
+
import { isLambderTransportFailure } from '../shared/transport/LambderApiTransport.js';
|
|
9
|
+
import { DEFAULT_SESSION_TOKEN_COOKIE_KEY, DEFAULT_SESSION_CSRF_COOKIE_KEY } from '../shared/wire/LambderSessionCookieNames.js';
|
|
10
|
+
import { lambderFetchTransport } from './lambderFetchTransport.js';
|
|
6
11
|
/**
|
|
7
12
|
* @typeParam TContract - The API contract, for typed names, payloads and guard inputs.
|
|
8
13
|
* @typeParam TProvidedGuards - Guard names guardInputsProvider covers; those APIs' options argument becomes optional.
|
|
@@ -12,8 +17,10 @@ export default class LambderCaller {
|
|
|
12
17
|
apiPath;
|
|
13
18
|
apiVersion;
|
|
14
19
|
timeoutMs;
|
|
20
|
+
/** The calls currently in flight, in the order they started. */
|
|
15
21
|
fetchTrackerList = [];
|
|
16
|
-
|
|
22
|
+
/** Whether any call is in flight. Derived, so it cannot drift from the list the way a separate flag did. */
|
|
23
|
+
get isLoading() { return this.fetchTrackerList.length > 0; }
|
|
17
24
|
versionExpiredHandler;
|
|
18
25
|
sessionExpiredHandler;
|
|
19
26
|
messageHandler;
|
|
@@ -21,24 +28,27 @@ export default class LambderCaller {
|
|
|
21
28
|
notAuthorizedHandler;
|
|
22
29
|
errorHandler;
|
|
23
30
|
apiInputValidationErrorHandler;
|
|
31
|
+
logListHandler;
|
|
24
32
|
fetchStartedHandler;
|
|
25
33
|
fetchEndedHandler;
|
|
26
34
|
guardInputsProvider;
|
|
27
|
-
sessionTokenCookieKey =
|
|
28
|
-
sessionCsrfCookieKey =
|
|
35
|
+
sessionTokenCookieKey = DEFAULT_SESSION_TOKEN_COOKIE_KEY;
|
|
36
|
+
sessionCsrfCookieKey = DEFAULT_SESSION_CSRF_COOKIE_KEY;
|
|
29
37
|
sessionCookieDomain;
|
|
30
38
|
requestCompression;
|
|
39
|
+
transport;
|
|
31
40
|
constructor(options) {
|
|
32
41
|
// The conditional provider option is resolved per instantiation;
|
|
33
42
|
// inside the class it is read through the plain shape.
|
|
34
|
-
const { apiPath, apiVersion, isCorsEnabled
|
|
35
|
-
this.apiPath = apiPath
|
|
43
|
+
const { apiPath, apiVersion, isCorsEnabled, timeoutMs, versionExpiredHandler, sessionExpiredHandler, messageHandler, errorMessageHandler, notAuthorizedHandler, errorHandler, logListHandler, fetchStartedHandler, fetchEndedHandler, apiInputValidationErrorHandler, sessionCookieDomain, requestCompression, guardInputsProvider, transport, } = options;
|
|
44
|
+
this.apiPath = apiPath;
|
|
36
45
|
this.apiVersion = apiVersion;
|
|
37
46
|
this.isCorsEnabled = isCorsEnabled;
|
|
38
47
|
this.timeoutMs = timeoutMs;
|
|
39
48
|
this.sessionCookieDomain = sessionCookieDomain;
|
|
40
49
|
// `?? false`: unlike the at-rest stores, this one is off unless asked for.
|
|
41
50
|
this.requestCompression = resolveCompressionOption(requestCompression ?? false, DEFAULT_REQUEST_COMPRESSION_SETTINGS);
|
|
51
|
+
this.transport = transport ?? lambderFetchTransport({ cors: this.isCorsEnabled });
|
|
42
52
|
this.versionExpiredHandler = versionExpiredHandler;
|
|
43
53
|
this.sessionExpiredHandler = sessionExpiredHandler;
|
|
44
54
|
this.messageHandler = messageHandler;
|
|
@@ -46,6 +56,7 @@ export default class LambderCaller {
|
|
|
46
56
|
this.notAuthorizedHandler = notAuthorizedHandler;
|
|
47
57
|
this.errorHandler = errorHandler;
|
|
48
58
|
this.apiInputValidationErrorHandler = apiInputValidationErrorHandler;
|
|
59
|
+
this.logListHandler = logListHandler;
|
|
49
60
|
this.fetchStartedHandler = fetchStartedHandler;
|
|
50
61
|
this.fetchEndedHandler = fetchEndedHandler;
|
|
51
62
|
this.guardInputsProvider = guardInputsProvider;
|
|
@@ -55,6 +66,11 @@ export default class LambderCaller {
|
|
|
55
66
|
this.sessionTokenCookieKey = sessionTokenCookieKey;
|
|
56
67
|
this.sessionCsrfCookieKey = sessionCsrfCookieKey;
|
|
57
68
|
}
|
|
69
|
+
/** Replaces how calls reach the server: a mock runtime, an in-process handler, a decorated transport. */
|
|
70
|
+
setTransport(transport) {
|
|
71
|
+
this.transport = transport;
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
58
74
|
/**
|
|
59
75
|
* A self-rotating idempotency key for a component or form that performs
|
|
60
76
|
* the same logical operation repeatedly. `current` is the key for the
|
|
@@ -83,20 +99,20 @@ export default class LambderCaller {
|
|
|
83
99
|
* confirmed success. Uses crypto.randomUUID when available and falls back
|
|
84
100
|
* to a v4 UUID from getRandomValues, because randomUUID only exists in
|
|
85
101
|
* secure contexts (plain-http LAN device testing lacks it).
|
|
102
|
+
*
|
|
103
|
+
* A runtime with neither throws rather than reaching for Math.random: the
|
|
104
|
+
* key must be UNGUESSABLE, since it is what scopes the replay record for a
|
|
105
|
+
* logged-out client, and a guessable one hands that client's stored
|
|
106
|
+
* response to whoever guesses it.
|
|
86
107
|
*/
|
|
87
108
|
static createIdempotencyKey() {
|
|
88
109
|
const cryptoObj = globalThis.crypto;
|
|
89
110
|
if (cryptoObj?.randomUUID)
|
|
90
111
|
return cryptoObj.randomUUID();
|
|
112
|
+
if (!cryptoObj?.getRandomValues)
|
|
113
|
+
throw new Error("LambderCaller.createIdempotencyKey needs crypto.getRandomValues: an idempotency key must be unguessable, and this runtime offers no random source that is.");
|
|
91
114
|
const bytes = new Uint8Array(16);
|
|
92
|
-
|
|
93
|
-
cryptoObj.getRandomValues(bytes);
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
for (let i = 0; i < 16; i += 1) {
|
|
97
|
-
bytes[i] = Math.floor(Math.random() * 256);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
115
|
+
cryptoObj.getRandomValues(bytes);
|
|
100
116
|
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
101
117
|
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
102
118
|
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
@@ -104,7 +120,7 @@ export default class LambderCaller {
|
|
|
104
120
|
}
|
|
105
121
|
clearSessionCookies() {
|
|
106
122
|
const domainOption = this.sessionCookieDomain;
|
|
107
|
-
const hostname =
|
|
123
|
+
const hostname = globalThis.location?.hostname ?? "";
|
|
108
124
|
const resolvedDomain = typeof domainOption === "function" ? domainOption(hostname) : domainOption;
|
|
109
125
|
for (const key of [this.sessionTokenCookieKey, this.sessionCsrfCookieKey]) {
|
|
110
126
|
// Host-only and domain-scoped cookies are distinct entries; clear both.
|
|
@@ -126,19 +142,31 @@ export default class LambderCaller {
|
|
|
126
142
|
const notAuthorizedHandler = options?.notAuthorizedHandler ?? this.notAuthorizedHandler;
|
|
127
143
|
const errorHandler = options?.errorHandler ?? this.errorHandler;
|
|
128
144
|
const apiInputValidationErrorHandler = options?.apiInputValidationErrorHandler ?? this.apiInputValidationErrorHandler;
|
|
145
|
+
const logListHandler = options?.logListHandler ?? this.logListHandler;
|
|
129
146
|
const fetchStartedHandler = options?.fetchStartedHandler ?? this.fetchStartedHandler;
|
|
130
147
|
const fetchEndedHandler = options?.fetchEndedHandler ?? this.fetchEndedHandler;
|
|
131
148
|
const headers = options?.headers;
|
|
132
|
-
const fetchTracker = { apiName
|
|
149
|
+
const fetchTracker = { apiName };
|
|
150
|
+
// Dropped the moment the call settles, and idempotently, since the
|
|
151
|
+
// finally block below runs for the paths fetchEnded never reaches.
|
|
152
|
+
// Left in, the list grew by one per call forever, and every handler
|
|
153
|
+
// call scanned all of it: a long-lived page paid more per call the
|
|
154
|
+
// longer it had been open.
|
|
155
|
+
const dropFetchTracker = () => {
|
|
156
|
+
const at = this.fetchTrackerList.indexOf(fetchTracker);
|
|
157
|
+
if (at !== -1)
|
|
158
|
+
this.fetchTrackerList.splice(at, 1);
|
|
159
|
+
};
|
|
160
|
+
let fetchEndCalled = false;
|
|
133
161
|
const fetchEnded = async (fetchResult) => {
|
|
134
|
-
|
|
135
|
-
if (
|
|
162
|
+
dropFetchTracker();
|
|
163
|
+
if (fetchEndCalled || !fetchEndedHandler)
|
|
136
164
|
return;
|
|
137
|
-
|
|
165
|
+
fetchEndCalled = true;
|
|
138
166
|
await fetchEndedHandler({
|
|
139
167
|
fetchParams: { apiName, payload, headers },
|
|
140
168
|
fetchResult,
|
|
141
|
-
activeFetchList: this.fetchTrackerList
|
|
169
|
+
activeFetchList: [...this.fetchTrackerList],
|
|
142
170
|
});
|
|
143
171
|
};
|
|
144
172
|
let errorHandlerCalled = false;
|
|
@@ -148,36 +176,33 @@ export default class LambderCaller {
|
|
|
148
176
|
errorHandlerCalled = true;
|
|
149
177
|
await errorHandler(err);
|
|
150
178
|
};
|
|
151
|
-
// Timeout
|
|
152
|
-
//
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
externalSignal.addEventListener("abort", () => controller.abort(externalSignal.reason), { once: true });
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
timeoutId = setTimeout(() => { timedOut = true; controller.abort(); }, timeoutMs);
|
|
169
|
-
signal = controller.signal;
|
|
170
|
-
}
|
|
179
|
+
// Timeout and abort wiring, shared with LambderInvokeCaller so the two
|
|
180
|
+
// cannot drift on what a late or abandoned call means.
|
|
181
|
+
const abort = createCallAbort({ timeoutMs: options?.timeoutMs ?? this.timeoutMs, signal: options?.signal });
|
|
182
|
+
const signal = abort.signal;
|
|
183
|
+
/** Reports a call that was given up on, or null while it still stands. */
|
|
184
|
+
const abandonedOutcome = async (stage) => {
|
|
185
|
+
const failure = abort.abortFailure(stage);
|
|
186
|
+
if (!failure)
|
|
187
|
+
return null;
|
|
188
|
+
await fetchEnded(failure.error);
|
|
189
|
+
await reportError(failure.error);
|
|
190
|
+
const outcome = { ok: false, reason: failure.reason, error: failure.error };
|
|
191
|
+
return outcome;
|
|
192
|
+
};
|
|
171
193
|
try {
|
|
172
194
|
this.fetchTrackerList.push(fetchTracker);
|
|
173
195
|
if (fetchStartedHandler)
|
|
174
196
|
await fetchStartedHandler({
|
|
175
197
|
fetchParams: { apiName, payload, headers, },
|
|
176
|
-
activeFetchList: this.fetchTrackerList
|
|
198
|
+
activeFetchList: [...this.fetchTrackerList],
|
|
177
199
|
});
|
|
178
200
|
const version = this.apiVersion;
|
|
201
|
+
// js-cookie reads nothing without a document, and there is no
|
|
202
|
+
// location outside a page: both are "" then, and a transport that
|
|
203
|
+
// carries a cookie jar fills the token in from it.
|
|
179
204
|
const token = Cookies.get(this.sessionCsrfCookieKey) || "";
|
|
180
|
-
const siteHost =
|
|
205
|
+
const siteHost = globalThis.location?.hostname ?? "";
|
|
181
206
|
// Provider values underneath, per-call values on top.
|
|
182
207
|
const providedGuardInputs = this.guardInputsProvider
|
|
183
208
|
? await this.guardInputsProvider(apiName)
|
|
@@ -188,45 +213,65 @@ export default class LambderCaller {
|
|
|
188
213
|
// without CompressionStream always sends the payload plainly.
|
|
189
214
|
// Nothing here runs (the extra stringify included) unless
|
|
190
215
|
// compression is actually a possibility for this call.
|
|
191
|
-
const compressionMinBytes = options?.compressRequest
|
|
192
|
-
: options?.compressRequest === false ? null
|
|
193
|
-
: this.requestCompression?.minBytes ?? null;
|
|
216
|
+
const compressionMinBytes = resolveRequestCompressionMinBytes(options?.compressRequest, this.requestCompression);
|
|
194
217
|
const compressedPayload = compressionMinBytes !== null && payload !== undefined && isRequestCompressionAvailable()
|
|
195
218
|
? await compressPayloadGzip(JSON.stringify(payload), compressionMinBytes)
|
|
196
219
|
: null;
|
|
197
|
-
|
|
220
|
+
// A call the site has already given up on does not reach the
|
|
221
|
+
// transport at all: honouring request.signal is the transport's
|
|
222
|
+
// obligation, and not every transport does.
|
|
223
|
+
const refused = await abandonedOutcome("beforeSending");
|
|
224
|
+
if (refused)
|
|
225
|
+
return refused;
|
|
226
|
+
let answer;
|
|
198
227
|
try {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
apiName, version, token, siteHost,
|
|
208
|
-
...(compressedPayload ?? { payload }),
|
|
209
|
-
...(guardInputs !== undefined ? { guardInputs } : {}),
|
|
210
|
-
...(options?.idempotencyKey !== undefined ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
211
|
-
}),
|
|
228
|
+
answer = await this.transport({
|
|
229
|
+
apiPath: this.apiPath,
|
|
230
|
+
apiName, version, token, siteHost,
|
|
231
|
+
csrfCookieKey: this.sessionCsrfCookieKey,
|
|
232
|
+
...(compressedPayload ? { compressed: compressedPayload } : { payload }),
|
|
233
|
+
...(guardInputs !== undefined ? { guardInputs } : {}),
|
|
234
|
+
...(options?.idempotencyKey !== undefined ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
235
|
+
...(headers ? { headers } : {}),
|
|
212
236
|
...(signal ? { signal } : {}),
|
|
213
237
|
});
|
|
214
238
|
}
|
|
215
239
|
catch (err) {
|
|
216
|
-
const wrappedError = err
|
|
240
|
+
const wrappedError = coerceToError(err, "Request failed");
|
|
217
241
|
await fetchEnded(wrappedError);
|
|
218
242
|
await reportError(wrappedError);
|
|
219
|
-
|
|
243
|
+
// The caller's own abort wins, since only it knows about that.
|
|
244
|
+
// Otherwise a transport that named its reason is believed:
|
|
245
|
+
// "protocol" means something came back and was not an answer,
|
|
246
|
+
// which is what this caller already calls `server`.
|
|
247
|
+
const reason = abort.timedOut() ? 'timeout'
|
|
248
|
+
: isLambderTransportFailure(err) && err.reason === 'protocol' ? 'server'
|
|
249
|
+
: 'network';
|
|
250
|
+
return { ok: false, reason, error: wrappedError };
|
|
220
251
|
}
|
|
252
|
+
// An answer that arrives after the call was given up on is not a
|
|
253
|
+
// success. A transport that ignores request.signal resolves late,
|
|
254
|
+
// and believing it would report ok on a 20ms timeoutMs 300ms in,
|
|
255
|
+
// handing the call site data it had already abandoned.
|
|
256
|
+
const late = await abandonedOutcome("afterAnswering");
|
|
257
|
+
if (late)
|
|
258
|
+
return late;
|
|
221
259
|
// The reading of the answer is shared with LambderInvokeCaller;
|
|
222
260
|
// only what to do about each outcome is this caller's.
|
|
223
|
-
const outcome = await resolveApiOutcome(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
261
|
+
const outcome = await resolveApiOutcome(answer);
|
|
262
|
+
// Every answer's logs, surfaced once and before any branch that
|
|
263
|
+
// returns: the 500 whose global error handler attached a crash and
|
|
264
|
+
// a logList is the answer whose log trail is worth the most, and
|
|
265
|
+
// surfacing them under the envelope reads meant it was the one
|
|
266
|
+
// answer that never reached logListHandler at all.
|
|
267
|
+
const logList = outcome.logList;
|
|
268
|
+
if (logList?.length) {
|
|
269
|
+
if (logListHandler)
|
|
270
|
+
await logListHandler(apiName, logList);
|
|
271
|
+
else
|
|
272
|
+
for (const record of logList)
|
|
273
|
+
console.log("[lambder]", record);
|
|
274
|
+
}
|
|
230
275
|
if (!outcome.ok && outcome.reason === 'server') {
|
|
231
276
|
await fetchEnded(outcome.error);
|
|
232
277
|
await reportError(outcome.error);
|
|
@@ -242,13 +287,10 @@ export default class LambderCaller {
|
|
|
242
287
|
}
|
|
243
288
|
return outcome;
|
|
244
289
|
}
|
|
290
|
+
// Whatever is left carries the envelope: a success or one of the
|
|
291
|
+
// envelope's own refusals, which is why no assertion is needed.
|
|
245
292
|
const data = outcome.response;
|
|
246
293
|
await fetchEnded(data);
|
|
247
|
-
if (data.logList?.length) {
|
|
248
|
-
for (const record of data.logList) {
|
|
249
|
-
console.log("[lambder]", record);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
294
|
if (!outcome.ok && outcome.reason === 'versionExpired') {
|
|
253
295
|
if (versionExpiredHandler) {
|
|
254
296
|
await versionExpiredHandler();
|
|
@@ -277,11 +319,13 @@ export default class LambderCaller {
|
|
|
277
319
|
}
|
|
278
320
|
return outcome;
|
|
279
321
|
}
|
|
280
|
-
|
|
322
|
+
// Presence, not truthiness: the envelope keeps a message an app
|
|
323
|
+
// spelled out as the empty string, so the handler runs for it.
|
|
324
|
+
if (data.message !== undefined && messageHandler) {
|
|
281
325
|
await messageHandler(data.message);
|
|
282
326
|
}
|
|
283
327
|
if (!outcome.ok && outcome.reason === 'errorMessage') {
|
|
284
|
-
if (errorMessageHandler) {
|
|
328
|
+
if (errorMessageHandler && data.errorMessage !== undefined) {
|
|
285
329
|
await errorMessageHandler(data.errorMessage);
|
|
286
330
|
}
|
|
287
331
|
return outcome;
|
|
@@ -291,7 +335,7 @@ export default class LambderCaller {
|
|
|
291
335
|
catch (err) {
|
|
292
336
|
// Escape hatch for anything above (typically an app handler throwing):
|
|
293
337
|
// dispatch never throws, so api()/apiOutcome() call sites never do.
|
|
294
|
-
const wrappedError = err
|
|
338
|
+
const wrappedError = coerceToError(err, "The call failed before it produced an outcome");
|
|
295
339
|
try {
|
|
296
340
|
await fetchEnded(wrappedError);
|
|
297
341
|
await reportError(wrappedError);
|
|
@@ -300,25 +344,38 @@ export default class LambderCaller {
|
|
|
300
344
|
return { ok: false, reason: 'unknown', error: wrappedError };
|
|
301
345
|
}
|
|
302
346
|
finally {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
clearTimeout(timeoutId);
|
|
347
|
+
dropFetchTracker();
|
|
348
|
+
abort.detach();
|
|
306
349
|
}
|
|
307
350
|
}
|
|
308
351
|
;
|
|
309
352
|
/**
|
|
310
353
|
* Full-fidelity call: resolves to a discriminated LambderApiOutcome
|
|
311
|
-
* instead of collapsing every failure to
|
|
354
|
+
* instead of collapsing every failure to undefined. Never throws.
|
|
355
|
+
*
|
|
356
|
+
* The output is computed from the contract in the return type rather than
|
|
357
|
+
* taken as a type parameter, so a call site cannot replace it by
|
|
358
|
+
* annotating what it assigns to.
|
|
312
359
|
*/
|
|
313
|
-
async apiOutcome(apiName,
|
|
314
|
-
|
|
360
|
+
async apiOutcome(apiName, ...rest) {
|
|
361
|
+
// The tuple is a conditional type on an unresolved TApiName, so its
|
|
362
|
+
// elements read as unknown from inside; the contract shaped them on
|
|
363
|
+
// the way in, which is where the guarantee belongs.
|
|
364
|
+
const [payload, options] = rest;
|
|
365
|
+
return await this.dispatch(apiName, payload, options);
|
|
315
366
|
}
|
|
316
367
|
;
|
|
317
|
-
/**
|
|
318
|
-
|
|
319
|
-
|
|
368
|
+
/**
|
|
369
|
+
* The payload on success, `undefined` on every failure except a
|
|
370
|
+
* structured refusal, which hands back whatever payload the envelope
|
|
371
|
+
* carried (usually null). Neither is distinguishable from a legitimately
|
|
372
|
+
* null or undefined payload: use apiOutcome() when that matters.
|
|
373
|
+
*/
|
|
374
|
+
async api(apiName, ...rest) {
|
|
375
|
+
const [payload, options] = rest;
|
|
376
|
+
const outcome = await this.dispatch(apiName, payload, options);
|
|
320
377
|
if (outcome.ok)
|
|
321
|
-
return outcome.
|
|
322
|
-
return outcome.reason === 'errorMessage' ? outcome.response
|
|
378
|
+
return outcome.payload;
|
|
379
|
+
return outcome.reason === 'errorMessage' ? outcome.response.payload : undefined;
|
|
323
380
|
}
|
|
324
381
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LambderApiTransport } from "../shared/transport/LambderApiTransport.js";
|
|
2
|
+
/**
|
|
3
|
+
* The production transport: one POST of the envelope to the API path over
|
|
4
|
+
* fetch, with the cookie and CORS behaviour a browser call needs. The
|
|
5
|
+
* caller's default, built from its isCorsEnabled option.
|
|
6
|
+
*/
|
|
7
|
+
export declare const lambderFetchTransport: (options?: {
|
|
8
|
+
cors?: boolean;
|
|
9
|
+
}) => LambderApiTransport;
|