lambder 14.1.1 → 15.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 +429 -0
- package/README.md +14 -17
- package/dist/api/LambderApiDeclarations.d.ts +11 -5
- package/dist/api/LambderApiDefinition.d.ts +2 -1
- package/dist/api/LambderApiEnvelope.d.ts +2 -2
- package/dist/api/LambderApiEnvelope.js +8 -2
- package/dist/api/LambderApiGuards.d.ts +44 -5
- package/dist/api/LambderApiGuards.js +27 -0
- package/dist/api/LambderApiIdempotency.d.ts +14 -7
- package/dist/api/LambderApiIdempotency.js +39 -27
- package/dist/api/LambderApiPipeline.d.ts +25 -11
- package/dist/api/LambderApiPipeline.js +34 -17
- package/dist/api/LambderApiRateLimits.d.ts +28 -21
- package/dist/api/LambderApiRateLimits.js +55 -35
- package/dist/api/LambderApiRefusals.d.ts +10 -4
- package/dist/api/LambderApiRefusals.js +31 -5
- package/dist/api/LambderApiRequest.d.ts +21 -1
- package/dist/api/LambderApiRequest.js +31 -2
- package/dist/build/checkApiRefusals.d.ts +8 -3
- package/dist/build/checkApiRefusals.js +80 -29
- package/dist/build/contractPrinterProcess.d.ts +11 -0
- package/dist/build/contractPrinterProcess.js +17 -0
- package/dist/build/generateApiFiles.d.ts +26 -5
- package/dist/build/generateApiFiles.js +68 -7
- package/dist/build/writeApiSchemas.d.ts +109 -0
- package/dist/build/writeApiSchemas.js +308 -0
- package/dist/build.d.ts +3 -1
- package/dist/build.js +2 -0
- package/dist/client/LambderCaller.d.ts +25 -11
- package/dist/client/LambderCaller.js +57 -49
- package/dist/client/LambderUploadRunner.d.ts +55 -27
- package/dist/client/LambderUploadRunner.js +89 -34
- package/dist/client.d.ts +6 -6
- package/dist/client.js +2 -2
- package/dist/core/Lambder.d.ts +31 -21
- package/dist/core/Lambder.js +79 -23
- package/dist/core/LambderCallSummary.d.ts +1 -1
- package/dist/core/LambderContext.d.ts +59 -7
- package/dist/core/LambderContext.js +31 -15
- package/dist/core/LambderCrashHandling.d.ts +5 -0
- package/dist/core/LambderCrashHandling.js +11 -1
- package/dist/core/LambderCreateOptions.d.ts +28 -17
- package/dist/core/LambderCreateOptions.js +14 -7
- package/dist/core/LambderResolver.d.ts +3 -2
- package/dist/core/LambderResponseBuilder.d.ts +20 -9
- package/dist/core/LambderResponseBuilder.js +39 -8
- package/dist/core/LambderTemplatingEngine.d.ts +24 -6
- package/dist/core/LambderTemplatingEngine.js +23 -3
- package/dist/index.d.ts +15 -12
- package/dist/index.js +7 -5
- package/dist/invoke/LambderInvokeCaller.d.ts +25 -4
- package/dist/invoke/LambderInvokeCaller.js +47 -9
- package/dist/invoke/LambderInvokeOutcome.d.ts +7 -6
- package/dist/mock/LambderMockAdapterDoor.d.ts +40 -0
- package/dist/mock/LambderMockAdapterDoor.js +10 -0
- package/dist/mock/LambderMockApiSchemas.d.ts +28 -0
- package/dist/mock/LambderMockApiSchemas.js +150 -0
- package/dist/mock/LambderMockApp.d.ts +56 -48
- package/dist/mock/LambderMockApp.js +107 -62
- package/dist/mock/LambderMockBrowserCookies.js +18 -5
- package/dist/mock/LambderMockCreateOptions.d.ts +51 -4
- package/dist/mock/LambderMockFailureInjector.d.ts +12 -3
- package/dist/mock/LambderMockFailureInjector.js +12 -8
- package/dist/mock/LambderMockTypes.d.ts +8 -4
- package/dist/mock/lambderMockInvokeTransport.js +2 -9
- package/dist/mock/lambderMockMswHandler.d.ts +19 -13
- package/dist/mock/lambderMockMswHandler.js +25 -22
- package/dist/mock.d.ts +1 -0
- package/dist/secrets/LambderOneShotSecrets.d.ts +1 -1
- package/dist/secrets/LambderOneShotSecrets.js +6 -36
- package/dist/secrets/LambderPasswordHasher.d.ts +56 -0
- package/dist/secrets/LambderPasswordHasher.js +168 -0
- package/dist/session/LambderSessionController.d.ts +37 -4
- package/dist/session/LambderSessionController.js +36 -6
- package/dist/session/LambderSessionManager.d.ts +33 -3
- package/dist/session/LambderSessionManager.js +55 -16
- package/dist/shared/LambderHtml.d.ts +23 -4
- package/dist/shared/LambderHtml.js +17 -5
- package/dist/shared/LambderI18n.d.ts +106 -16
- package/dist/shared/LambderI18n.js +259 -74
- package/dist/shared/contracts/LambderCache.d.ts +14 -1
- package/dist/shared/contracts/LambderRateLimiter.d.ts +5 -6
- package/dist/shared/transport/LambderApiTransport.d.ts +3 -2
- package/dist/shared/util/LambderBackoffTimer.d.ts +24 -13
- package/dist/shared/util/LambderBackoffTimer.js +40 -12
- package/dist/shared/util/LambderCallAbort.d.ts +22 -6
- package/dist/shared/util/LambderCallAbort.js +37 -9
- package/dist/shared/util/LambderErrorReportMark.d.ts +4 -0
- package/dist/shared/util/LambderErrorReportMark.js +22 -0
- package/dist/shared/util/LambderKeyFieldDigest.d.ts +46 -0
- package/dist/shared/util/LambderKeyFieldDigest.js +75 -0
- package/dist/shared/util/LambderSignedClaims.d.ts +19 -0
- package/dist/shared/util/LambderSignedClaims.js +54 -0
- package/dist/shared/util/LambderSwappableInstances.d.ts +29 -0
- package/dist/shared/util/LambderSwappableInstances.js +54 -0
- package/dist/shared/util/LambderTestingDoors.d.ts +6 -2
- package/dist/shared/util/LambderTestingDoors.js +6 -2
- package/dist/shared/util/LambderTextDigest.d.ts +5 -5
- package/dist/shared/util/LambderTextDigest.js +5 -5
- package/dist/shared/wire/LambderApiContract.d.ts +1 -1
- package/dist/shared/wire/LambderApiOptionEntries.d.ts +9 -1
- package/dist/shared/wire/LambderApiOptionEntries.js +15 -0
- package/dist/shared/wire/LambderApiOptionValues.d.ts +3 -2
- package/dist/shared/wire/LambderApiOutcome.d.ts +37 -19
- package/dist/shared/wire/LambderApiOutcome.js +1 -1
- package/dist/shared/wire/LambderApiRefusal.d.ts +39 -12
- package/dist/shared/wire/LambderApiRefusal.js +3 -3
- package/dist/shared/wire/LambderApiSchemaEntries.d.ts +21 -0
- package/dist/shared/wire/LambderApiSchemaEntries.js +18 -0
- package/dist/shared/wire/LambderCallOptions.d.ts +3 -3
- package/dist/shared/wire/LambderCallOutcome.d.ts +2 -1
- package/dist/shared/wire/LambderCallOutcome.js +3 -2
- package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +5 -3
- package/dist/shared/wire/LambderIdempotencyKeyScope.js +5 -3
- package/dist/shared/wire/LambderInvokeApiId.d.ts +3 -3
- package/dist/shared/wire/LambderInvokeApiId.js +3 -3
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +7 -6
- package/dist/shared/wire/LambderOutcomeAssertions.js +1 -1
- package/dist/stores/LambderCacheValues.d.ts +2 -5
- package/dist/stores/LambderDdbCache.d.ts +15 -9
- package/dist/stores/LambderDdbCache.js +25 -6
- package/dist/stores/LambderDdbIdempotencyStore.d.ts +6 -4
- package/dist/stores/LambderDdbIdempotencyStore.js +32 -52
- package/dist/stores/LambderDdbOneShotSecretStore.d.ts +11 -0
- package/dist/stores/LambderDdbOneShotSecretStore.js +17 -5
- package/dist/stores/LambderDdbSdk.d.ts +59 -29
- package/dist/stores/LambderDdbSdk.js +116 -44
- package/dist/stores/LambderDdbSessionStore.d.ts +26 -11
- package/dist/stores/LambderDdbSessionStore.js +133 -68
- package/dist/stores/LambderMemoryCache.d.ts +2 -2
- package/dist/stores/LambderS3FileSource.d.ts +3 -2
- package/dist/stores/LambderS3FileSource.js +11 -13
- package/dist/stores/LambderS3UploadBucket.d.ts +12 -0
- package/dist/stores/LambderS3UploadBucket.js +23 -0
- package/dist/stores/LambderStorageBackedCache.d.ts +2 -2
- package/dist/stores/LambderStoredText.d.ts +26 -0
- package/dist/stores/LambderStoredText.js +25 -0
- package/dist/testing/LambderConformanceRunner.d.ts +9 -2
- package/dist/testing/LambderConformanceRunner.js +9 -2
- package/dist/testing/LambderTestApp.d.ts +65 -7
- package/dist/testing/LambderTestApp.js +101 -6
- package/dist/testing/LambderTestVisitor.d.ts +43 -12
- package/dist/testing/LambderTestVisitor.js +35 -11
- package/dist/testing/bootLambdaPackage.d.ts +105 -0
- package/dist/testing/bootLambdaPackage.js +265 -0
- package/dist/testing/lambdaPackageBootProcess.d.ts +39 -0
- package/dist/testing/lambdaPackageBootProcess.js +130 -0
- package/dist/testing/lambderCacheConformance.d.ts +26 -0
- package/dist/testing/lambderCacheConformance.js +287 -0
- package/dist/testing/lambderCacheStorageConformance.d.ts +25 -0
- package/dist/testing/lambderCacheStorageConformance.js +159 -0
- package/dist/testing/lambderSessionStoreConformance.d.ts +3 -3
- package/dist/testing/lambderSessionStoreConformance.js +3 -3
- package/dist/testing.d.ts +7 -1
- package/dist/testing.js +5 -0
- package/package.json +1 -6
- package/dist/shared/util/boundKeyField.d.ts +0 -20
- package/dist/shared/util/boundKeyField.js +0 -34
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { LambderUncheckedRefusalMessage } from '../shared/wire/LambderApiRefusal.js';
|
|
2
2
|
import { type LambderRequestCompressionOption } from '../shared/wire/LambderRequestPayload.js';
|
|
3
3
|
import type { LambderApiContractShape, LambderContractAnyRefusalMessage, LambderContractRefusalMessage } from '../shared/wire/LambderApiContract.js';
|
|
4
|
-
import { type LambderApiOutcome, type LambderValidationError } from '../shared/wire/LambderApiOutcome.js';
|
|
4
|
+
import { type LambderApiFailure, type LambderApiOutcome, type LambderValidationError } from '../shared/wire/LambderApiOutcome.js';
|
|
5
5
|
import { type LambderCallArgs, type LambderContractOutputOf, type LambderGuardInputsProviderOption, type LambderSharedCallOptions } from '../shared/wire/LambderCallOptions.js';
|
|
6
6
|
import { type LambderApiTransport } from '../shared/transport/LambderApiTransport.js';
|
|
7
7
|
import { type LambderApiSignatureMap } from '../shared/wire/LambderApiSignatureMap.js';
|
|
@@ -23,12 +23,23 @@ type FetchStartEventHandler = (params: {
|
|
|
23
23
|
fetchParams: EventHandlerFetchParams;
|
|
24
24
|
activeFetchList: FetchTracker[];
|
|
25
25
|
}) => void | Promise<void>;
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Told that a call ended, however it ended, with the outcome apiOutcome()
|
|
28
|
+
* resolves to. TMessage is what the calls it hears can refuse with, as for
|
|
29
|
+
* RefusalHandler.
|
|
30
|
+
*/
|
|
31
|
+
type FetchEndEventHandler<TMessage extends LambderUncheckedRefusalMessage = LambderUncheckedRefusalMessage> = (params: {
|
|
27
32
|
fetchParams: EventHandlerFetchParams;
|
|
28
|
-
fetchResult:
|
|
33
|
+
fetchResult: LambderApiOutcome<unknown, TMessage>;
|
|
29
34
|
activeFetchList: FetchTracker[];
|
|
30
35
|
}) => void | Promise<void>;
|
|
31
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Told a failure no other handler takes, with the error to report and the
|
|
38
|
+
* failure outcome it came from (its reason, status, refusal and response),
|
|
39
|
+
* so a reporter can word or file it by reason without reading the message.
|
|
40
|
+
* A call its own signal aborted is never one. TMessage as for RefusalHandler.
|
|
41
|
+
*/
|
|
42
|
+
type ErrorHandler<TMessage extends LambderUncheckedRefusalMessage = LambderUncheckedRefusalMessage> = (error: Error, failure: LambderApiFailure<TMessage>) => void | Promise<void>;
|
|
32
43
|
type ValidationErrorHandler = (zodError: LambderValidationError) => (void | false) | Promise<(void | false)>;
|
|
33
44
|
/**
|
|
34
45
|
* Handed the refusal as its message object, a plain-string refusal
|
|
@@ -51,13 +62,14 @@ export type LambderCallOptions<TMessage extends LambderUncheckedRefusalMessage =
|
|
|
51
62
|
refusalHandler?: RefusalHandler<TMessage>;
|
|
52
63
|
apiInputValidationErrorHandler?: ValidationErrorHandler;
|
|
53
64
|
notAuthorizedHandler?: NotifyHandler;
|
|
54
|
-
errorHandler?: ErrorHandler
|
|
65
|
+
errorHandler?: ErrorHandler<TMessage>;
|
|
55
66
|
logListHandler?: LambderLogListHandler;
|
|
56
67
|
fetchStartedHandler?: FetchStartEventHandler;
|
|
57
|
-
fetchEndedHandler?: FetchEndEventHandler
|
|
68
|
+
fetchEndedHandler?: FetchEndEventHandler<TMessage>;
|
|
58
69
|
};
|
|
59
70
|
type LambderCallerBaseOptions<TMessage extends LambderUncheckedRefusalMessage = LambderUncheckedRefusalMessage> = {
|
|
60
|
-
apiPath:
|
|
71
|
+
/** Must match the server's apiPath. Default: "/api", the server's own default. */
|
|
72
|
+
apiPath?: string;
|
|
61
73
|
/** Sent with every call as `version`, informational: the server stamps its own on every answer. */
|
|
62
74
|
apiVersion?: string;
|
|
63
75
|
/**
|
|
@@ -82,11 +94,12 @@ type LambderCallerBaseOptions<TMessage extends LambderUncheckedRefusalMessage =
|
|
|
82
94
|
/** Handed every refusal a call of this caller comes back with, typed with every code the contract declares. */
|
|
83
95
|
refusalHandler?: RefusalHandler<TMessage>;
|
|
84
96
|
notAuthorizedHandler?: NotifyHandler;
|
|
85
|
-
|
|
97
|
+
/** Handed every failure no other handler takes (see ErrorHandler), typed with every code the contract declares. */
|
|
98
|
+
errorHandler?: ErrorHandler<TMessage>;
|
|
86
99
|
/** Receives each answer's logList, with the API name. Default: console.log with a `[lambder]` prefix, one line per entry. */
|
|
87
100
|
logListHandler?: LambderLogListHandler;
|
|
88
101
|
fetchStartedHandler?: FetchStartEventHandler;
|
|
89
|
-
fetchEndedHandler?: FetchEndEventHandler
|
|
102
|
+
fetchEndedHandler?: FetchEndEventHandler<TMessage>;
|
|
90
103
|
apiInputValidationErrorHandler?: ValidationErrorHandler;
|
|
91
104
|
/** Must mirror the server's session cookie Domain, otherwise expired cookies cannot be cleared. */
|
|
92
105
|
sessionCookieDomain?: string | ((hostname: string) => string | undefined | null);
|
|
@@ -143,8 +156,9 @@ declare class LambderCallerCore<TContract extends LambderApiContractShape = any,
|
|
|
143
156
|
* The endpoint's output on success, `undefined` on every failure. An
|
|
144
157
|
* output is always an object or an array, so the result is truthy exactly
|
|
145
158
|
* when the call succeeded; the handlers configured on the caller have
|
|
146
|
-
* already been told why it did not
|
|
147
|
-
* reason at the
|
|
159
|
+
* already been told why it did not, unless the call's own signal aborted
|
|
160
|
+
* it, which tells none. Use apiOutcome() to branch on the reason at the
|
|
161
|
+
* call site.
|
|
148
162
|
*/
|
|
149
163
|
api<TApiName extends keyof TContract & string = string>(apiName: TApiName, ...rest: LambderCallArgs<TContract, TApiName, TProvidedGuards, LambderCallOptions<LambderContractRefusalMessage<TContract, TApiName>>>): Promise<LambderContractOutputOf<TContract, TApiName> | undefined>;
|
|
150
164
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Cookies from 'js-cookie';
|
|
2
2
|
import { compressPayloadGzip, isRequestCompressionAvailable, resolveRequestCompressionMinBytes, DEFAULT_REQUEST_COMPRESSION_SETTINGS, } from '../shared/wire/LambderRequestPayload.js';
|
|
3
3
|
import { resolveCompressionOption } from '../shared/wire/LambderCompressionOption.js';
|
|
4
|
-
import { resolveApiOutcome } from '../shared/wire/LambderApiOutcome.js';
|
|
4
|
+
import { resolveApiOutcome, } from '../shared/wire/LambderApiOutcome.js';
|
|
5
5
|
import { mergeGuardInputs, } from '../shared/wire/LambderCallOptions.js';
|
|
6
6
|
import { beginIdempotentAttempt, IDEMPOTENT_ATTEMPT_NOT_SENT } from '../shared/wire/LambderIdempotencyKeyScope.js';
|
|
7
7
|
import { createCallAbort } from '../shared/util/LambderCallAbort.js';
|
|
@@ -13,6 +13,7 @@ import { LambderReloadLoopBreaker, RELOAD_LOOP_WINDOW_MS } from './LambderReload
|
|
|
13
13
|
import { lambderFetchTransport } from './lambderFetchTransport.js';
|
|
14
14
|
import { withApiGroupCalls } from '../shared/wire/LambderApiGroupCalls.js';
|
|
15
15
|
import { splitApiName } from '../shared/wire/LambderApiNames.js';
|
|
16
|
+
import { DEFAULT_API_PATH } from '../shared/wire/LambderDefaultApiPath.js';
|
|
16
17
|
/**
|
|
17
18
|
* What keeps a stale bundle from reloading itself forever (see the class):
|
|
18
19
|
* one for the page, shared by every caller it builds, since a reload is the
|
|
@@ -39,9 +40,10 @@ class LambderCallerCore {
|
|
|
39
40
|
get isLoading() { return this.fetchTrackerList.length > 0; }
|
|
40
41
|
#versionExpiredHandler;
|
|
41
42
|
#sessionExpiredHandler;
|
|
42
|
-
//
|
|
43
|
-
// constructor
|
|
44
|
-
// generic over one endpoint's message, hands
|
|
43
|
+
// These three hear every endpoint's calls, so they are held at the widest
|
|
44
|
+
// message: the constructor options type them to the contract's codes,
|
|
45
|
+
// and dispatch, generic over one endpoint's message, hands them one of
|
|
46
|
+
// those.
|
|
45
47
|
#refusalHandler;
|
|
46
48
|
#notAuthorizedHandler;
|
|
47
49
|
#errorHandler;
|
|
@@ -59,7 +61,7 @@ class LambderCallerCore {
|
|
|
59
61
|
// The conditional provider option is resolved per instantiation;
|
|
60
62
|
// inside the class it is read through the plain shape.
|
|
61
63
|
const { apiPath, apiVersion, apiSignatures, isCorsEnabled, timeoutMs, versionExpiredHandler, sessionExpiredHandler, refusalHandler, notAuthorizedHandler, errorHandler, logListHandler, fetchStartedHandler, fetchEndedHandler, apiInputValidationErrorHandler, sessionCookieDomain, requestCompression, guardInputsProvider, transport, } = options;
|
|
62
|
-
this.#apiPath = apiPath;
|
|
64
|
+
this.#apiPath = apiPath ?? DEFAULT_API_PATH;
|
|
63
65
|
this.#apiVersion = apiVersion;
|
|
64
66
|
this.#apiSignatures = apiSignatures;
|
|
65
67
|
this.#timeoutMs = timeoutMs;
|
|
@@ -137,38 +139,50 @@ class LambderCallerCore {
|
|
|
137
139
|
this.fetchTrackerList.splice(at, 1);
|
|
138
140
|
};
|
|
139
141
|
let fetchEndCalled = false;
|
|
140
|
-
const fetchEnded = async (
|
|
142
|
+
const fetchEnded = async (outcome) => {
|
|
141
143
|
dropFetchTracker();
|
|
142
144
|
if (fetchEndCalled || !fetchEndedHandler)
|
|
143
145
|
return;
|
|
144
146
|
fetchEndCalled = true;
|
|
145
147
|
await fetchEndedHandler({
|
|
146
148
|
fetchParams: { apiName, payload, headers },
|
|
147
|
-
fetchResult,
|
|
149
|
+
fetchResult: outcome,
|
|
148
150
|
activeFetchList: [...this.fetchTrackerList],
|
|
149
151
|
});
|
|
150
152
|
};
|
|
151
153
|
let errorHandlerCalled = false;
|
|
152
|
-
const reportError = async (
|
|
154
|
+
const reportError = async (error, failure) => {
|
|
153
155
|
if (errorHandlerCalled || !errorHandler)
|
|
154
156
|
return;
|
|
155
157
|
errorHandlerCalled = true;
|
|
156
|
-
await errorHandler(
|
|
158
|
+
await errorHandler(error, failure);
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Ends a call no answer was read for: the attempt told, the lifecycle
|
|
162
|
+
* closed, and the failure reported, unless the site's own signal is
|
|
163
|
+
* what ended it. The site gave that call up itself (a superseded
|
|
164
|
+
* read, a view that closed), and reporting it would put a "could not
|
|
165
|
+
* reach the server" in front of a person for something the page chose.
|
|
166
|
+
*/
|
|
167
|
+
const unansweredOutcome = async (failure, attemptOutcome) => {
|
|
168
|
+
idempotentAttempt.settle(attemptOutcome);
|
|
169
|
+
await fetchEnded(failure);
|
|
170
|
+
if (failure.reason !== 'aborted')
|
|
171
|
+
await reportError(failure.error, failure);
|
|
172
|
+
return failure;
|
|
157
173
|
};
|
|
158
174
|
// Timeout and abort wiring, shared with LambderInvokeCaller so the two
|
|
159
175
|
// cannot drift on what a late or abandoned call means.
|
|
160
176
|
const abort = createCallAbort({ timeoutMs: options?.timeoutMs ?? this.#timeoutMs, signal: options?.signal });
|
|
161
177
|
const signal = abort.signal;
|
|
162
|
-
/**
|
|
178
|
+
/** Ends a call that was given up on, or null while it still stands. */
|
|
163
179
|
const abandonedOutcome = async (stage) => {
|
|
164
|
-
const
|
|
165
|
-
if (!
|
|
180
|
+
const abandoned = abort.abortFailure(stage);
|
|
181
|
+
if (!abandoned)
|
|
166
182
|
return null;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
await
|
|
170
|
-
const outcome = { ok: false, reason: failure.reason, error: failure.error };
|
|
171
|
-
return outcome;
|
|
183
|
+
const failure = { ok: false, reason: abandoned.reason, error: abandoned.error };
|
|
184
|
+
// Nothing sent tried nothing; a request that left may have run.
|
|
185
|
+
return await unansweredOutcome(failure, stage === "beforeSending" ? IDEMPOTENT_ATTEMPT_NOT_SENT : failure);
|
|
172
186
|
};
|
|
173
187
|
try {
|
|
174
188
|
// A name that is not group.action names no endpoint and no path:
|
|
@@ -177,8 +191,9 @@ class LambderCallerCore {
|
|
|
177
191
|
if (!splitApiName(apiName)) {
|
|
178
192
|
idempotentAttempt.settle(IDEMPOTENT_ATTEMPT_NOT_SENT);
|
|
179
193
|
const error = new Error(`Lambder: "${apiName}" is not an endpoint name. An endpoint is named group.action, both identifiers.`);
|
|
180
|
-
|
|
181
|
-
|
|
194
|
+
const failure = { ok: false, reason: 'unknown', error };
|
|
195
|
+
await reportError(error, failure);
|
|
196
|
+
return failure;
|
|
182
197
|
}
|
|
183
198
|
this.fetchTrackerList.push(fetchTracker);
|
|
184
199
|
if (fetchStartedHandler)
|
|
@@ -235,18 +250,15 @@ class LambderCallerCore {
|
|
|
235
250
|
});
|
|
236
251
|
}
|
|
237
252
|
catch (err) {
|
|
238
|
-
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
const reason = abort.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
await fetchEnded(wrappedError);
|
|
248
|
-
await reportError(wrappedError);
|
|
249
|
-
return { ok: false, reason, error: wrappedError };
|
|
253
|
+
// The caller's own abort wins, timeout or signal, since only it
|
|
254
|
+
// knows which aborted the call. Otherwise a transport that named
|
|
255
|
+
// its reason is believed: "protocol" means something came back
|
|
256
|
+
// and was not an answer, which is what this caller already
|
|
257
|
+
// calls `server`.
|
|
258
|
+
const reason = abort.abortReason()
|
|
259
|
+
?? (isLambderTransportFailure(err) && err.reason === 'protocol' ? 'server' : 'network');
|
|
260
|
+
const failure = { ok: false, reason, error: coerceToError(err, "Request failed") };
|
|
261
|
+
return await unansweredOutcome(failure, failure);
|
|
250
262
|
}
|
|
251
263
|
// An answer that arrives after the call was given up on is not a
|
|
252
264
|
// success. A transport that ignores request.signal resolves late,
|
|
@@ -271,25 +283,20 @@ class LambderCallerCore {
|
|
|
271
283
|
for (const record of logList)
|
|
272
284
|
console.log("[lambder]", record);
|
|
273
285
|
}
|
|
286
|
+
await fetchEnded(outcome);
|
|
274
287
|
if (!outcome.ok && outcome.reason === 'server') {
|
|
275
|
-
await
|
|
276
|
-
await reportError(outcome.error);
|
|
288
|
+
await reportError(outcome.error, outcome);
|
|
277
289
|
return outcome;
|
|
278
290
|
}
|
|
279
291
|
if (!outcome.ok && outcome.reason === 'validation') {
|
|
280
|
-
await fetchEnded(null);
|
|
281
292
|
if (apiInputValidationErrorHandler) {
|
|
282
293
|
await apiInputValidationErrorHandler(outcome.zodError);
|
|
283
294
|
}
|
|
284
295
|
else {
|
|
285
|
-
await reportError(new Error("API Input Validation Error", { cause: outcome.zodError }));
|
|
296
|
+
await reportError(new Error("API Input Validation Error", { cause: outcome.zodError }), outcome);
|
|
286
297
|
}
|
|
287
298
|
return outcome;
|
|
288
299
|
}
|
|
289
|
-
// Whatever is left carries the envelope: a success or one of the
|
|
290
|
-
// envelope's own refusals, which is why no assertion is needed.
|
|
291
|
-
const data = outcome.response;
|
|
292
|
-
await fetchEnded(data);
|
|
293
300
|
if (!outcome.ok && outcome.reason === 'versionExpired') {
|
|
294
301
|
// A page asks for one reload at a time, however many of its
|
|
295
302
|
// calls are refused. A call refused again after a reload
|
|
@@ -300,7 +307,7 @@ class LambderCallerCore {
|
|
|
300
307
|
if (decision === "alreadyAsked")
|
|
301
308
|
return outcome;
|
|
302
309
|
if (decision === "loopConfirmed") {
|
|
303
|
-
await reportError(new Error(`Version expired again for API "${apiName}" within ${RELOAD_LOOP_WINDOW_MS / 60000} minutes of a reload: the bundle being served is still the stale one, so versionExpiredHandler was not called again.`));
|
|
310
|
+
await reportError(new Error(`Version expired again for API "${apiName}" within ${RELOAD_LOOP_WINDOW_MS / 60000} minutes of a reload: the bundle being served is still the stale one, so versionExpiredHandler was not called again.`), outcome);
|
|
304
311
|
return outcome;
|
|
305
312
|
}
|
|
306
313
|
await pageReloadLoopBreaker.runReloadAsk(async () => {
|
|
@@ -308,7 +315,7 @@ class LambderCallerCore {
|
|
|
308
315
|
await versionExpiredHandler();
|
|
309
316
|
}
|
|
310
317
|
else {
|
|
311
|
-
await reportError(new Error("Version Expired; Please refresh;"));
|
|
318
|
+
await reportError(new Error("Version Expired; Please refresh;"), outcome);
|
|
312
319
|
}
|
|
313
320
|
});
|
|
314
321
|
return outcome;
|
|
@@ -334,7 +341,7 @@ class LambderCallerCore {
|
|
|
334
341
|
await sessionExpiredHandler();
|
|
335
342
|
}
|
|
336
343
|
else {
|
|
337
|
-
await reportError(new Error("Session Expired; Please log in again;"));
|
|
344
|
+
await reportError(new Error("Session Expired; Please log in again;"), outcome);
|
|
338
345
|
}
|
|
339
346
|
return outcome;
|
|
340
347
|
}
|
|
@@ -343,7 +350,7 @@ class LambderCallerCore {
|
|
|
343
350
|
await notAuthorizedHandler();
|
|
344
351
|
}
|
|
345
352
|
else {
|
|
346
|
-
await reportError(new Error("Not Authorized;"));
|
|
353
|
+
await reportError(new Error("Not Authorized;"), outcome);
|
|
347
354
|
}
|
|
348
355
|
return outcome;
|
|
349
356
|
}
|
|
@@ -358,13 +365,13 @@ class LambderCallerCore {
|
|
|
358
365
|
catch (err) {
|
|
359
366
|
// Escape hatch for anything above (typically an app handler throwing):
|
|
360
367
|
// dispatch never throws, so api()/apiOutcome() call sites never do.
|
|
361
|
-
const
|
|
368
|
+
const failure = { ok: false, reason: 'unknown', error: coerceToError(err, "The call failed before it produced an outcome") };
|
|
362
369
|
try {
|
|
363
|
-
await fetchEnded(
|
|
364
|
-
await reportError(
|
|
370
|
+
await fetchEnded(failure);
|
|
371
|
+
await reportError(failure.error, failure);
|
|
365
372
|
}
|
|
366
373
|
catch { /* an app handler threw again; never propagate */ }
|
|
367
|
-
return
|
|
374
|
+
return failure;
|
|
368
375
|
}
|
|
369
376
|
finally {
|
|
370
377
|
// Whatever ended the call before its attempt was told (a provider
|
|
@@ -398,8 +405,9 @@ class LambderCallerCore {
|
|
|
398
405
|
* The endpoint's output on success, `undefined` on every failure. An
|
|
399
406
|
* output is always an object or an array, so the result is truthy exactly
|
|
400
407
|
* when the call succeeded; the handlers configured on the caller have
|
|
401
|
-
* already been told why it did not
|
|
402
|
-
* reason at the
|
|
408
|
+
* already been told why it did not, unless the call's own signal aborted
|
|
409
|
+
* it, which tells none. Use apiOutcome() to branch on the reason at the
|
|
410
|
+
* call site.
|
|
403
411
|
*/
|
|
404
412
|
async api(apiName, ...rest) {
|
|
405
413
|
const [payload, options] = rest;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type LambderUploadFileFacts, type LambderUploadRule, type LambderUploadRuleVerdict, type LambderUploadTicket } from "../shared/contracts/LambderUploadBucket.js";
|
|
2
|
+
import type { LambderApiFailure, LambderApiOutcome } from "../shared/wire/LambderApiOutcome.js";
|
|
2
3
|
/** Where an upload is, in order. `sentBytes` only moves during `uploading`. */
|
|
3
4
|
export type LambderUploadPhase = "hashing" | "requesting" | "uploading" | "confirming";
|
|
4
5
|
export type LambderUploadProgress = {
|
|
@@ -9,50 +10,74 @@ export type LambderUploadProgress = {
|
|
|
9
10
|
export type LambderUploadFailureReason = LambderUploadRuleVerdict
|
|
10
11
|
/** The browser could not read the file (moved, deleted, or a cloud placeholder that never downloaded). */
|
|
11
12
|
| "fileUnreadable"
|
|
12
|
-
/** The app's ticket endpoint would not issue a ticket. */
|
|
13
|
+
/** The app's ticket endpoint would not issue a ticket: it refused, or its call failed in a way a retry cannot cure. */
|
|
13
14
|
| "ticketRefused"
|
|
14
15
|
/** Storage answered and said no, for a reason a retry cannot cure. */
|
|
15
16
|
| "storageRejected"
|
|
16
|
-
/** Storage could not be reached, or kept stalling, through every attempt. */
|
|
17
|
+
/** Storage, or the app's ticket or confirm endpoint, could not be reached, or kept stalling or failing, through every attempt. */
|
|
17
18
|
| "networkFailed"
|
|
18
|
-
/** The bytes are stored, and the app's confirm endpoint would not confirm them. */
|
|
19
|
-
| "confirmRefused"
|
|
20
|
-
/**
|
|
19
|
+
/** The bytes are stored, and the app's confirm endpoint would not confirm them: it refused, or its call failed in a way a retry cannot cure. */
|
|
20
|
+
| "confirmRefused"
|
|
21
|
+
/** The upload's signal aborted it. */
|
|
22
|
+
| "aborted";
|
|
23
|
+
/**
|
|
24
|
+
* How an upload failed: `reason` for a screen to word, the underlying error
|
|
25
|
+
* as `cause`, and the failed outcome of the app's own call as `callFailure`
|
|
26
|
+
* when that call is what ended the upload, so a screen words a refusal by its
|
|
27
|
+
* code (`callFailure.refusal.code`) as it would on the call itself.
|
|
28
|
+
*/
|
|
21
29
|
export declare class LambderUploadError extends Error {
|
|
22
30
|
readonly reason: LambderUploadFailureReason;
|
|
31
|
+
/** The ticket or confirm call's failure outcome, when the upload ended on one. */
|
|
32
|
+
readonly callFailure?: LambderApiFailure;
|
|
23
33
|
constructor(reason: LambderUploadFailureReason, options?: {
|
|
24
34
|
cause?: unknown;
|
|
25
35
|
detail?: string;
|
|
36
|
+
callFailure?: LambderApiFailure;
|
|
26
37
|
});
|
|
27
38
|
}
|
|
28
|
-
|
|
39
|
+
/**
|
|
40
|
+
* What a runner is built from. The app's ticket and confirm calls answer with
|
|
41
|
+
* the outcome a caller's `.outcome()` resolves to, so the runner can tell a
|
|
42
|
+
* call that got no usable answer, which it tries again, from a refusal, which
|
|
43
|
+
* it does not.
|
|
44
|
+
*/
|
|
45
|
+
export type LambderUploadRunnerOptions<TicketOutput extends {
|
|
46
|
+
ticket: LambderUploadTicket;
|
|
47
|
+
}, Receipt> = {
|
|
29
48
|
uploadRule: LambderUploadRule;
|
|
30
49
|
/**
|
|
31
|
-
* The app's ticket endpoint
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
50
|
+
* The app's ticket endpoint, as its outcome
|
|
51
|
+
* (`caller.invoices.requestUpload.outcome(input, { signal })`). Its output
|
|
52
|
+
* carries the `ticket` and whatever the confirm endpoint needs to find this
|
|
53
|
+
* upload again (the id of the record it made), which the runner hands to
|
|
54
|
+
* confirmUpload unread. `signal` is the upload's own, for the call to pass
|
|
55
|
+
* on so an abort stops it too.
|
|
35
56
|
*/
|
|
36
57
|
requestTicket: (fileFacts: LambderUploadFileFacts, call: {
|
|
37
58
|
signal: AbortSignal | undefined;
|
|
38
|
-
}) => Promise<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
59
|
+
}) => Promise<LambderApiOutcome<TicketOutput>>;
|
|
60
|
+
/**
|
|
61
|
+
* The app's confirm endpoint, as its outcome: handed the ticket
|
|
62
|
+
* endpoint's output, the server checks the stored object and answers its
|
|
63
|
+
* record of it, the receipt upload() resolves to.
|
|
64
|
+
*/
|
|
65
|
+
confirmUpload: (ticketOutput: TicketOutput, call: {
|
|
44
66
|
signal: AbortSignal | undefined;
|
|
45
|
-
}) => Promise<Receipt
|
|
67
|
+
}) => Promise<LambderApiOutcome<Receipt>>;
|
|
46
68
|
/** The app's way of forgetting a confirmed upload the person removed again. Without one, discard() does nothing. */
|
|
47
69
|
discardUpload?: (receipt: Receipt) => Promise<void>;
|
|
48
70
|
/**
|
|
49
|
-
* How
|
|
50
|
-
* a failure a retry can cure (a 5xx,
|
|
51
|
-
*
|
|
52
|
-
* `
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
71
|
+
* How a step is tried again when it got no usable answer: storage that
|
|
72
|
+
* cannot be reached, stalls, or answers a failure a retry can cure (a 5xx,
|
|
73
|
+
* RequestTimeout, SlowDown), and a ticket or confirm call that failed as
|
|
74
|
+
* `network`, `timeout` or `server`. Each wait is `baseDelayMs` plus a
|
|
75
|
+
* random share of a ceiling that starts at `baseDelayMs` and doubles with
|
|
76
|
+
* every failed attempt, the whole never past `maxDelayMs` (the ladder of
|
|
77
|
+
* LambderBackoffTimer), so many browsers dropped together do not come
|
|
78
|
+
* back in step, not even the first time. The attempts are per step: the
|
|
79
|
+
* ladder starts again after each step that succeeds. Default: 4
|
|
80
|
+
* attempts, waits from one second to 15.
|
|
56
81
|
*/
|
|
57
82
|
storageRetry?: {
|
|
58
83
|
attempts?: number;
|
|
@@ -67,13 +92,16 @@ export type LambderUploadRunnerOptions<Reference, Receipt> = {
|
|
|
67
92
|
*/
|
|
68
93
|
stallTimeoutMs?: number;
|
|
69
94
|
};
|
|
70
|
-
export declare class LambderUploadRunner<
|
|
95
|
+
export declare class LambderUploadRunner<TicketOutput extends {
|
|
96
|
+
ticket: LambderUploadTicket;
|
|
97
|
+
}, Receipt> {
|
|
71
98
|
private readonly options;
|
|
72
|
-
|
|
99
|
+
/** How many times a step is tried again after its first try: the storageRetry option's attempts, less that first one. */
|
|
100
|
+
private readonly retries;
|
|
73
101
|
/** The ladder one upload's waits climb; each upload() builds a timer of its own from it, so two uploads never share a count. */
|
|
74
102
|
private readonly backoff;
|
|
75
103
|
private readonly stallTimeoutMs;
|
|
76
|
-
constructor(options: LambderUploadRunnerOptions<
|
|
104
|
+
constructor(options: LambderUploadRunnerOptions<TicketOutput, Receipt>);
|
|
77
105
|
/** For a file input's `accept`, so the picker only offers what the rule takes. */
|
|
78
106
|
get acceptedTypes(): string;
|
|
79
107
|
get maxBytes(): number;
|