@friggframework/core 2.0.0--canary.522.893db5d.0 → 2.0.0--canary.545.35013d9.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.
@@ -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 };