@particle-academy/fancy-connector-core 0.4.0 → 0.6.0
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 +138 -1
- package/README.md +28 -0
- package/dist/index.cjs +61 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -2
- package/dist/index.d.ts +41 -2
- package/dist/index.js +61 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -346,6 +346,19 @@ type DeliveryOutcome<T> = {
|
|
|
346
346
|
gaveUp?: string;
|
|
347
347
|
/** The classification of the last failure, so a host can route on it. */
|
|
348
348
|
kind?: FailureKind;
|
|
349
|
+
/**
|
|
350
|
+
* The last failure **as it was thrown** — absent when the call worked.
|
|
351
|
+
*
|
|
352
|
+
* `kind` and `gaveUp` are what `deliver()` concluded; this is what it
|
|
353
|
+
* concluded it FROM. Until 0.5.0 an outcome carried only the conclusions, so
|
|
354
|
+
* everything the error knew beyond them — the HTTP status, the provider's own
|
|
355
|
+
* code, the class it was raised as — stopped here, and every failed
|
|
356
|
+
* `callConnector` reached its host with none of it. See `failureFrom()` in
|
|
357
|
+
* `client.ts`.
|
|
358
|
+
*
|
|
359
|
+
* `unknown`, because `send` may throw anything.
|
|
360
|
+
*/
|
|
361
|
+
error?: unknown;
|
|
349
362
|
};
|
|
350
363
|
/**
|
|
351
364
|
* The message a person reads when an ambiguous failure could not be retried.
|
|
@@ -774,6 +787,26 @@ type ServiceDescriptor = {
|
|
|
774
787
|
faker: ConnectorFaker;
|
|
775
788
|
/** Header the provider uses for idempotency, when it has one. */
|
|
776
789
|
idempotencyHeader?: string;
|
|
790
|
+
/**
|
|
791
|
+
* Where this provider puts its OWN error code on a failed response, when it
|
|
792
|
+
* publishes one — read only for a status of 400 or above, and carried as
|
|
793
|
+
* `error.providerCode`.
|
|
794
|
+
*
|
|
795
|
+
* **Declared per service, never guessed.** The shapes do not agree, and the
|
|
796
|
+
* near-misses are the dangerous part: Bluesky's `error` is a code
|
|
797
|
+
* (`"AuthenticationRequired"`), Mastodon's `error` is a sentence
|
|
798
|
+
* (`"The access token is invalid"`), Discord's code is a number under `code`,
|
|
799
|
+
* and some providers use a header. A generic reader would publish Mastodon's
|
|
800
|
+
* sentence as a code — the plausible answer that is wrong. With no
|
|
801
|
+
* declaration the field is absent, which says *nobody said where to look*.
|
|
802
|
+
*
|
|
803
|
+
* Return a string or an integer (carried as its decimal string); anything
|
|
804
|
+
* else, a blank string, or a throw, leaves the code absent. **A throw never
|
|
805
|
+
* reaches the call**: a code is a detail on an answer the status already gave,
|
|
806
|
+
* and a reader choking on an HTML error page must not turn an explicit `401`
|
|
807
|
+
* into an unclassified failure.
|
|
808
|
+
*/
|
|
809
|
+
providerCodeFrom?: (response: TransportResponse) => string | number | null | undefined;
|
|
777
810
|
};
|
|
778
811
|
type ConnectorRequest = {
|
|
779
812
|
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
@@ -1108,7 +1141,13 @@ declare class ConnectorError extends Error {
|
|
|
1108
1141
|
* failure must not be assumed safe.
|
|
1109
1142
|
*/
|
|
1110
1143
|
readonly kind: FailureKind;
|
|
1111
|
-
|
|
1144
|
+
/**
|
|
1145
|
+
* `options.cause` is the standard `Error` cause — what this error was built
|
|
1146
|
+
* from. A failed call's error carries the classified error there, and a thrown
|
|
1147
|
+
* transport's carries the original exception, so nothing below is lost when
|
|
1148
|
+
* the message is rewritten for a person.
|
|
1149
|
+
*/
|
|
1150
|
+
constructor(message: string, ctx: ConnectorErrorContext, options?: ErrorOptions);
|
|
1112
1151
|
/**
|
|
1113
1152
|
* Safe to retry **without knowing anything about the connector**.
|
|
1114
1153
|
*
|
|
@@ -1141,7 +1180,7 @@ declare class ConnectorRateLimited extends ConnectorError {
|
|
|
1141
1180
|
readonly retryAfter?: number;
|
|
1142
1181
|
constructor(message: string, ctx: ConnectorErrorContext & {
|
|
1143
1182
|
retryAfter?: number;
|
|
1144
|
-
});
|
|
1183
|
+
}, options?: ErrorOptions);
|
|
1145
1184
|
get classified(): Classified;
|
|
1146
1185
|
}
|
|
1147
1186
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -346,6 +346,19 @@ type DeliveryOutcome<T> = {
|
|
|
346
346
|
gaveUp?: string;
|
|
347
347
|
/** The classification of the last failure, so a host can route on it. */
|
|
348
348
|
kind?: FailureKind;
|
|
349
|
+
/**
|
|
350
|
+
* The last failure **as it was thrown** — absent when the call worked.
|
|
351
|
+
*
|
|
352
|
+
* `kind` and `gaveUp` are what `deliver()` concluded; this is what it
|
|
353
|
+
* concluded it FROM. Until 0.5.0 an outcome carried only the conclusions, so
|
|
354
|
+
* everything the error knew beyond them — the HTTP status, the provider's own
|
|
355
|
+
* code, the class it was raised as — stopped here, and every failed
|
|
356
|
+
* `callConnector` reached its host with none of it. See `failureFrom()` in
|
|
357
|
+
* `client.ts`.
|
|
358
|
+
*
|
|
359
|
+
* `unknown`, because `send` may throw anything.
|
|
360
|
+
*/
|
|
361
|
+
error?: unknown;
|
|
349
362
|
};
|
|
350
363
|
/**
|
|
351
364
|
* The message a person reads when an ambiguous failure could not be retried.
|
|
@@ -774,6 +787,26 @@ type ServiceDescriptor = {
|
|
|
774
787
|
faker: ConnectorFaker;
|
|
775
788
|
/** Header the provider uses for idempotency, when it has one. */
|
|
776
789
|
idempotencyHeader?: string;
|
|
790
|
+
/**
|
|
791
|
+
* Where this provider puts its OWN error code on a failed response, when it
|
|
792
|
+
* publishes one — read only for a status of 400 or above, and carried as
|
|
793
|
+
* `error.providerCode`.
|
|
794
|
+
*
|
|
795
|
+
* **Declared per service, never guessed.** The shapes do not agree, and the
|
|
796
|
+
* near-misses are the dangerous part: Bluesky's `error` is a code
|
|
797
|
+
* (`"AuthenticationRequired"`), Mastodon's `error` is a sentence
|
|
798
|
+
* (`"The access token is invalid"`), Discord's code is a number under `code`,
|
|
799
|
+
* and some providers use a header. A generic reader would publish Mastodon's
|
|
800
|
+
* sentence as a code — the plausible answer that is wrong. With no
|
|
801
|
+
* declaration the field is absent, which says *nobody said where to look*.
|
|
802
|
+
*
|
|
803
|
+
* Return a string or an integer (carried as its decimal string); anything
|
|
804
|
+
* else, a blank string, or a throw, leaves the code absent. **A throw never
|
|
805
|
+
* reaches the call**: a code is a detail on an answer the status already gave,
|
|
806
|
+
* and a reader choking on an HTML error page must not turn an explicit `401`
|
|
807
|
+
* into an unclassified failure.
|
|
808
|
+
*/
|
|
809
|
+
providerCodeFrom?: (response: TransportResponse) => string | number | null | undefined;
|
|
777
810
|
};
|
|
778
811
|
type ConnectorRequest = {
|
|
779
812
|
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
@@ -1108,7 +1141,13 @@ declare class ConnectorError extends Error {
|
|
|
1108
1141
|
* failure must not be assumed safe.
|
|
1109
1142
|
*/
|
|
1110
1143
|
readonly kind: FailureKind;
|
|
1111
|
-
|
|
1144
|
+
/**
|
|
1145
|
+
* `options.cause` is the standard `Error` cause — what this error was built
|
|
1146
|
+
* from. A failed call's error carries the classified error there, and a thrown
|
|
1147
|
+
* transport's carries the original exception, so nothing below is lost when
|
|
1148
|
+
* the message is rewritten for a person.
|
|
1149
|
+
*/
|
|
1150
|
+
constructor(message: string, ctx: ConnectorErrorContext, options?: ErrorOptions);
|
|
1112
1151
|
/**
|
|
1113
1152
|
* Safe to retry **without knowing anything about the connector**.
|
|
1114
1153
|
*
|
|
@@ -1141,7 +1180,7 @@ declare class ConnectorRateLimited extends ConnectorError {
|
|
|
1141
1180
|
readonly retryAfter?: number;
|
|
1142
1181
|
constructor(message: string, ctx: ConnectorErrorContext & {
|
|
1143
1182
|
retryAfter?: number;
|
|
1144
|
-
});
|
|
1183
|
+
}, options?: ErrorOptions);
|
|
1145
1184
|
get classified(): Classified;
|
|
1146
1185
|
}
|
|
1147
1186
|
/**
|
package/dist/index.js
CHANGED
|
@@ -137,7 +137,8 @@ async function deliver(send, policy = DEFAULT_RETRY, sleep = (ms) => new Promise
|
|
|
137
137
|
ok: false,
|
|
138
138
|
attempts,
|
|
139
139
|
kind: classified.kind,
|
|
140
|
-
gaveUp: classified.kind === "ambiguous" ? `${AMBIGUOUS_REFUSAL} (${classified.detail})` : classified.detail
|
|
140
|
+
gaveUp: classified.kind === "ambiguous" ? `${AMBIGUOUS_REFUSAL} (${classified.detail})` : classified.detail,
|
|
141
|
+
error
|
|
141
142
|
};
|
|
142
143
|
}
|
|
143
144
|
if (attempt >= budget) {
|
|
@@ -146,7 +147,8 @@ async function deliver(send, policy = DEFAULT_RETRY, sleep = (ms) => new Promise
|
|
|
146
147
|
ok: false,
|
|
147
148
|
attempts,
|
|
148
149
|
kind: classified.kind,
|
|
149
|
-
gaveUp: `Gave up after ${attempt} attempts. ${classified.detail}
|
|
150
|
+
gaveUp: `Gave up after ${attempt} attempts. ${classified.detail}`,
|
|
151
|
+
error
|
|
150
152
|
};
|
|
151
153
|
}
|
|
152
154
|
const backoff = Math.min(policy.baseDelayMs * 2 ** (attempt - 1), policy.maxDelayMs);
|
|
@@ -184,8 +186,14 @@ var ConnectorError = class extends Error {
|
|
|
184
186
|
* failure must not be assumed safe.
|
|
185
187
|
*/
|
|
186
188
|
kind = "ambiguous";
|
|
187
|
-
|
|
188
|
-
|
|
189
|
+
/**
|
|
190
|
+
* `options.cause` is the standard `Error` cause — what this error was built
|
|
191
|
+
* from. A failed call's error carries the classified error there, and a thrown
|
|
192
|
+
* transport's carries the original exception, so nothing below is lost when
|
|
193
|
+
* the message is rewritten for a person.
|
|
194
|
+
*/
|
|
195
|
+
constructor(message, ctx, options) {
|
|
196
|
+
super(message, options?.cause === void 0 ? void 0 : { cause: options.cause });
|
|
189
197
|
this.name = new.target.name;
|
|
190
198
|
this.service = ctx.service;
|
|
191
199
|
this.operation = ctx.operation;
|
|
@@ -217,8 +225,8 @@ var ConnectorRateLimited = class extends ConnectorError {
|
|
|
217
225
|
kind = "refused-explicitly";
|
|
218
226
|
/** Seconds to wait, when the provider said. */
|
|
219
227
|
retryAfter;
|
|
220
|
-
constructor(message, ctx) {
|
|
221
|
-
super(message, ctx);
|
|
228
|
+
constructor(message, ctx, options) {
|
|
229
|
+
super(message, ctx, options);
|
|
222
230
|
this.retryAfter = ctx.retryAfter;
|
|
223
231
|
}
|
|
224
232
|
get classified() {
|
|
@@ -265,7 +273,7 @@ function classifyHttp(status, ctx, body, retryAfter) {
|
|
|
265
273
|
function classifyThrown(cause, ctx) {
|
|
266
274
|
const classified = classifyError(cause);
|
|
267
275
|
const message = `${ctx.service}.${ctx.operation}: ${classified.detail}`;
|
|
268
|
-
return classified.kind === "unreachable" ? new ConnectorUnreachable(message, ctx) : new ConnectorAmbiguous(message, ctx);
|
|
276
|
+
return classified.kind === "unreachable" ? new ConnectorUnreachable(message, ctx, { cause }) : new ConnectorAmbiguous(message, ctx, { cause });
|
|
269
277
|
}
|
|
270
278
|
function httpFailure(status, body, retryAfter) {
|
|
271
279
|
const classified = classifyStatus(status, body, retryAfter);
|
|
@@ -496,7 +504,13 @@ async function callConnector(service, options) {
|
|
|
496
504
|
if (response.status < 400) {
|
|
497
505
|
return response.body.trim() === "" ? null : parseJson(response.body, ctx);
|
|
498
506
|
}
|
|
499
|
-
|
|
507
|
+
const providerCode = readProviderCode(service, response);
|
|
508
|
+
throw classifyHttp(
|
|
509
|
+
response.status,
|
|
510
|
+
providerCode === void 0 ? ctx : { ...ctx, providerCode },
|
|
511
|
+
response.body,
|
|
512
|
+
readRetryAfter(response.headers)
|
|
513
|
+
);
|
|
500
514
|
},
|
|
501
515
|
{
|
|
502
516
|
...DEFAULT_RETRY,
|
|
@@ -515,12 +529,39 @@ async function callConnector(service, options) {
|
|
|
515
529
|
function failureFrom(outcome, ctx, idempotent) {
|
|
516
530
|
const last = outcome.attempts.at(-1);
|
|
517
531
|
const kind = outcome.kind ?? last?.kind ?? "ambiguous";
|
|
518
|
-
const
|
|
532
|
+
const message = outcome.gaveUp ?? `${ctx.service}.${ctx.operation} failed.`;
|
|
533
|
+
const cause = outcome.error;
|
|
534
|
+
const said = cause instanceof ConnectorError ? cause : void 0;
|
|
535
|
+
const context = {
|
|
536
|
+
...ctx,
|
|
537
|
+
...said?.status === void 0 ? {} : { status: said.status },
|
|
538
|
+
...said?.providerCode === void 0 ? {} : { providerCode: said.providerCode }
|
|
539
|
+
};
|
|
540
|
+
const error = failureOfClass(kind, said, message, context, cause === void 0 ? void 0 : { cause });
|
|
519
541
|
Object.defineProperty(error, "kind", { value: kind, enumerable: false });
|
|
520
542
|
Object.defineProperty(error, "attempts", { value: outcome.attempts, enumerable: false });
|
|
521
543
|
Object.defineProperty(error, "idempotent", { value: idempotent, enumerable: false });
|
|
522
544
|
return error;
|
|
523
545
|
}
|
|
546
|
+
function failureOfClass(kind, said, message, context, options) {
|
|
547
|
+
if (said?.kind === kind && said instanceof ConnectorAuthError) {
|
|
548
|
+
return new ConnectorAuthError(message, context, options);
|
|
549
|
+
}
|
|
550
|
+
if (said?.kind === kind && said instanceof ConnectorRateLimited) {
|
|
551
|
+
const retryAfter = said.retryAfter === void 0 ? {} : { retryAfter: said.retryAfter };
|
|
552
|
+
return new ConnectorRateLimited(message, { ...context, ...retryAfter }, options);
|
|
553
|
+
}
|
|
554
|
+
switch (kind) {
|
|
555
|
+
case "unreachable":
|
|
556
|
+
return new ConnectorUnreachable(message, context, options);
|
|
557
|
+
case "refused-explicitly":
|
|
558
|
+
return new ConnectorTransient(message, context, options);
|
|
559
|
+
case "rejected":
|
|
560
|
+
return new ConnectorRequestError(message, context, options);
|
|
561
|
+
case "ambiguous":
|
|
562
|
+
return new ConnectorAmbiguous(message, context, options);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
524
565
|
async function prepare(service, connection, options) {
|
|
525
566
|
const base = connection.baseUrl ?? service.baseUrls[connection.mode];
|
|
526
567
|
if (!base) {
|
|
@@ -601,6 +642,17 @@ function parseJson(body, ctx) {
|
|
|
601
642
|
throw error;
|
|
602
643
|
}
|
|
603
644
|
}
|
|
645
|
+
function readProviderCode(service, response) {
|
|
646
|
+
if (!service.providerCodeFrom) return void 0;
|
|
647
|
+
let code;
|
|
648
|
+
try {
|
|
649
|
+
code = service.providerCodeFrom(response);
|
|
650
|
+
} catch {
|
|
651
|
+
return void 0;
|
|
652
|
+
}
|
|
653
|
+
if (typeof code === "number") return Number.isSafeInteger(code) ? String(code) : void 0;
|
|
654
|
+
return typeof code === "string" && code.trim() !== "" ? code : void 0;
|
|
655
|
+
}
|
|
604
656
|
function readRetryAfter(headers) {
|
|
605
657
|
const raw = headers["retry-after"];
|
|
606
658
|
if (!raw) return void 0;
|