@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 CHANGED
@@ -370,49 +370,11 @@ class MyIntegration extends IntegrationBase {
370
370
  - `BasicAuthRequester` - Basic authentication
371
371
 
372
372
  **Rate limits** (ADR-049): an API module declares `static rateLimit` on its
373
- `Requester` subclass. For a throttled response the Requester looks for a hint
374
- in this order:
375
-
376
- | Step | Source |
377
- |---|---|
378
- | 1 | `classify({ status, headers, body })` of the module. It recognises a limit that is not a plain 429 (a 403 limit code, a `policyName` in the body) and may name only a `reason` |
379
- | 2 | The header parsers in `parsers` order. Default: `retryAfter`, `resetHeaders`, `ietf` |
380
- | 3 | The static policy: `minRetryAfterMs`, or the `perMs` of the window named by the `reason` |
381
- | 4 | The fixed `backOff` ladder (1, 3, 10, 30, 60, 180 s) |
382
-
383
- - A 429 with no hint from steps 1 to 3 keeps today's ladder: the same calls,
384
- the same delays, then a plain `FetchError`. Nothing budgets it. A response
385
- that `classify` names as a limit but that has no time follows the same
386
- ladder, and the last error is a `FetchError` flagged `isRateLimited` with
387
- the `reason`, so the queue worker does not halt it.
388
- - With a hint, the wait is `max(hint, minRetryAfterMs, 1 s)` plus at most 10 %
389
- jitter. The total sleep of one request is capped at `maxInProcessWaitMs`
390
- (default 5 minutes) and at the time left in the invocation less one request
391
- timeout (`remainingInvocationMs()`).
392
- - A wait that does not fit throws `RateLimitError` (`isRateLimited`, `retryAt`,
393
- `waitMs`, `reason`, `policy`, `source`, `module`, `scopeKey`). The queue
394
- worker does not halt it, whatever its status.
395
- - `Retry-After` is read on a 429 only. Another status counts only when
396
- `classify` recognises it. A 5xx keeps the 5xx ladder.
397
- - `maxInProcessWaitMs: 0` turns off sleeping: every hinted wait throws.
398
- - A module that does not use the Requester (for example one on jsforce) calls
399
- `classifyRateLimit(policy, { status, headers, body })` around its own client
400
- and throws `RateLimitError` itself.
401
-
402
- ```javascript
403
- class Api extends OAuth2Requester {
404
- static rateLimit = {
405
- scope: 'entity',
406
- minRetryAfterMs: 1_000,
407
- classify({ status, body }) {
408
- if (status === 403 && body?.code === 'DAILY_LIMIT') {
409
- return { reason: 'daily', waitMs: 60 * 60_000 };
410
- }
411
- return null;
412
- },
413
- };
414
- }
415
- ```
373
+ `Requester` subclass. A hinted wait (from `classify()`, the header parsers or
374
+ the static policy) sleeps within a budget, or throws `RateLimitError`
375
+ (`isRateLimited`, `retryAt`) when it does not fit. A 429 with no hint keeps the
376
+ fixed `backOff` ladder. See
377
+ [Rate Limits](../../docs/reference/api-module-definition-and-functions.md#rate-limits-static-ratelimit).
416
378
 
417
379
  **Module Factory**:
418
380
  - `ModuleFactory` - Creates and configures API module instances
@@ -568,53 +530,6 @@ if (!userId) {
568
530
  }
569
531
  ```
570
532
 
571
- **Rate-limit message** (ADR-049): when a wait is too long to sleep, the
572
- Requester tells its delegate (`RATE_LIMITED`, with the `RateLimitError`) and
573
- then throws. The notification takes the path of `CREDENTIAL_INVALIDATED`:
574
- Requester, `Module`, `IntegrationBase`.
575
-
576
- - The `Module` sends `{ moduleName, reason, retryAt, policy, statusCode, links }`.
577
- `links` is `static rateLimit.userHints[reason].links`, or `[]`. The message,
578
- url, body and headers of the error are never sent: the message reaches end
579
- users.
580
- - `IntegrationBase` writes one `WARN` (`integration.<name>.rate_limited`), and
581
- `RecordRateLimitMessage` appends one item to the stored `warnings`. It
582
- changes no status. A rate limit is not an `ERROR`, and an `ERROR` status
583
- makes the queue worker drop every webhook.
584
- - A report for the same module whose reset time is within 60 s of a stored one
585
- is skipped. The check reads the stored `warnings` with
586
- `findIntegrationMessages`, not `this.messages`, which is per instance and
587
- empty on Postgres. It is a read then a write, so workers that report at the
588
- same instant can still store a few copies.
589
- - Every hop is best effort. A failure is logged, and the caller still gets the
590
- `RateLimitError`.
591
-
592
- ```javascript
593
- {
594
- title: 'Rate limit reached',
595
- message: 'The hubspot API rate limit was reached and resets at 2026-09-28 14:31 UTC.',
596
- timestamp: 1790000000000,
597
- code: 'RATE_LIMITED',
598
- module: 'hubspot',
599
- reason: 'daily',
600
- retryAt: '2026-09-28T14:30:15.000Z',
601
- actions: [
602
- { type: 'RETRY_WHEN_READY' },
603
- { type: 'LINK', label: 'API usage limits', url: 'https://example.com/limits' },
604
- ],
605
- }
606
- ```
607
-
608
- The text is fixed. It holds the module name and the reset time in UTC, rounded
609
- up to the minute, and nothing from the request.
610
- `updateIntegrationMessages(id, type, title, body, timestamp)` still works, and
611
- it also takes one item object, `updateIntegrationMessages(id, type, item)`,
612
- whose keys are stored as they are. Every call appends.
613
- `findIntegrationMessages(id, type)` reads the stored items of one type. The
614
- `messages` of the record from `findIntegrationById` is not filled on Postgres
615
- and Mongo, because the schema keeps the messages in the `errors`, `warnings`,
616
- `info` and `logs` columns.
617
-
618
533
  ### 9. Logging System (`/logs`)
619
534
 
620
535
  **Purpose**: One redacted JSON record per line to stdout (ADR-048). See `docs/guides/LOGGING.md`.
@@ -9,11 +9,6 @@ function storage() {
9
9
  return globalThis[STORE_KEY];
10
10
  }
11
11
 
12
- /**
13
- * Runs fn with the time the invocation ends, as epoch milliseconds. A nested
14
- * scope can shorten the deadline and never extend it. A deadline that is not
15
- * a finite number runs fn with no limit.
16
- */
17
12
  function runWithInvocationDeadline(deadlineAt, fn) {
18
13
  if (typeof deadlineAt !== 'number' || !Number.isFinite(deadlineAt)) {
19
14
  return fn();
@@ -25,25 +20,20 @@ function runWithInvocationDeadline(deadlineAt, fn) {
25
20
  return storage().run(Object.freeze({ deadlineAt: effective }), fn);
26
21
  }
27
22
 
28
- /**
29
- * The end of a Lambda invocation, from its context. Undefined when the
30
- * context cannot tell.
31
- */
32
- function deadlineFromContext(context) {
23
+ function remainingTimeMs(context) {
33
24
  try {
34
25
  const remaining = context?.getRemainingTimeInMillis?.();
35
- return typeof remaining === 'number' && Number.isFinite(remaining)
36
- ? Date.now() + remaining
37
- : undefined;
38
- } catch {
39
- return undefined;
26
+ return typeof remaining === 'number' ? remaining : Infinity;
27
+ } catch (_) {
28
+ return Infinity;
40
29
  }
41
30
  }
42
31
 
43
- /**
44
- * Milliseconds left in the invocation. Infinity outside a deadline scope
45
- * (tests, `frigg start`, scripts).
46
- */
32
+ function deadlineFromContext(context) {
33
+ const remaining = remainingTimeMs(context);
34
+ return Number.isFinite(remaining) ? Date.now() + remaining : undefined;
35
+ }
36
+
47
37
  function remainingInvocationMs(now = Date.now()) {
48
38
  const store = storage().getStore();
49
39
  return store ? Math.max(0, store.deadlineAt - now) : Infinity;
@@ -52,5 +42,6 @@ function remainingInvocationMs(now = Date.now()) {
52
42
  module.exports = {
53
43
  deadlineFromContext,
54
44
  remainingInvocationMs,
45
+ remainingTimeMs,
55
46
  runWithInvocationDeadline,
56
47
  };
@@ -3,6 +3,7 @@ const { summarizeMessageBody } = require('../logs/summarize-event');
3
3
  const { flushSinks, hasFlushableSinks } = require('../logs/logger-runtime');
4
4
  const {
5
5
  deadlineFromContext,
6
+ remainingTimeMs,
6
7
  runWithInvocationDeadline,
7
8
  } = require('./invocation-deadline');
8
9
 
@@ -115,15 +116,6 @@ function withDeadline(ms, fn) {
115
116
  });
116
117
  }
117
118
 
118
- function remainingTimeMs(context) {
119
- try {
120
- const remaining = context?.getRemainingTimeInMillis?.();
121
- return typeof remaining === 'number' ? remaining : Infinity;
122
- } catch (_) {
123
- return Infinity;
124
- }
125
- }
126
-
127
119
  async function flushInvocation({
128
120
  telemetry,
129
121
  usageRollup,
@@ -55,15 +55,13 @@ class FetchError extends BaseError {
55
55
 
56
56
  static async create(options = {}) {
57
57
  const { response } = options;
58
- const provided = options.responseBody ?? options.body;
59
- let responseBody = provided;
60
- if (
61
- responseBody === undefined &&
62
- response &&
63
- !response.bodyUsed &&
64
- typeof response.text === 'function'
65
- ) {
66
- responseBody = (await response.text()) || provided;
58
+ let { responseBody } = options;
59
+ if (responseBody === undefined || responseBody === null) {
60
+ const canRead =
61
+ response &&
62
+ !response.bodyUsed &&
63
+ typeof response.text === 'function';
64
+ responseBody = (canRead && (await response.text())) || options.body;
67
65
  }
68
66
  const ErrorClass = typeof this === 'function' ? this : FetchError;
69
67
  return new ErrorClass({ ...options, responseBody });
@@ -14,10 +14,6 @@ function resolveTiming({ hint, waitMs, now }) {
14
14
  return { waitMs: fallbackWaitMs, retryAt: new Date(now + fallbackWaitMs) };
15
15
  }
16
16
 
17
- /**
18
- * A FetchError for a response that said a limit was hit and told us, or the
19
- * module's policy told us, when to call again. `retryAt` is that time.
20
- */
21
17
  class RateLimitError extends FetchError {
22
18
  constructor({
23
19
  hint,
@@ -17,9 +17,6 @@ const {
17
17
  const {
18
18
  UpdateIntegrationConfig,
19
19
  } = require('./use-cases/update-integration-config');
20
- const {
21
- RecordRateLimitMessage,
22
- } = require('./use-cases/record-rate-limit-message');
23
20
  const { validateExtensionBinding } = require('./extension');
24
21
  const { getTelemetry } = require('../telemetry/telemetry-runtime');
25
22
  const { instrumentHandler } = require('../telemetry/instrument-handler');
@@ -62,9 +59,6 @@ class IntegrationBase {
62
59
  updateIntegrationConfig = new UpdateIntegrationConfig({
63
60
  integrationRepository: this.integrationRepository,
64
61
  });
65
- recordRateLimitMessage = new RecordRateLimitMessage({
66
- integrationRepository: this.integrationRepository,
67
- });
68
62
 
69
63
  // this.telemetry.count('records.synced', n, { entity: 'contact' })
70
64
  telemetry = bindTelemetryContext(getTelemetry(), () =>
@@ -871,9 +865,7 @@ class IntegrationBase {
871
865
  * something integration-level needs attention. Today this catches the
872
866
  * `CREDENTIAL_INVALIDATED` event Module fires from `markCredentialsInvalid`
873
867
  * and flips this integration's status to ERROR so the queue worker
874
- * stops processing further webhooks until the user re-authorizes. It also
875
- * catches `RATE_LIMITED`, which records one warning for the user and
876
- * changes no status: a rate limit is not an error.
868
+ * stops processing further webhooks until the user re-authorizes.
877
869
  *
878
870
  * Modules are wired to this delegate in `_appendModules()`, which runs
879
871
  * during `setIntegrationRecord()` — this covers every construction path
@@ -916,17 +908,6 @@ class IntegrationBase {
916
908
  });
917
909
  await this.persistStatus('ENABLED');
918
910
  }
919
-
920
- if (delegateString === 'RATE_LIMITED') {
921
- const moduleName = object?.moduleName ?? notifier?.name;
922
- this.logger.warn('Module reported a rate limit', {
923
- eventName: `${this.logger.name}.rate_limited`,
924
- moduleName,
925
- reason: object?.reason,
926
- retryAt: object?.retryAt,
927
- });
928
- await this._recordRateLimitMessage({ ...object, moduleName });
929
- }
930
911
  }
931
912
 
932
913
  /**
@@ -952,23 +933,6 @@ class IntegrationBase {
952
933
  });
953
934
  }
954
935
  }
955
-
956
- /**
957
- * Best-effort: the module's request is about to throw a RateLimitError,
958
- * and a failed warning must not change that. A rate limit is not an ERROR,
959
- * so this never touches the integration status.
960
- * @param {Object} payload - The RATE_LIMITED payload of the module.
961
- */
962
- async _recordRateLimitMessage(payload) {
963
- try {
964
- await this.recordRateLimitMessage.execute(this.id, payload);
965
- } catch (error) {
966
- this.logger.error('Failed to record rate limit message', {
967
- eventName: `${this.logger.name}.rate_limit_message_record_failed`,
968
- error,
969
- });
970
- }
971
- }
972
936
  }
973
937
 
974
938
  module.exports = { IntegrationBase };
@@ -14,7 +14,6 @@ const {
14
14
  IntegrationRepositoryInterface,
15
15
  } = require('./integration-repository-interface');
16
16
  const { validateConfigPatch } = require('./config-patch-shared');
17
- const { toMessageItem } = require('./message-item-shared');
18
17
 
19
18
  class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
20
19
  constructor() {
@@ -95,7 +94,7 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
95
94
  async updateIntegrationMessages(
96
95
  integrationId,
97
96
  messageType,
98
- messageTitleOrItem,
97
+ messageTitle,
99
98
  messageBody,
100
99
  messageTimestamp
101
100
  ) {
@@ -109,12 +108,11 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
109
108
  }
110
109
  const messages = this._extractMessages(existing);
111
110
  const list = Array.isArray(messages[messageType]) ? [...messages[messageType]] : [];
112
- const item = toMessageItem(
113
- messageTitleOrItem,
114
- messageBody,
115
- messageTimestamp
116
- );
117
- list.push({ ...item, title: item.title ?? null });
111
+ list.push({
112
+ title: messageTitle ?? null,
113
+ message: messageBody,
114
+ timestamp: messageTimestamp,
115
+ });
118
116
  const updatedMessages = { ...messages, [messageType]: list };
119
117
  await updateOne(
120
118
  this.prisma,
@@ -134,23 +132,6 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
134
132
  return true;
135
133
  }
136
134
 
137
- async findIntegrationMessages(integrationId, messageType) {
138
- const objectId = toObjectId(integrationId);
139
- if (!objectId) {
140
- throw new Error(`Integration ${integrationId} not found`);
141
- }
142
- const existing = await findOne(this.prisma, 'Integration', {
143
- _id: objectId,
144
- });
145
- if (!existing) {
146
- throw new Error(`Integration ${integrationId} not found`);
147
- }
148
- const messages = this._extractMessages(existing);
149
- return Array.isArray(messages[messageType])
150
- ? messages[messageType]
151
- : [];
152
- }
153
-
154
135
  async createIntegration(entities, userId, config) {
155
136
  const now = new Date();
156
137
  const document = {
@@ -97,43 +97,26 @@ class IntegrationRepositoryInterface {
97
97
  }
98
98
 
99
99
  /**
100
- * Append a message to the stored messages of one type. Takes the
101
- * positional form (title, body, timestamp) or one message item object,
102
- * whose keys are stored as they are.
100
+ * Update integration messages
103
101
  *
104
102
  * @param {string|number} integrationId - Integration ID
105
103
  * @param {string} messageType - Type of message (errors, warnings, info, logs)
106
- * @param {string|Object} messageTitleOrItem - Message title, or the whole message item
107
- * @param {string} [messageBody] - Message body (positional form)
108
- * @param {Date} [messageTimestamp] - Message timestamp (positional form)
104
+ * @param {string} messageTitle - Message title
105
+ * @param {string} messageBody - Message body
106
+ * @param {Date} messageTimestamp - Message timestamp
109
107
  * @returns {Promise<boolean>} Success indicator
110
108
  * @abstract
111
109
  */
112
110
  async updateIntegrationMessages(
113
111
  integrationId,
114
112
  messageType,
115
- messageTitleOrItem,
113
+ messageTitle,
116
114
  messageBody,
117
115
  messageTimestamp
118
116
  ) {
119
117
  throw new Error('Method updateIntegrationMessages must be implemented by subclass');
120
118
  }
121
119
 
122
- /**
123
- * Find the stored messages of one type. Reads the stored items, not the
124
- * `messages` of an integration record, which some adapters do not fill.
125
- *
126
- * @param {string|number} integrationId - Integration ID
127
- * @param {string} messageType - Type of message (errors, warnings, info, logs)
128
- * @returns {Promise<Array>} The stored message items of that type, oldest first
129
- * @abstract
130
- */
131
- async findIntegrationMessages(integrationId, messageType) {
132
- throw new Error(
133
- 'Method findIntegrationMessages must be implemented by subclass'
134
- );
135
- }
136
-
137
120
  /**
138
121
  * Create a new integration
139
122
  *
@@ -3,7 +3,6 @@ const {
3
3
  IntegrationRepositoryInterface,
4
4
  } = require('./integration-repository-interface');
5
5
  const { validateConfigPatch } = require('./config-patch-shared');
6
- const { toMessageItem } = require('./message-item-shared');
7
6
 
8
7
  /**
9
8
  * MongoDB Integration Repository Adapter
@@ -230,15 +229,15 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
230
229
  *
231
230
  * @param {string} integrationId - Integration ID
232
231
  * @param {string} messageType - Type of message (errors, warnings, info, logs)
233
- * @param {string|Object} messageTitleOrItem - Message title, or the whole message item
234
- * @param {string} [messageBody] - Message body (positional form)
235
- * @param {Date} [messageTimestamp] - Message timestamp (positional form)
232
+ * @param {string} messageTitle - Message title
233
+ * @param {string} messageBody - Message body
234
+ * @param {Date} messageTimestamp - Message timestamp
236
235
  * @returns {Promise<boolean>} Success indicator
237
236
  */
238
237
  async updateIntegrationMessages(
239
238
  integrationId,
240
239
  messageType,
241
- messageTitleOrItem,
240
+ messageTitle,
242
241
  messageBody,
243
242
  messageTimestamp
244
243
  ) {
@@ -252,14 +251,17 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
252
251
  }
253
252
 
254
253
  // Parse existing messages (JSON field)
255
- const messageArray = Array.isArray(integration[messageType])
256
- ? integration[messageType]
254
+ const messages = integration.messages || {};
255
+ const messageArray = Array.isArray(messages[messageType])
256
+ ? messages[messageType]
257
257
  : [];
258
258
 
259
259
  // Add new message
260
- messageArray.push(
261
- toMessageItem(messageTitleOrItem, messageBody, messageTimestamp)
262
- );
260
+ messageArray.push({
261
+ title: messageTitle,
262
+ message: messageBody,
263
+ timestamp: messageTimestamp,
264
+ });
263
265
 
264
266
  // Update messages
265
267
  await this.prisma.integration.update({
@@ -272,28 +274,6 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
272
274
  return true; // Mongoose compatibility
273
275
  }
274
276
 
275
- /**
276
- * Find the stored messages of one type
277
- *
278
- * @param {string} integrationId - Integration ID
279
- * @param {string} messageType - Type of message (errors, warnings, info, logs)
280
- * @returns {Promise<Array>} The stored message items of that type, oldest first
281
- */
282
- async findIntegrationMessages(integrationId, messageType) {
283
- const integration = await this.prisma.integration.findUnique({
284
- where: { id: integrationId },
285
- select: { [messageType]: true },
286
- });
287
-
288
- if (!integration) {
289
- throw new Error(`Integration ${integrationId} not found`);
290
- }
291
-
292
- return Array.isArray(integration[messageType])
293
- ? integration[messageType]
294
- : [];
295
- }
296
-
297
277
  /**
298
278
  * Find all integrations whose entity set includes the given entity ID.
299
279
  *
@@ -3,7 +3,6 @@ const {
3
3
  IntegrationRepositoryInterface,
4
4
  } = require('./integration-repository-interface');
5
5
  const { validateConfigPatch } = require('./config-patch-shared');
6
- const { toMessageItem } = require('./message-item-shared');
7
6
  const { strictIntId } = require('./report-id');
8
7
 
9
8
  /**
@@ -274,15 +273,15 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
274
273
  *
275
274
  * @param {string} integrationId - Integration ID (string from application layer)
276
275
  * @param {string} messageType - Type of message (errors, warnings, info, logs)
277
- * @param {string|Object} messageTitleOrItem - Message title, or the whole message item
278
- * @param {string} [messageBody] - Message body (positional form)
279
- * @param {Date} [messageTimestamp] - Message timestamp (positional form)
276
+ * @param {string} messageTitle - Message title
277
+ * @param {string} messageBody - Message body
278
+ * @param {Date} messageTimestamp - Message timestamp
280
279
  * @returns {Promise<boolean>} Success indicator
281
280
  */
282
281
  async updateIntegrationMessages(
283
282
  integrationId,
284
283
  messageType,
285
- messageTitleOrItem,
284
+ messageTitle,
286
285
  messageBody,
287
286
  messageTimestamp
288
287
  ) {
@@ -298,14 +297,17 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
298
297
  }
299
298
 
300
299
  // Parse existing messages (JSON field)
301
- const messageArray = Array.isArray(integration[messageType])
302
- ? integration[messageType]
300
+ const messages = integration.messages || {};
301
+ const messageArray = Array.isArray(messages[messageType])
302
+ ? messages[messageType]
303
303
  : [];
304
304
 
305
305
  // Add new message
306
- messageArray.push(
307
- toMessageItem(messageTitleOrItem, messageBody, messageTimestamp)
308
- );
306
+ messageArray.push({
307
+ title: messageTitle,
308
+ message: messageBody,
309
+ timestamp: messageTimestamp,
310
+ });
309
311
 
310
312
  // Update messages
311
313
  await this.prisma.integration.update({
@@ -318,29 +320,6 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
318
320
  return true; // Mongoose compatibility
319
321
  }
320
322
 
321
- /**
322
- * Find the stored messages of one type
323
- *
324
- * @param {string} integrationId - Integration ID (string from application layer)
325
- * @param {string} messageType - Type of message (errors, warnings, info, logs)
326
- * @returns {Promise<Array>} The stored message items of that type, oldest first
327
- */
328
- async findIntegrationMessages(integrationId, messageType) {
329
- const intId = this._convertId(integrationId);
330
- const integration = await this.prisma.integration.findUnique({
331
- where: { id: intId },
332
- select: { [messageType]: true },
333
- });
334
-
335
- if (!integration) {
336
- throw new Error(`Integration ${integrationId} not found`);
337
- }
338
-
339
- return Array.isArray(integration[messageType])
340
- ? integration[messageType]
341
- : [];
342
- }
343
-
344
323
  /**
345
324
  * Create a new integration
346
325
  *
@@ -1,6 +1,5 @@
1
1
  const { v4: uuid } = require('uuid');
2
2
  const { validateConfigPatch } = require('../../repositories/config-patch-shared');
3
- const { toMessageItem } = require('../../repositories/message-item-shared');
4
3
 
5
4
  class TestIntegrationRepository {
6
5
  constructor() {
@@ -73,30 +72,18 @@ class TestIntegrationRepository {
73
72
  return results;
74
73
  }
75
74
 
76
- async updateIntegrationMessages(id, type, titleOrItem, body, timestamp) {
75
+ async updateIntegrationMessages(id, type, title, body, timestamp) {
77
76
  const rec = this.store.get(id);
78
77
  if (!rec) {
79
78
  this.operationHistory.push({ operation: 'updateMessages', id, success: false });
80
79
  return false;
81
80
  }
82
81
  if (!rec.messages[type]) rec.messages[type] = [];
83
- rec.messages[type].push(toMessageItem(titleOrItem, body, timestamp));
82
+ rec.messages[type].push({ title, message: body, timestamp });
84
83
  this.operationHistory.push({ operation: 'updateMessages', id, type, success: true });
85
84
  return true;
86
85
  }
87
86
 
88
- async findIntegrationMessages(id, type) {
89
- const rec = this.store.get(id);
90
- this.operationHistory.push({
91
- operation: 'findMessages',
92
- id,
93
- type,
94
- found: !!rec,
95
- });
96
- if (!rec) throw new Error(`Integration ${id} not found`);
97
- return [...(rec.messages[type] ?? [])];
98
- }
99
-
100
87
  async updateIntegrationConfig(id, config) {
101
88
  if (config === null || config === undefined) {
102
89
  throw new Error('Config parameter is required');
@@ -13,21 +13,19 @@ class UpdateIntegrationMessages {
13
13
  }
14
14
 
15
15
  /**
16
- * Executes the integration messages update. Pass the positional form
17
- * (title, body, timestamp) or one message item object; the keys of an item
18
- * object are stored as they are.
16
+ * Executes the integration messages update.
19
17
  * @async
20
18
  * @param {string} integrationId - ID of the integration to update.
21
19
  * @param {string} messageType - Type of message: 'errors', 'warnings', 'info', or 'logs'.
22
- * @param {string|Object} messageTitleOrItem - Title of the message, or the whole message item.
23
- * @param {string} [messageBody] - Body content of the message (positional form).
24
- * @param {string} [messageTimestamp] - Timestamp when the message was created (positional form).
20
+ * @param {string} messageTitle - Title of the message.
21
+ * @param {string} messageBody - Body content of the message.
22
+ * @param {string} messageTimestamp - Timestamp when the message was created.
25
23
  * @returns {Promise<Object>} The updated integration record.
26
24
  */
27
25
  async execute(
28
26
  integrationId,
29
27
  messageType,
30
- messageTitleOrItem,
28
+ messageTitle,
31
29
  messageBody,
32
30
  messageTimestamp
33
31
  ) {
@@ -35,7 +33,7 @@ class UpdateIntegrationMessages {
35
33
  await this.integrationRepository.updateIntegrationMessages(
36
34
  integrationId,
37
35
  messageType,
38
- messageTitleOrItem,
36
+ messageTitle,
39
37
  messageBody,
40
38
  messageTimestamp
41
39
  );
package/logs/serialize.js CHANGED
@@ -235,6 +235,20 @@ function errorCode(err) {
235
235
  return undefined;
236
236
  }
237
237
 
238
+ function rateLimitFields(err) {
239
+ const retryAt = attempt(
240
+ () => (err.isRateLimited ? err.retryAt : undefined),
241
+ undefined
242
+ );
243
+ if (!(retryAt instanceof Date) || Number.isNaN(retryAt.getTime())) {
244
+ return {};
245
+ }
246
+ const reason = attempt(() => err.reason, undefined);
247
+ return typeof reason === 'string'
248
+ ? { retryAt: retryAt.toISOString(), reason: cleanString(reason) }
249
+ : { retryAt: retryAt.toISOString() };
250
+ }
251
+
238
252
  function errorStack(err, type, message) {
239
253
  const stack = attempt(() => err.stack, undefined);
240
254
  if (typeof stack !== 'string') return undefined;
@@ -279,6 +293,7 @@ function serializeErrorAt(err, depth, seen) {
279
293
  if (code !== undefined) out.code = code;
280
294
  const status = errorStatus(err);
281
295
  if (status !== undefined) out.status = status;
296
+ Object.assign(out, rateLimitFields(err));
282
297
  const stack = errorStack(err, type, message);
283
298
  if (stack !== undefined) out.stack = stack;
284
299