@friggframework/core 2.0.0--canary.658.93c8e07.0 → 2.0.0--canary.656.4fb07b4.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/CLAUDE.md +5 -90
- package/core/invocation-deadline.js +10 -19
- package/core/invocation-scope.js +1 -9
- package/errors/fetch-error.js +7 -9
- package/errors/rate-limit-error.js +0 -4
- package/integrations/integration-base.js +1 -37
- package/integrations/repositories/integration-repository-documentdb.js +6 -25
- package/integrations/repositories/integration-repository-interface.js +5 -22
- package/integrations/repositories/integration-repository-mongo.js +12 -32
- package/integrations/repositories/integration-repository-postgres.js +12 -33
- package/integrations/tests/doubles/test-integration-repository.js +2 -15
- package/integrations/use-cases/update-integration-messages.js +6 -8
- package/logs/serialize.js +15 -0
- package/modules/module.js +0 -30
- package/modules/requester/rate-limit/parsers.js +1 -24
- package/modules/requester/rate-limit/policy.js +21 -52
- package/modules/requester/requester.js +26 -88
- package/package.json +5 -5
- package/types/core/index.d.ts +0 -9
- package/types/errors/index.d.ts +0 -15
- package/types/integrations/index.d.ts +4 -25
- package/types/module-plugin/index.d.ts +2 -20
- package/integrations/repositories/message-item-shared.js +0 -24
- package/integrations/use-cases/record-rate-limit-message.js +0 -85
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
declare module "@friggframework/module-plugin" {
|
|
2
2
|
import { Delegate, IFriggDelegate } from "@friggframework/core";
|
|
3
|
-
import type {
|
|
3
|
+
import type { RateLimitHint } from "@friggframework/errors";
|
|
4
4
|
|
|
5
5
|
export interface Credential {
|
|
6
6
|
id?: string;
|
|
@@ -44,21 +44,12 @@ declare module "@friggframework/module-plugin" {
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
export type RateLimitPolicy = {
|
|
47
|
-
/** The key a limit counts against. Default `entity`. */
|
|
48
47
|
scope?: RateLimitScope;
|
|
49
48
|
windows?: RateLimitWindow[];
|
|
50
49
|
maxConcurrency?: number;
|
|
51
|
-
/** A wait never shorter than this, for every throttled response. */
|
|
52
50
|
minRetryAfterMs?: number;
|
|
53
|
-
/** The most a request sleeps in process, in total. Default 300000. */
|
|
54
51
|
maxInProcessWaitMs?: number;
|
|
55
|
-
/** Header parsers, in order. Default all three. */
|
|
56
52
|
parsers?: Array<"retryAfter" | "resetHeaders" | "ietf">;
|
|
57
|
-
/**
|
|
58
|
-
* Recognises a limit that is not a plain 429, or names its reason. Returns
|
|
59
|
-
* null when the response is not a limit. A hint with no time takes its
|
|
60
|
-
* time from the parsers or the policy.
|
|
61
|
-
*/
|
|
62
53
|
classify?(
|
|
63
54
|
signal: RateLimitSignal
|
|
64
55
|
): Partial<Omit<RateLimitHint, "source">> & {
|
|
@@ -75,8 +66,6 @@ declare module "@friggframework/module-plugin" {
|
|
|
75
66
|
static requestTimeoutMs?: number;
|
|
76
67
|
|
|
77
68
|
DLGT_INVALID_AUTH: string;
|
|
78
|
-
/** Notified with the `RateLimitError` right before a wait that is too long throws. */
|
|
79
|
-
DLGT_RATE_LIMITED: string;
|
|
80
69
|
requestTimeoutMs: number;
|
|
81
70
|
backOff: number[];
|
|
82
71
|
fetch: any;
|
|
@@ -96,12 +85,6 @@ declare module "@friggframework/module-plugin" {
|
|
|
96
85
|
parseBody(response: any): Promise<any>;
|
|
97
86
|
refreshAuth(): Promise<any>;
|
|
98
87
|
_adoptNewerCredential(): Promise<boolean>;
|
|
99
|
-
/**
|
|
100
|
-
* Tells the integration about a `RateLimitError` the module throws itself.
|
|
101
|
-
* The Requester calls it for its own errors; a module built on a vendor SDK
|
|
102
|
-
* calls it before its own throw.
|
|
103
|
-
*/
|
|
104
|
-
_notifyRateLimited(rateLimitError: RateLimitError): Promise<void>;
|
|
105
88
|
|
|
106
89
|
delegate: any;
|
|
107
90
|
delegateTypes: any[];
|
|
@@ -119,7 +102,6 @@ declare module "@friggframework/module-plugin" {
|
|
|
119
102
|
isRefreshable: boolean;
|
|
120
103
|
refreshCount: number;
|
|
121
104
|
DLGT_INVALID_AUTH: string;
|
|
122
|
-
DLGT_RATE_LIMITED: string;
|
|
123
105
|
fetch: any;
|
|
124
106
|
|
|
125
107
|
parseBody(response: any): Promise<any>;
|
|
@@ -135,7 +117,6 @@ declare module "@friggframework/module-plugin" {
|
|
|
135
117
|
_delete(options: RequestOptions): Promise<any>;
|
|
136
118
|
refreshAuth(): Promise<any>;
|
|
137
119
|
_adoptNewerCredential(): Promise<boolean>;
|
|
138
|
-
_notifyRateLimited(rateLimitError: RateLimitError): Promise<void>;
|
|
139
120
|
}
|
|
140
121
|
|
|
141
122
|
type RequestOptions = {
|
|
@@ -149,6 +130,7 @@ declare module "@friggframework/module-plugin" {
|
|
|
149
130
|
type RequesterConstructor = {
|
|
150
131
|
backOff?: number[];
|
|
151
132
|
fetch?: any;
|
|
133
|
+
random?: () => number;
|
|
152
134
|
};
|
|
153
135
|
|
|
154
136
|
export class ApiKeyRequester
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
const isItem = (value) =>
|
|
2
|
-
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* The stored shape of a message, for IntegrationRepository
|
|
6
|
-
* .updateIntegrationMessages(). The method takes the positional form (title,
|
|
7
|
-
* body, timestamp) or one item object. The keys of an item object are stored
|
|
8
|
-
* as they are, so a caller can attach what a client acts on, like a code or
|
|
9
|
-
* a list of actions.
|
|
10
|
-
* @param {string|Object} titleOrItem - The title, or the whole item.
|
|
11
|
-
* @param {string} [messageBody] - Ignored when an item is given.
|
|
12
|
-
* @param {number|Date} [messageTimestamp] - Ignored when an item is given.
|
|
13
|
-
* @returns {Object} A new object; the given item is never returned itself.
|
|
14
|
-
*/
|
|
15
|
-
function toMessageItem(titleOrItem, messageBody, messageTimestamp) {
|
|
16
|
-
if (isItem(titleOrItem)) return { ...titleOrItem };
|
|
17
|
-
return {
|
|
18
|
-
title: titleOrItem,
|
|
19
|
-
message: messageBody,
|
|
20
|
-
timestamp: messageTimestamp,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
module.exports = { toMessageItem };
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
const RATE_LIMITED = 'RATE_LIMITED';
|
|
2
|
-
const SAME_LIMIT_WINDOW_MS = 60_000;
|
|
3
|
-
const MINUTE_MS = 60_000;
|
|
4
|
-
|
|
5
|
-
const isLink = (link) =>
|
|
6
|
-
typeof link?.label === 'string' && typeof link?.url === 'string';
|
|
7
|
-
|
|
8
|
-
const linkActions = (links) =>
|
|
9
|
-
(Array.isArray(links) ? links : [])
|
|
10
|
-
.filter(isLink)
|
|
11
|
-
.map(({ label, url }) => ({ type: 'LINK', label, url }));
|
|
12
|
-
|
|
13
|
-
function utcMinuteAfter(date) {
|
|
14
|
-
const minute = new Date(Math.ceil(date.getTime() / MINUTE_MS) * MINUTE_MS);
|
|
15
|
-
return `${minute.toISOString().slice(0, 16).replace('T', ' ')} UTC`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Use case that records one user-facing warning when a module reports a rate
|
|
20
|
-
* limit that is too long to wait for. The text is fixed and the payload's
|
|
21
|
-
* message, url, body and headers are never read, because the message reaches
|
|
22
|
-
* end users. It changes no integration status: a rate limit is not an error.
|
|
23
|
-
* @class RecordRateLimitMessage
|
|
24
|
-
*/
|
|
25
|
-
class RecordRateLimitMessage {
|
|
26
|
-
/**
|
|
27
|
-
* @param {Object} params - Configuration parameters.
|
|
28
|
-
* @param {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface} params.integrationRepository - Repository for integration data operations.
|
|
29
|
-
*/
|
|
30
|
-
constructor({ integrationRepository }) {
|
|
31
|
-
this.integrationRepository = integrationRepository;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Writes the warning, unless the stored warnings already hold one for the
|
|
36
|
-
* same module whose reset time is within a minute of this one. Many workers
|
|
37
|
-
* hit one limit together and each reports it, so the check reads the
|
|
38
|
-
* stored warnings and not the messages of this instance.
|
|
39
|
-
* @async
|
|
40
|
-
* @param {string} integrationId - ID of the integration to warn on.
|
|
41
|
-
* @param {Object} payload - The RATE_LIMITED payload of the Module.
|
|
42
|
-
* @param {string} payload.moduleName
|
|
43
|
-
* @param {string} payload.reason
|
|
44
|
-
* @param {Date|string|number} payload.retryAt
|
|
45
|
-
* @param {Array<{label: string, url: string}>} [payload.links]
|
|
46
|
-
* @returns {Promise<boolean>} True when it wrote a warning.
|
|
47
|
-
* @throws When the read or the write fails; the caller decides what a
|
|
48
|
-
* failed warning is worth.
|
|
49
|
-
*/
|
|
50
|
-
async execute(integrationId, { moduleName, reason, retryAt, links }) {
|
|
51
|
-
const resetAt = new Date(retryAt);
|
|
52
|
-
const warnings =
|
|
53
|
-
await this.integrationRepository.findIntegrationMessages(
|
|
54
|
-
integrationId,
|
|
55
|
-
'warnings'
|
|
56
|
-
);
|
|
57
|
-
const alreadyRecorded = warnings.some(
|
|
58
|
-
(warning) =>
|
|
59
|
-
warning?.code === RATE_LIMITED &&
|
|
60
|
-
warning.module === moduleName &&
|
|
61
|
-
Math.abs(Date.parse(warning.retryAt) - resetAt.getTime()) <=
|
|
62
|
-
SAME_LIMIT_WINDOW_MS
|
|
63
|
-
);
|
|
64
|
-
if (alreadyRecorded) return false;
|
|
65
|
-
|
|
66
|
-
const resetsAt = utcMinuteAfter(resetAt);
|
|
67
|
-
await this.integrationRepository.updateIntegrationMessages(
|
|
68
|
-
integrationId,
|
|
69
|
-
'warnings',
|
|
70
|
-
{
|
|
71
|
-
title: 'Rate limit reached',
|
|
72
|
-
message: `The ${moduleName} API rate limit was reached and resets at ${resetsAt}.`,
|
|
73
|
-
timestamp: Date.now(),
|
|
74
|
-
code: RATE_LIMITED,
|
|
75
|
-
module: moduleName,
|
|
76
|
-
reason,
|
|
77
|
-
retryAt: resetAt.toISOString(),
|
|
78
|
-
actions: [{ type: 'RETRY_WHEN_READY' }, ...linkActions(links)],
|
|
79
|
-
}
|
|
80
|
-
);
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
module.exports = { RecordRateLimitMessage };
|