@friggframework/core 2.0.0--canary.608.4bec88a.0 → 2.0.0--canary.608.e6b65ff.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.
@@ -1,6 +1,5 @@
1
1
  const { Worker } = require('../../core/Worker');
2
- // Direct path (not the package index) to avoid a circular require: index.js
3
- // re-exports this module.
2
+ // Direct path (not the package index) avoids a circular require.
4
3
  const { createHandler } = require('../../core/create-handler');
5
4
  const {
6
5
  GetIntegrationInstance,
@@ -17,21 +16,11 @@ const {
17
16
  } = require('../../integrations/utils/map-integration-dto');
18
17
 
19
18
  /**
20
- * App-level worker for events dispatched with `dispatch: 'queue'`.
21
- *
22
- * A single Lambda serves every integration: messages arrive on the FIFO queue
23
- * serialized per `MessageGroupId = integrationId`, so concurrent mutations for
24
- * one integration run one-at-a-time. The worker re-hydrates the integration by
25
- * id + owning user and runs `instance.send(event, data)` — byte-identical to
26
- * the in-process (sync) path it replaces.
27
- *
28
- * Failure handling:
29
- * - missing ids / un-hydratable / id mismatch → HaltError (discard, no retry)
30
- * - handler throw → record ERROR status + a warning message, then rethrow so
31
- * SQS retries and ultimately routes to the FIFO DLQ.
32
- *
33
- * DISABLED/ERROR integrations are NOT discarded — these are user-initiated
34
- * mutations (including ERROR recovery), unlike webhook/cron traffic.
19
+ * App-level worker for `dispatch: 'queue'` events. One Lambda serves every
20
+ * integration; the FIFO queue serializes per integrationId. Re-hydrates by id +
21
+ * owning user, then runs `instance.send(event, data)` (identical to the sync path).
22
+ * Terminal failures discard; everything else retries FIFO DLQ. DISABLED/ERROR
23
+ * integrations are still processed (these are user-initiated mutations).
35
24
  */
36
25
  class UserActionWorker extends Worker {
37
26
  constructor({ getIntegrationInstance } = {}) {
@@ -66,8 +55,6 @@ class UserActionWorker extends Worker {
66
55
  }
67
56
 
68
57
  async _run(params) {
69
- // Routing metadata is at the envelope top level; `data` is the exact
70
- // handler payload (byte-identical to the sync path).
71
58
  const { event, data = {}, integrationId, userId, requestId } = params;
72
59
  const logCtx = { event, integrationId, userId, requestId };
73
60
 
@@ -89,11 +76,8 @@ class UserActionWorker extends Worker {
89
76
  userId
90
77
  );
91
78
  } catch (error) {
92
- // Discard ONLY terminal failures (gone / not owned / unknown class)
93
- // no retry can ever succeed. Transient failures (DB blip, Prisma
94
- // timeout, KMS throttle during credential decrypt) are NOT marked
95
- // terminal, so they retry and ultimately reach the FIFO DLQ instead
96
- // of being silently dropped.
79
+ // Only terminal failures discard; transient ones (DB/Prisma/KMS
80
+ // blips) retry FIFO DLQ rather than being silently dropped.
97
81
  if (error.isTerminal) {
98
82
  error.isHaltError = true;
99
83
  console.warn(
@@ -151,10 +135,8 @@ class UserActionWorker extends Worker {
151
135
  }
152
136
  }
153
137
 
154
- // Wrap in createHandler so the Lambda gets the same runtime setup as every
155
- // other DB-touching worker: secretsToEnv() (SECRET_ARN injection), connectPrisma()
156
- // (+ Mongo schema init), and callbackWaitsForEmptyEventLoop=false. The method's
157
- // return value ({ batchItemFailures }) passes through for ReportBatchItemFailures.
138
+ // createHandler gives the Lambda the standard worker setup (secretsToEnv,
139
+ // connectPrisma, callbackWaitsForEmptyEventLoop) and passes the result through.
158
140
  const userActionQueueWorker = createHandler({
159
141
  eventName: 'UserActionQueueWorker',
160
142
  isUserFacingResponse: false,
@@ -31,9 +31,7 @@ const constantsToBeMigrated = {
31
31
  LIFE_CYCLE_EVENT: 'LIFE_CYCLE_EVENT',
32
32
  USER_ACTION: 'USER_ACTION',
33
33
  },
34
- // Per-event dispatch mode. 'sync' (default) runs the handler in-process and
35
- // returns its result; 'queue' routes the event through the framework-owned
36
- // FIFO queue, serialized by integrationId, returning a 202 ack.
34
+ // 'sync' runs in-process (default); 'queue' routes through the FIFO queue.
37
35
  dispatch: {
38
36
  SYNC: 'sync',
39
37
  QUEUE: 'queue',
@@ -529,11 +527,8 @@ class IntegrationBase {
529
527
  ...this.events,
530
528
  };
531
529
 
532
- // Apply Definition.eventDispatch onto the resolved handler entries. This
533
- // lets a default lifecycle event (e.g. ON_UPDATE) be marked 'queue'
534
- // without re-declaring its handler. An inline `dispatch` on the event
535
- // entry always wins; unknown event names are ignored (a typo stays sync
536
- // rather than crashing initialize()).
530
+ // Definition.eventDispatch marks default events; inline dispatch wins,
531
+ // unknown names are ignored.
537
532
  const eventDispatch = this.constructor.Definition?.eventDispatch || {};
538
533
  for (const [eventName, mode] of Object.entries(eventDispatch)) {
539
534
  if (this.on[eventName] && this.on[eventName].dispatch === undefined) {
@@ -72,9 +72,6 @@ class CreateIntegration {
72
72
  modules,
73
73
  });
74
74
 
75
- // ON_CREATE runs in-process by default. Routed through the dispatch
76
- // helper so an integration can opt into dispatch:'queue' if desired;
77
- // when queued, an ack is returned instead of the DTO.
78
75
  await integrationInstance.initialize();
79
76
  const outcome = await dispatchIntegrationEvent({
80
77
  instance: integrationInstance,
@@ -3,16 +3,10 @@ const { QueuerUtil } = require('../../queues');
3
3
 
4
4
  const SCHEMA_VERSION = 1;
5
5
 
6
- // Default lifecycle events that MUTATE and are routed through this helper, so
7
- // they may be queued. Read-shaped defaults (GET_*/REFRESH_*/WEBHOOK_RECEIVED)
8
- // must never be queued — a queued read would return a 202 ack instead of the
9
- // data the caller expects. ON_DELETE is intentionally excluded: deletion is
10
- // dispatched directly (and the record is gone immediately after), so a queued
11
- // worker re-hydrating by id would discard it as terminal.
6
+ // ON_DELETE is excluded: it dispatches directly and the record is gone before a
7
+ // queued worker could re-hydrate it. Custom (non-default) events are mutating.
12
8
  const MUTATING_DEFAULT_EVENTS = new Set(['ON_CREATE', 'ON_UPDATE']);
13
9
 
14
- // Custom user actions (events not present in defaultEvents) are mutating by
15
- // intent; only default events are filtered against the allowlist above.
16
10
  function isMutatingEvent(instance, event) {
17
11
  if (!instance.defaultEvents || !instance.defaultEvents[event]) {
18
12
  return true;
@@ -20,22 +14,9 @@ function isMutatingEvent(instance, event) {
20
14
  return MUTATING_DEFAULT_EVENTS.has(event);
21
15
  }
22
16
 
23
- /**
24
- * Central producer decision for dispatching an integration event.
25
- *
26
- * If the event opts into `dispatch: 'queue'`, is a mutating event, and the
27
- * framework queue is configured, the event is enqueued on the app-level FIFO
28
- * queue (serialized per integrationId) and a `{ queued }` ack is returned.
29
- * Otherwise the handler runs in-process and its result is returned. When the
30
- * queue URL is missing we degrade gracefully to in-process execution.
31
- *
32
- * @param {Object} args
33
- * @param {Object} args.instance - Initialized integration instance (has `on`, `id`, `send`).
34
- * @param {string} args.event - Event name to dispatch.
35
- * @param {Object} args.data - Payload passed to the handler / placed in the envelope.
36
- * @param {string} args.userId - Owning user id (used by the worker to re-hydrate).
37
- * @returns {Promise<{queued:true, messageId:string, requestId:string} | {result:any}>}
38
- */
17
+ // Enqueues the event (returning a { queued } ack) when it opts into
18
+ // dispatch:'queue' and the queue is configured; otherwise runs it in-process
19
+ // and returns the result. Missing queue URL degrades to in-process.
39
20
  async function dispatchIntegrationEvent({ instance, event, data, userId }) {
40
21
  const mode = instance.on?.[event]?.dispatch;
41
22
  const queueUrl = process.env.USER_ACTION_QUEUE_URL;
@@ -51,9 +32,7 @@ async function dispatchIntegrationEvent({ instance, event, data, userId }) {
51
32
 
52
33
  if (eligible && queueUrl) {
53
34
  const requestId = uuid();
54
- // Routing metadata lives at the envelope top level (alongside event),
55
- // NOT inside `data`. `data` stays the exact handler payload the sync
56
- // path passes, so the worker's send(event, data) is byte-identical.
35
+ // Routing metadata at the top level keeps `data` identical to the sync path.
57
36
  const envelope = {
58
37
  schemaVersion: SCHEMA_VERSION,
59
38
  event,
@@ -34,7 +34,7 @@ class GetIntegrationInstance {
34
34
  const error = new Error(
35
35
  `No integration found by the ID of ${integrationId}`
36
36
  );
37
- // Terminal: the integration does not exist no retry can succeed.
37
+ // isTerminal the queue worker discards these (no retry can succeed).
38
38
  error.isTerminal = true;
39
39
  throw error;
40
40
  }
@@ -49,7 +49,6 @@ class GetIntegrationInstance {
49
49
  const error = new Error(
50
50
  `No integration class found for type: ${integrationRecord.config.type}`
51
51
  );
52
- // Terminal: the integration type is not registered — no retry helps.
53
52
  error.isTerminal = true;
54
53
  throw error;
55
54
  }
@@ -58,7 +57,6 @@ class GetIntegrationInstance {
58
57
  const error = new Error(
59
58
  `Integration ${integrationId} does not belong to User ${userId}`
60
59
  );
61
- // Terminal: ownership mismatch — no retry can succeed.
62
60
  error.isTerminal = true;
63
61
  throw error;
64
62
  }
@@ -82,10 +82,6 @@ class UpdateIntegration {
82
82
  modules,
83
83
  });
84
84
 
85
- // 5. Complete async initialization and dispatch the update event.
86
- // When ON_UPDATE is marked dispatch:'queue', the update is enqueued
87
- // (serialized per integration) and an ack is returned; otherwise it
88
- // runs in-process and the updated DTO is returned (unchanged behavior).
89
85
  await integrationInstance.initialize();
90
86
  const outcome = await dispatchIntegrationEvent({
91
87
  instance: integrationInstance,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.608.4bec88a.0",
4
+ "version": "2.0.0--canary.608.e6b65ff.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -38,9 +38,9 @@
38
38
  }
39
39
  },
40
40
  "devDependencies": {
41
- "@friggframework/eslint-config": "2.0.0--canary.608.4bec88a.0",
42
- "@friggframework/prettier-config": "2.0.0--canary.608.4bec88a.0",
43
- "@friggframework/test": "2.0.0--canary.608.4bec88a.0",
41
+ "@friggframework/eslint-config": "2.0.0--canary.608.e6b65ff.0",
42
+ "@friggframework/prettier-config": "2.0.0--canary.608.e6b65ff.0",
43
+ "@friggframework/test": "2.0.0--canary.608.e6b65ff.0",
44
44
  "@prisma/client": "^6.19.3",
45
45
  "@types/lodash": "4.17.15",
46
46
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -80,5 +80,5 @@
80
80
  "publishConfig": {
81
81
  "access": "public"
82
82
  },
83
- "gitHead": "4bec88a53c1c12df8f84aac9b2eeb6b23ba04c05"
83
+ "gitHead": "e6b65ff3bf4a3b3e7777a1f2a6cf1cecd24bedd7"
84
84
  }
@@ -90,11 +90,8 @@ const inspectBatchResult = (result, queueUrl, buffer) => {
90
90
  };
91
91
 
92
92
  const QueuerUtil = {
93
- // `messageGroupId`/`messageDeduplicationId` are only used for FIFO queues.
94
- // Standard sends omit them, so the command is byte-identical to before.
95
- // FIFO queues require a deduplication id whenever ContentBasedDeduplication
96
- // is off — we default to a uuid so every send is treated as distinct (two
97
- // distinct requests with identical bodies must both run, not be dropped).
93
+ // FIFO-only: default a unique dedup id so identical bodies aren't dropped.
94
+ // Standard sends pass no opts and stay unchanged.
98
95
  send: async (message, queueUrl, { messageGroupId, messageDeduplicationId } = {}) => {
99
96
  const command = new SendMessageCommand({
100
97
  MessageBody: JSON.stringify(message),