@friggframework/core 2.0.0--canary.522.893db5d.0 → 2.0.0--canary.545.7dc47e9.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/index.js CHANGED
@@ -92,7 +92,12 @@ const utils = require('./utils');
92
92
 
93
93
  // const {Sync } = require('./syncs/model');
94
94
 
95
- const { QueuerUtil } = require('./queues');
95
+ const {
96
+ QueuerUtil,
97
+ QueueProvider,
98
+ createQueueProvider,
99
+ QUEUE_PROVIDERS,
100
+ } = require('./queues');
96
101
 
97
102
  module.exports = {
98
103
  // assertions
@@ -187,6 +192,9 @@ module.exports = {
187
192
  ModuleFactory,
188
193
  // queues
189
194
  QueuerUtil,
195
+ QueueProvider,
196
+ createQueueProvider,
197
+ QUEUE_PROVIDERS,
190
198
 
191
199
  // utils
192
200
  ...utils,
@@ -5,13 +5,15 @@
5
5
  * Follows hexagonal architecture with interface + adapters pattern.
6
6
  *
7
7
  * Providers:
8
- * - eventbridge: AWS EventBridge Scheduler (production)
8
+ * - eventbridge: AWS EventBridge Scheduler (production on AWS)
9
+ * - netlify: Netlify poll-and-dispatch scheduler (production on Netlify)
9
10
  * - mock: In-memory mock scheduler (local development)
10
11
  */
11
12
 
12
13
  const { SchedulerServiceInterface } = require('./scheduler-service-interface');
13
14
  const { EventBridgeSchedulerAdapter } = require('./eventbridge-scheduler-adapter');
14
15
  const { MockSchedulerAdapter } = require('./mock-scheduler-adapter');
16
+ const { NetlifySchedulerAdapter } = require('./netlify-scheduler-adapter');
15
17
  const {
16
18
  createSchedulerService,
17
19
  SCHEDULER_PROVIDERS,
@@ -25,6 +27,7 @@ module.exports = {
25
27
  // Adapters
26
28
  EventBridgeSchedulerAdapter,
27
29
  MockSchedulerAdapter,
30
+ NetlifySchedulerAdapter,
28
31
 
29
32
  // Factory
30
33
  createSchedulerService,
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Netlify Scheduler Adapter
3
+ *
4
+ * Implements SchedulerServiceInterface for Netlify deployments.
5
+ *
6
+ * Netlify Scheduled Functions use cron syntax but are defined statically in
7
+ * netlify.toml or via the @netlify/functions schedule() helper. They cannot
8
+ * be created dynamically at runtime like EventBridge Scheduler.
9
+ *
10
+ * Strategy for one-time scheduled jobs on Netlify:
11
+ * 1. Store the schedule in the database (same as mock adapter pattern)
12
+ * 2. A Netlify Scheduled Function runs on a cron interval (e.g., every 5 min)
13
+ * 3. The cron function queries for due schedules and dispatches them
14
+ * to a background function for execution
15
+ *
16
+ * This is a "poll-and-dispatch" pattern vs EventBridge's "push" pattern.
17
+ * Trade-off: slightly less precise timing (up to cron interval delay),
18
+ * but works on any platform without cloud-specific scheduling APIs.
19
+ *
20
+ * For integrations that need the queue provider to dispatch jobs:
21
+ * - queueResourceId maps to the background function URL path
22
+ * - Payload is stored and forwarded when the cron fires
23
+ */
24
+
25
+ const { SchedulerServiceInterface } = require('./scheduler-service-interface');
26
+
27
+ class NetlifySchedulerAdapter extends SchedulerServiceInterface {
28
+ /**
29
+ * @param {Object} options
30
+ * @param {Object} options.repository - Repository for persisting schedules
31
+ * Must implement: save(schedule), delete(scheduleName),
32
+ * findByName(scheduleName), findDue(now)
33
+ * @param {Object} [options.queueProvider] - Queue provider for dispatching due jobs
34
+ */
35
+ constructor(options = {}) {
36
+ super();
37
+ this.repository = options.repository;
38
+ this.queueProvider = options.queueProvider;
39
+
40
+ if (!this.repository) {
41
+ console.warn(
42
+ '[NetlifyScheduler] No repository provided. ' +
43
+ 'Schedules will be stored in memory (lost on function restart). ' +
44
+ 'Provide a database-backed repository for production use.'
45
+ );
46
+ this._inMemorySchedules = new Map();
47
+ }
48
+ }
49
+
50
+ async scheduleOneTime({
51
+ scheduleName,
52
+ scheduleAt,
53
+ queueResourceId,
54
+ payload,
55
+ }) {
56
+ if (!scheduleName) throw new Error('scheduleName is required');
57
+ if (!scheduleAt || !(scheduleAt instanceof Date))
58
+ throw new Error('scheduleAt must be a valid Date object');
59
+ if (!queueResourceId) throw new Error('queueResourceId is required');
60
+
61
+ const scheduleData = {
62
+ scheduleName,
63
+ scheduledAt: scheduleAt.toISOString(),
64
+ queueResourceId,
65
+ payload,
66
+ createdAt: new Date().toISOString(),
67
+ state: 'PENDING',
68
+ };
69
+
70
+ if (this.repository) {
71
+ await this.repository.save(scheduleData);
72
+ } else {
73
+ this._inMemorySchedules.set(scheduleName, scheduleData);
74
+ }
75
+
76
+ console.log(
77
+ `[NetlifyScheduler] Scheduled: ${scheduleName} for ${scheduleAt.toISOString()}`
78
+ );
79
+
80
+ return {
81
+ scheduledJobId: `netlify-schedule-${scheduleName}`,
82
+ scheduledAt: scheduleAt.toISOString(),
83
+ };
84
+ }
85
+
86
+ async deleteSchedule(scheduleName) {
87
+ if (!scheduleName) throw new Error('scheduleName is required');
88
+
89
+ if (this.repository) {
90
+ await this.repository.delete(scheduleName);
91
+ } else {
92
+ this._inMemorySchedules.delete(scheduleName);
93
+ }
94
+
95
+ console.log(`[NetlifyScheduler] Deleted schedule: ${scheduleName}`);
96
+ }
97
+
98
+ async getScheduleStatus(scheduleName) {
99
+ if (!scheduleName) throw new Error('scheduleName is required');
100
+
101
+ let schedule;
102
+ if (this.repository) {
103
+ schedule = await this.repository.findByName(scheduleName);
104
+ } else {
105
+ schedule = this._inMemorySchedules.get(scheduleName);
106
+ }
107
+
108
+ if (!schedule) {
109
+ return { exists: false };
110
+ }
111
+
112
+ return {
113
+ exists: true,
114
+ scheduledAt: schedule.scheduledAt,
115
+ state: schedule.state,
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Process due schedules. Called by the Netlify cron function.
121
+ *
122
+ * Finds all schedules where scheduledAt <= now, dispatches them
123
+ * to the queue provider, and marks them as completed.
124
+ *
125
+ * @returns {Promise<{ processed: number, errors: number }>}
126
+ */
127
+ async processDueSchedules() {
128
+ if (!this.repository) {
129
+ console.warn(
130
+ '[NetlifyScheduler] Cannot process due schedules without a repository'
131
+ );
132
+ return { processed: 0, errors: 0 };
133
+ }
134
+
135
+ const now = new Date();
136
+ const dueSchedules = await this.repository.findDue(now);
137
+
138
+ let processed = 0;
139
+ let errors = 0;
140
+
141
+ for (const schedule of dueSchedules) {
142
+ try {
143
+ if (this.queueProvider) {
144
+ await this.queueProvider.send(
145
+ schedule.payload,
146
+ schedule.queueResourceId
147
+ );
148
+ } else {
149
+ console.log(
150
+ `[NetlifyScheduler] No queue provider — logging payload for: ${schedule.scheduleName}`,
151
+ JSON.stringify(schedule.payload)
152
+ );
153
+ }
154
+
155
+ await this.repository.delete(schedule.scheduleName);
156
+ processed++;
157
+ } catch (error) {
158
+ console.error(
159
+ `[NetlifyScheduler] Error processing schedule ${schedule.scheduleName}:`,
160
+ error
161
+ );
162
+ errors++;
163
+ }
164
+ }
165
+
166
+ console.log(
167
+ `[NetlifyScheduler] Processed ${processed} due schedules, ${errors} errors`
168
+ );
169
+ return { processed, errors };
170
+ }
171
+ }
172
+
173
+ module.exports = { NetlifySchedulerAdapter };
@@ -13,10 +13,12 @@
13
13
 
14
14
  const { EventBridgeSchedulerAdapter } = require('./eventbridge-scheduler-adapter');
15
15
  const { MockSchedulerAdapter } = require('./mock-scheduler-adapter');
16
+ const { NetlifySchedulerAdapter } = require('./netlify-scheduler-adapter');
16
17
 
17
18
  const SCHEDULER_PROVIDERS = {
18
19
  EVENTBRIDGE: 'eventbridge',
19
20
  MOCK: 'mock',
21
+ NETLIFY: 'netlify',
20
22
  };
21
23
 
22
24
  const LOCAL_STAGES = ['dev', 'test', 'local'];
@@ -44,9 +46,11 @@ function determineProvider() {
44
46
  * Create a scheduler service instance
45
47
  *
46
48
  * @param {Object} options
47
- * @param {string} options.provider - Scheduler provider ('eventbridge' or 'mock')
49
+ * @param {string} options.provider - Scheduler provider ('eventbridge', 'mock', or 'netlify')
48
50
  * @param {string} options.region - AWS region (for EventBridge)
49
51
  * @param {boolean} options.verbose - Verbose logging (for Mock)
52
+ * @param {Object} options.repository - Schedule repository (for Netlify - persists schedules)
53
+ * @param {Object} options.queueProvider - Queue provider (for Netlify - dispatches due jobs)
50
54
  * @returns {SchedulerServiceInterface} Implementation of scheduler interface
51
55
  */
52
56
  function createSchedulerService(options = {}) {
@@ -61,6 +65,11 @@ function createSchedulerService(options = {}) {
61
65
  return new MockSchedulerAdapter({
62
66
  verbose: options.verbose,
63
67
  });
68
+ case SCHEDULER_PROVIDERS.NETLIFY:
69
+ return new NetlifySchedulerAdapter({
70
+ repository: options.repository,
71
+ queueProvider: options.queueProvider,
72
+ });
64
73
  default:
65
74
  throw new Error(`Unknown scheduler provider: ${provider}`);
66
75
  }
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.522.893db5d.0",
4
+ "version": "2.0.0--canary.545.7dc47e9.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -39,9 +39,9 @@
39
39
  }
40
40
  },
41
41
  "devDependencies": {
42
- "@friggframework/eslint-config": "2.0.0--canary.522.893db5d.0",
43
- "@friggframework/prettier-config": "2.0.0--canary.522.893db5d.0",
44
- "@friggframework/test": "2.0.0--canary.522.893db5d.0",
42
+ "@friggframework/eslint-config": "2.0.0--canary.545.7dc47e9.0",
43
+ "@friggframework/prettier-config": "2.0.0--canary.545.7dc47e9.0",
44
+ "@friggframework/test": "2.0.0--canary.545.7dc47e9.0",
45
45
  "@prisma/client": "^6.17.0",
46
46
  "@types/lodash": "4.17.15",
47
47
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -82,5 +82,5 @@
82
82
  "publishConfig": {
83
83
  "access": "public"
84
84
  },
85
- "gitHead": "893db5dc366945920987fecc1e8ab53754f76f63"
85
+ "gitHead": "7dc47e99f0c313a737dba49babf5c32bad7adae6"
86
86
  }
package/queues/index.js CHANGED
@@ -1,4 +1,23 @@
1
1
  const { QueuerUtil } = require('./queuer-util');
2
+ const { QueueProvider } = require('./queue-provider');
3
+ const {
4
+ createQueueProvider,
5
+ determineProvider,
6
+ QUEUE_PROVIDERS,
7
+ } = require('./queue-provider-factory');
8
+ const {
9
+ SqsQueueProvider,
10
+ NetlifyBackgroundProvider,
11
+ QStashQueueProvider,
12
+ } = require('./providers');
13
+
2
14
  module.exports = {
3
15
  QueuerUtil,
16
+ QueueProvider,
17
+ createQueueProvider,
18
+ determineProvider,
19
+ QUEUE_PROVIDERS,
20
+ SqsQueueProvider,
21
+ NetlifyBackgroundProvider,
22
+ QStashQueueProvider,
4
23
  };
@@ -0,0 +1,9 @@
1
+ const { SqsQueueProvider } = require('./sqs-queue-provider');
2
+ const { NetlifyBackgroundProvider } = require('./netlify-background-provider');
3
+ const { QStashQueueProvider } = require('./qstash-queue-provider');
4
+
5
+ module.exports = {
6
+ SqsQueueProvider,
7
+ NetlifyBackgroundProvider,
8
+ QStashQueueProvider,
9
+ };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Netlify Background Functions Queue Provider (Adapter)
3
+ *
4
+ * Uses Netlify Background Functions for async job processing.
5
+ * Background functions have a 15-minute execution limit and are triggered via HTTP POST.
6
+ * They return 202 immediately while processing continues.
7
+ *
8
+ * Queue ID format: The function name or URL path for the background function.
9
+ * Convention: function files named with '-background' suffix (e.g., 'worker-background.js')
10
+ *
11
+ * Limitations:
12
+ * - No built-in retry (must implement manually)
13
+ * - No dead-letter queue
14
+ * - No FIFO ordering guarantees
15
+ * - 15-minute max execution time
16
+ */
17
+ const { QueueProvider } = require('../queue-provider');
18
+
19
+ class NetlifyBackgroundProvider extends QueueProvider {
20
+ constructor(options = {}) {
21
+ super();
22
+ // Base URL for triggering background functions (e.g., https://mysite.netlify.app)
23
+ this.baseUrl = options.baseUrl || process.env.URL || '';
24
+ }
25
+
26
+ /**
27
+ * Send a message by invoking a Netlify Background Function via HTTP POST.
28
+ *
29
+ * @param {Object} message - Message payload
30
+ * @param {string} queueId - Background function path (e.g., '/.netlify/functions/worker-background')
31
+ */
32
+ async send(message, queueId) {
33
+ const url = `${this.baseUrl}${queueId}`;
34
+ const response = await fetch(url, {
35
+ method: 'POST',
36
+ headers: { 'Content-Type': 'application/json' },
37
+ body: JSON.stringify(message),
38
+ });
39
+
40
+ if (!response.ok && response.status !== 202) {
41
+ throw new Error(
42
+ `Failed to invoke background function at ${queueId}: ${response.status} ${response.statusText}`
43
+ );
44
+ }
45
+
46
+ return { status: response.status, queueId };
47
+ }
48
+
49
+ /**
50
+ * Send batch of messages sequentially (background functions are HTTP-triggered).
51
+ */
52
+ async batchSend(entries = [], queueId) {
53
+ const results = [];
54
+ for (const entry of entries) {
55
+ const result = await this.send(entry, queueId);
56
+ results.push(result);
57
+ }
58
+ return { results };
59
+ }
60
+
61
+ /**
62
+ * Parse Netlify Background Function event.
63
+ * Background functions receive the raw HTTP event body as the event.
64
+ */
65
+ parseEvent(event) {
66
+ const body =
67
+ typeof event.body === 'string'
68
+ ? JSON.parse(event.body)
69
+ : event.body;
70
+
71
+ // Background functions receive a single message per invocation
72
+ return [body];
73
+ }
74
+ }
75
+
76
+ module.exports = { NetlifyBackgroundProvider };
@@ -0,0 +1,122 @@
1
+ /**
2
+ * QStash Queue Provider (Adapter)
3
+ *
4
+ * Upstash QStash implementation - platform-agnostic HTTP-based message queue.
5
+ * Works on any platform (Netlify, Vercel, AWS, bare metal, etc.)
6
+ *
7
+ * Features:
8
+ * - Configurable retry with exponential backoff
9
+ * - Dead-letter queue via callbacks
10
+ * - At-least-once delivery
11
+ * - Scheduling support (delay, cron)
12
+ * - Message deduplication
13
+ *
14
+ * Requires:
15
+ * - QSTASH_TOKEN env var (from Upstash console)
16
+ * - QSTASH_CURRENT_SIGNING_KEY and QSTASH_NEXT_SIGNING_KEY for verification
17
+ *
18
+ * Queue ID format: The destination URL that QStash will POST to when delivering the message.
19
+ */
20
+ const { QueueProvider } = require('../queue-provider');
21
+
22
+ const QSTASH_API_BASE = 'https://qstash.upstash.io/v2';
23
+
24
+ class QStashQueueProvider extends QueueProvider {
25
+ constructor(options = {}) {
26
+ super();
27
+ this.token =
28
+ options.token || process.env.QSTASH_TOKEN;
29
+ this.retries =
30
+ options.retries !== undefined ? options.retries : 3;
31
+ }
32
+
33
+ /**
34
+ * Send a message via QStash.
35
+ *
36
+ * @param {Object} message - Message payload
37
+ * @param {string} queueId - Destination URL that QStash will POST to
38
+ */
39
+ async send(message, queueId) {
40
+ if (!this.token) {
41
+ throw new Error(
42
+ 'QSTASH_TOKEN is required. Get one at https://console.upstash.com'
43
+ );
44
+ }
45
+
46
+ const headers = {
47
+ Authorization: `Bearer ${this.token}`,
48
+ 'Content-Type': 'application/json',
49
+ 'Upstash-Retries': String(this.retries),
50
+ };
51
+
52
+ const response = await fetch(
53
+ `${QSTASH_API_BASE}/publish/${queueId}`,
54
+ {
55
+ method: 'POST',
56
+ headers,
57
+ body: JSON.stringify(message),
58
+ }
59
+ );
60
+
61
+ if (!response.ok) {
62
+ const errorBody = await response.text().catch(() => 'unknown');
63
+ throw new Error(
64
+ `QStash publish failed: ${response.status} ${response.statusText} - ${errorBody}`
65
+ );
66
+ }
67
+
68
+ return response.json();
69
+ }
70
+
71
+ /**
72
+ * Send batch of messages via QStash batch API.
73
+ */
74
+ async batchSend(entries = [], queueId) {
75
+ if (!this.token) {
76
+ throw new Error(
77
+ 'QSTASH_TOKEN is required. Get one at https://console.upstash.com'
78
+ );
79
+ }
80
+
81
+ const messages = entries.map((entry) => ({
82
+ destination: queueId,
83
+ body: JSON.stringify(entry),
84
+ headers: { 'Content-Type': 'application/json' },
85
+ retries: this.retries,
86
+ }));
87
+
88
+ const response = await fetch(`${QSTASH_API_BASE}/batch`, {
89
+ method: 'POST',
90
+ headers: {
91
+ Authorization: `Bearer ${this.token}`,
92
+ 'Content-Type': 'application/json',
93
+ },
94
+ body: JSON.stringify(messages),
95
+ });
96
+
97
+ if (!response.ok) {
98
+ const errorBody = await response.text().catch(() => 'unknown');
99
+ throw new Error(
100
+ `QStash batch publish failed: ${response.status} - ${errorBody}`
101
+ );
102
+ }
103
+
104
+ return response.json();
105
+ }
106
+
107
+ /**
108
+ * Parse QStash webhook delivery event.
109
+ * QStash POSTs the message body directly to the destination URL.
110
+ */
111
+ parseEvent(event) {
112
+ const body =
113
+ typeof event.body === 'string'
114
+ ? JSON.parse(event.body)
115
+ : event.body;
116
+
117
+ // QStash delivers a single message per invocation
118
+ return [body];
119
+ }
120
+ }
121
+
122
+ module.exports = { QStashQueueProvider };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * SQS Queue Provider (Adapter)
3
+ *
4
+ * AWS SQS implementation of the QueueProvider interface.
5
+ * Extracted from the original queuer-util.js to support multi-provider architecture.
6
+ */
7
+ const { v4: uuid } = require('uuid');
8
+ const {
9
+ SQSClient,
10
+ SendMessageCommand,
11
+ SendMessageBatchCommand,
12
+ } = require('@aws-sdk/client-sqs');
13
+ const { QueueProvider } = require('../queue-provider');
14
+
15
+ const awsConfigOptions = () => {
16
+ const config = {};
17
+ if (process.env.IS_OFFLINE) {
18
+ config.credentials = {
19
+ accessKeyId: 'test-aws-key',
20
+ secretAccessKey: 'test-aws-secret',
21
+ };
22
+ config.region = 'us-east-1';
23
+ }
24
+ if (process.env.AWS_ENDPOINT) {
25
+ config.endpoint = process.env.AWS_ENDPOINT;
26
+ }
27
+ return config;
28
+ };
29
+
30
+ class SqsQueueProvider extends QueueProvider {
31
+ constructor() {
32
+ super();
33
+ this.sqs = new SQSClient(awsConfigOptions());
34
+ }
35
+
36
+ async send(message, queueUrl) {
37
+ const command = new SendMessageCommand({
38
+ MessageBody: JSON.stringify(message),
39
+ QueueUrl: queueUrl,
40
+ });
41
+ return this.sqs.send(command);
42
+ }
43
+
44
+ async batchSend(entries = [], queueUrl) {
45
+ const buffer = [];
46
+ const batchSize = 10;
47
+
48
+ for (const entry of entries) {
49
+ buffer.push({
50
+ Id: uuid(),
51
+ MessageBody: JSON.stringify(entry),
52
+ });
53
+
54
+ if (buffer.length === batchSize) {
55
+ const command = new SendMessageBatchCommand({
56
+ Entries: buffer,
57
+ QueueUrl: queueUrl,
58
+ });
59
+ await this.sqs.send(command);
60
+ buffer.splice(0, buffer.length);
61
+ }
62
+ }
63
+
64
+ if (buffer.length > 0) {
65
+ const command = new SendMessageBatchCommand({
66
+ Entries: buffer,
67
+ QueueUrl: queueUrl,
68
+ });
69
+ return this.sqs.send(command);
70
+ }
71
+
72
+ return {};
73
+ }
74
+
75
+ /**
76
+ * Parse SQS event format: event.Records[].body (JSON string)
77
+ */
78
+ parseEvent(event) {
79
+ const records = event?.Records || [];
80
+ return records.map((record) => JSON.parse(record.body));
81
+ }
82
+ }
83
+
84
+ module.exports = { SqsQueueProvider };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Queue Provider Factory
3
+ *
4
+ * Creates queue provider instances based on appDefinition or environment configuration.
5
+ *
6
+ * Priority order for determining provider:
7
+ * 1. Explicit `provider` option passed to factory
8
+ * 2. `appDefinition.queue.provider` property
9
+ * 3. `QUEUE_PROVIDER` environment variable
10
+ * 4. Default: 'sqs' (backward-compatible with existing AWS deployments)
11
+ *
12
+ * Supported providers:
13
+ * - 'sqs' → AWS SQS (default, existing behavior)
14
+ * - 'netlify-background' → Netlify Background Functions
15
+ * - 'qstash' → Upstash QStash (platform-agnostic)
16
+ */
17
+ const { SqsQueueProvider } = require('./providers/sqs-queue-provider');
18
+ const {
19
+ NetlifyBackgroundProvider,
20
+ } = require('./providers/netlify-background-provider');
21
+ const { QStashQueueProvider } = require('./providers/qstash-queue-provider');
22
+
23
+ const QUEUE_PROVIDERS = {
24
+ SQS: 'sqs',
25
+ NETLIFY_BACKGROUND: 'netlify-background',
26
+ QSTASH: 'qstash',
27
+ };
28
+
29
+ /**
30
+ * Determine the queue provider based on config hierarchy
31
+ *
32
+ * @param {Object} [options]
33
+ * @param {string} [options.provider] - Explicit provider override
34
+ * @param {Object} [options.appDefinition] - Frigg app definition object
35
+ * @returns {string} Provider name
36
+ */
37
+ function determineProvider(options = {}) {
38
+ if (options.provider) {
39
+ return options.provider;
40
+ }
41
+
42
+ if (options.appDefinition?.queue?.provider) {
43
+ return options.appDefinition.queue.provider;
44
+ }
45
+
46
+ if (process.env.QUEUE_PROVIDER) {
47
+ return process.env.QUEUE_PROVIDER;
48
+ }
49
+
50
+ return QUEUE_PROVIDERS.SQS;
51
+ }
52
+
53
+ /**
54
+ * Create a queue provider instance
55
+ *
56
+ * @param {Object} [options]
57
+ * @param {string} [options.provider] - Queue provider name
58
+ * @param {Object} [options.appDefinition] - Frigg app definition (reads queue.provider)
59
+ * @param {Object} [options.providerOptions] - Options passed to the provider constructor
60
+ * @returns {QueueProvider} Implementation of queue provider interface
61
+ */
62
+ function createQueueProvider(options = {}) {
63
+ const provider = determineProvider(options);
64
+ const providerOptions = options.providerOptions || {};
65
+
66
+ switch (provider) {
67
+ case QUEUE_PROVIDERS.SQS:
68
+ return new SqsQueueProvider(providerOptions);
69
+ case QUEUE_PROVIDERS.NETLIFY_BACKGROUND:
70
+ return new NetlifyBackgroundProvider(providerOptions);
71
+ case QUEUE_PROVIDERS.QSTASH:
72
+ return new QStashQueueProvider(providerOptions);
73
+ default:
74
+ throw new Error(
75
+ `Unknown queue provider: '${provider}'. Supported: ${Object.values(QUEUE_PROVIDERS).join(', ')}`
76
+ );
77
+ }
78
+ }
79
+
80
+ module.exports = {
81
+ createQueueProvider,
82
+ determineProvider,
83
+ QUEUE_PROVIDERS,
84
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Queue Provider Interface (Port)
3
+ *
4
+ * Defines the contract for queue message sending.
5
+ * All queue adapters must extend this interface.
6
+ *
7
+ * Following Frigg's hexagonal architecture pattern:
8
+ * - Port defines WHAT the service does (contract)
9
+ * - Adapters implement HOW (AWS SQS, Netlify Background Functions, QStash, etc.)
10
+ */
11
+ class QueueProvider {
12
+ /**
13
+ * Send a single message to a queue
14
+ *
15
+ * @param {Object} message - Message payload (will be JSON-serialized)
16
+ * @param {string} queueId - Queue identifier (URL, name, or endpoint depending on provider)
17
+ * @returns {Promise<Object>} Provider-specific response
18
+ */
19
+ async send(message, queueId) {
20
+ throw new Error('Method send must be implemented by subclass');
21
+ }
22
+
23
+ /**
24
+ * Send a batch of messages to a queue
25
+ *
26
+ * @param {Object[]} entries - Array of message payloads
27
+ * @param {string} queueId - Queue identifier
28
+ * @returns {Promise<Object>} Provider-specific response
29
+ */
30
+ async batchSend(entries, queueId) {
31
+ throw new Error('Method batchSend must be implemented by subclass');
32
+ }
33
+
34
+ /**
35
+ * Parse an incoming event into an array of message payloads.
36
+ * Each provider has its own event shape (SQS Records, HTTP body, etc.)
37
+ *
38
+ * @param {Object} event - Raw event from the runtime (Lambda event, HTTP request, etc.)
39
+ * @returns {Object[]} Array of parsed message payloads
40
+ */
41
+ parseEvent(event) {
42
+ throw new Error('Method parseEvent must be implemented by subclass');
43
+ }
44
+ }
45
+
46
+ module.exports = { QueueProvider };