@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.
- package/handlers/workers/user-action-worker.js +16 -25
- package/integrations/integration-base.js +3 -8
- package/integrations/use-cases/create-integration.js +0 -3
- package/integrations/use-cases/dispatch-integration-event.js +7 -29
- package/integrations/use-cases/get-integration-instance.js +1 -3
- package/integrations/use-cases/update-integration.js +0 -4
- package/package.json +5 -5
- package/queues/queuer-util.js +2 -5
|
@@ -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
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
//
|
|
90
|
-
//
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
533
|
-
//
|
|
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
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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.
|
|
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.
|
|
42
|
-
"@friggframework/prettier-config": "2.0.0--canary.608.
|
|
43
|
-
"@friggframework/test": "2.0.0--canary.608.
|
|
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": "
|
|
83
|
+
"gitHead": "e6b65ff3bf4a3b3e7777a1f2a6cf1cecd24bedd7"
|
|
84
84
|
}
|
package/queues/queuer-util.js
CHANGED
|
@@ -90,11 +90,8 @@ const inspectBatchResult = (result, queueUrl, buffer) => {
|
|
|
90
90
|
};
|
|
91
91
|
|
|
92
92
|
const QueuerUtil = {
|
|
93
|
-
//
|
|
94
|
-
// Standard sends
|
|
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),
|