@friggframework/core 2.0.0--canary.608.03436383054a.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,4 +1,6 @@
1
1
  const { Worker } = require('../../core/Worker');
2
+ // Direct path (not the package index) avoids a circular require.
3
+ const { createHandler } = require('../../core/create-handler');
2
4
  const {
3
5
  GetIntegrationInstance,
4
6
  } = require('../../integrations/use-cases/get-integration-instance');
@@ -14,21 +16,11 @@ const {
14
16
  } = require('../../integrations/utils/map-integration-dto');
15
17
 
16
18
  /**
17
- * App-level worker for events dispatched with `dispatch: 'queue'`.
18
- *
19
- * A single Lambda serves every integration: messages arrive on the FIFO queue
20
- * serialized per `MessageGroupId = integrationId`, so concurrent mutations for
21
- * one integration run one-at-a-time. The worker re-hydrates the integration by
22
- * id + owning user and runs `instance.send(event, data)` — byte-identical to
23
- * the in-process (sync) path it replaces.
24
- *
25
- * Failure handling:
26
- * - missing ids / un-hydratable / id mismatch → HaltError (discard, no retry)
27
- * - handler throw → record ERROR status + a warning message, then rethrow so
28
- * SQS retries and ultimately routes to the FIFO DLQ.
29
- *
30
- * DISABLED/ERROR integrations are NOT discarded — these are user-initiated
31
- * 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).
32
24
  */
33
25
  class UserActionWorker extends Worker {
34
26
  constructor({ getIntegrationInstance } = {}) {
@@ -63,8 +55,6 @@ class UserActionWorker extends Worker {
63
55
  }
64
56
 
65
57
  async _run(params) {
66
- // Routing metadata is at the envelope top level; `data` is the exact
67
- // handler payload (byte-identical to the sync path).
68
58
  const { event, data = {}, integrationId, userId, requestId } = params;
69
59
  const logCtx = { event, integrationId, userId, requestId };
70
60
 
@@ -86,11 +76,8 @@ class UserActionWorker extends Worker {
86
76
  userId
87
77
  );
88
78
  } catch (error) {
89
- // Discard ONLY terminal failures (gone / not owned / unknown class)
90
- // no retry can ever succeed. Transient failures (DB blip, Prisma
91
- // timeout, KMS throttle during credential decrypt) are NOT marked
92
- // terminal, so they retry and ultimately reach the FIFO DLQ instead
93
- // of being silently dropped.
79
+ // Only terminal failures discard; transient ones (DB/Prisma/KMS
80
+ // blips) retry FIFO DLQ rather than being silently dropped.
94
81
  if (error.isTerminal) {
95
82
  error.isHaltError = true;
96
83
  console.warn(
@@ -148,8 +135,12 @@ class UserActionWorker extends Worker {
148
135
  }
149
136
  }
150
137
 
151
- async function userActionQueueWorker(event, context) {
152
- return new UserActionWorker().run(event, context);
153
- }
138
+ // createHandler gives the Lambda the standard worker setup (secretsToEnv,
139
+ // connectPrisma, callbackWaitsForEmptyEventLoop) and passes the result through.
140
+ const userActionQueueWorker = createHandler({
141
+ eventName: 'UserActionQueueWorker',
142
+ isUserFacingResponse: false,
143
+ method: async (event, context) => new UserActionWorker().run(event, context),
144
+ });
154
145
 
155
146
  module.exports = { UserActionWorker, userActionQueueWorker };
@@ -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,17 +3,10 @@ const { QueuerUtil } = require('../../queues');
3
3
 
4
4
  const SCHEMA_VERSION = 1;
5
5
 
6
- // Default lifecycle events that MUTATE and may therefore be routed to the queue.
7
- // Read-shaped defaults (GET_*/REFRESH_*/WEBHOOK_RECEIVED) must never be queued —
8
- // a queued read would return a 202 ack instead of the data the caller expects.
9
- const MUTATING_DEFAULT_EVENTS = new Set([
10
- 'ON_CREATE',
11
- 'ON_UPDATE',
12
- 'ON_DELETE',
13
- ]);
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.
8
+ const MUTATING_DEFAULT_EVENTS = new Set(['ON_CREATE', 'ON_UPDATE']);
14
9
 
15
- // Custom user actions (events not present in defaultEvents) are mutating by
16
- // intent; only default events are filtered against the allowlist above.
17
10
  function isMutatingEvent(instance, event) {
18
11
  if (!instance.defaultEvents || !instance.defaultEvents[event]) {
19
12
  return true;
@@ -21,22 +14,9 @@ function isMutatingEvent(instance, event) {
21
14
  return MUTATING_DEFAULT_EVENTS.has(event);
22
15
  }
23
16
 
24
- /**
25
- * Central producer decision for dispatching an integration event.
26
- *
27
- * If the event opts into `dispatch: 'queue'`, is a mutating event, and the
28
- * framework queue is configured, the event is enqueued on the app-level FIFO
29
- * queue (serialized per integrationId) and a `{ queued }` ack is returned.
30
- * Otherwise the handler runs in-process and its result is returned. When the
31
- * queue URL is missing we degrade gracefully to in-process execution.
32
- *
33
- * @param {Object} args
34
- * @param {Object} args.instance - Initialized integration instance (has `on`, `id`, `send`).
35
- * @param {string} args.event - Event name to dispatch.
36
- * @param {Object} args.data - Payload passed to the handler / placed in the envelope.
37
- * @param {string} args.userId - Owning user id (used by the worker to re-hydrate).
38
- * @returns {Promise<{queued:true, messageId:string, requestId:string} | {result:any}>}
39
- */
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.
40
20
  async function dispatchIntegrationEvent({ instance, event, data, userId }) {
41
21
  const mode = instance.on?.[event]?.dispatch;
42
22
  const queueUrl = process.env.USER_ACTION_QUEUE_URL;
@@ -52,9 +32,7 @@ async function dispatchIntegrationEvent({ instance, event, data, userId }) {
52
32
 
53
33
  if (eligible && queueUrl) {
54
34
  const requestId = uuid();
55
- // Routing metadata lives at the envelope top level (alongside event),
56
- // NOT inside `data`. `data` stays the exact handler payload the sync
57
- // 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.
58
36
  const envelope = {
59
37
  schemaVersion: SCHEMA_VERSION,
60
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.03436383054a.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.03436383054a.0",
42
- "@friggframework/prettier-config": "2.0.0--canary.608.03436383054a.0",
43
- "@friggframework/test": "2.0.0--canary.608.03436383054a.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": "03436383054a3ee70bd4de8f13a1907890a627da"
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),