Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.8k
v0.5.111: non-polling webhook execs off trigger.dev, gmail subject headers, webhook trigger configs#3530
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
v0.5.111: non-polling webhook execs off trigger.dev, gmail subject headers, webhook trigger configs #3530
Changes from all commits
37d524bd5502d668d207dFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -22,7 +22,7 @@ export class TriggerBlockHandler implements BlockHandler { | ||
| } | ||
| const existingState = ctx.blockStates.get(block.id) | ||
| if (existingState?.output && Object.keys(existingState.output).length > 0) { | ||
| if (existingState?.output) { | ||
| return existingState.output | ||
| } | ||
icecrasher321 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| /** | ||
| * @vitest-environment node | ||
| */ | ||
| import { describe, expect, it } from 'vitest' | ||
| import { encodeRfc2047 } from './utils' | ||
| describe('encodeRfc2047', () => { | ||
| it('returns ASCII text unchanged', () => { | ||
| expect(encodeRfc2047('Simple ASCII Subject')).toBe('Simple ASCII Subject') | ||
| }) | ||
| it('returns empty string unchanged', () => { | ||
| expect(encodeRfc2047('')).toBe('') | ||
| }) | ||
| it('encodes emojis as RFC 2047 base64', () => { | ||
| const result = encodeRfc2047('Time to Stretch! 🧘') | ||
| expect(result).toBe('=?UTF-8?B?VGltZSB0byBTdHJldGNoISDwn6eY?=') | ||
| }) | ||
| it('round-trips non-ASCII subjects correctly', () => { | ||
| const subjects = ['Hello 世界', 'Café résumé', '🎉🎊🎈 Party!', '今週のミーティング'] | ||
| for (const subject of subjects) { | ||
| const encoded = encodeRfc2047(subject) | ||
| const match = encoded.match(/^=\?UTF-8\?B\?(.+)\?=$/) | ||
| expect(match).not.toBeNull() | ||
| const decoded = Buffer.from(match![1], 'base64').toString('utf-8') | ||
| expect(decoded).toBe(subject) | ||
| } | ||
| }) | ||
| it('does not double-encode already-encoded subjects', () => { | ||
| const alreadyEncoded = '=?UTF-8?B?VGltZSB0byBTdHJldGNoISDwn6eY?=' | ||
| expect(encodeRfc2047(alreadyEncoded)).toBe(alreadyEncoded) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -294,6 +294,19 @@ function generateBoundary(): string { | ||
| return `----=_Part_${Date.now()}_${Math.random().toString(36).substring(2, 15)}` | ||
| } | ||
| /** | ||
| * Encode a header value using RFC 2047 Base64 encoding if it contains non-ASCII characters. | ||
| * This matches Google's own Gmail API sample: `=?utf-8?B?${Buffer.from(subject).toString('base64')}?=` | ||
| * @see https://github.com/googleapis/google-api-nodejs-client/blob/main/samples/gmail/send.js | ||
| */ | ||
| export function encodeRfc2047(value: string): string { | ||
| // eslint-disable-next-line no-control-regex | ||
| if (/^[\x00-\x7F]*$/.test(value)) { | ||
| return value | ||
| } | ||
| return `=?UTF-8?B?${Buffer.from(value, 'utf-8').toString('base64')}?=` | ||
| } | ||
icecrasher321 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| /** | ||
| * Encode string or buffer to base64url format (URL-safe base64) | ||
| * Gmail API requires base64url encoding for the raw message field | ||
| @@ -333,7 +346,7 @@ export function buildSimpleEmailMessage(params: { | ||
| emailHeaders.push(`Bcc: ${bcc}`) | ||
| } | ||
| emailHeaders.push(`Subject: ${subject || ''}`) | ||
| emailHeaders.push(`Subject: ${encodeRfc2047(subject || '')}`) | ||
| if (inReplyTo) { | ||
| emailHeaders.push(`In-Reply-To: ${inReplyTo}`) | ||
| @@ -380,7 +393,7 @@ export function buildMimeMessage(params: BuildMimeMessageParams): string { | ||
| if (bcc) { | ||
| messageParts.push(`Bcc: ${bcc}`) | ||
| } | ||
| messageParts.push(`Subject: ${subject || ''}`) | ||
| messageParts.push(`Subject: ${encodeRfc2047(subject || '')}`) | ||
| if (inReplyTo) { | ||
| messageParts.push(`In-Reply-To: ${inReplyTo}`) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| /** | ||
| * @vitest-environment node | ||
| */ | ||
| import { describe, expect, it } from 'vitest' | ||
| import { POLLING_PROVIDERS } from '@/triggers/constants' | ||
| import { TRIGGER_REGISTRY } from '@/triggers/registry' | ||
| describe('POLLING_PROVIDERS sync with TriggerConfig.polling', () => { | ||
| it('matches every trigger with polling: true in the registry', () => { | ||
| const registryPollingProviders = new Set( | ||
| Object.values(TRIGGER_REGISTRY) | ||
| .filter((t) => t.polling === true) | ||
| .map((t) => t.provider) | ||
| ) | ||
| expect(POLLING_PROVIDERS).toEqual(registryPollingProviders) | ||
| }) | ||
| it('no trigger with polling: true is missing from POLLING_PROVIDERS', () => { | ||
| const missing: string[] = [] | ||
| for (const trigger of Object.values(TRIGGER_REGISTRY)) { | ||
| if (trigger.polling && !POLLING_PROVIDERS.has(trigger.provider)) { | ||
| missing.push(`${trigger.id} (provider: ${trigger.provider})`) | ||
| } | ||
| } | ||
| expect(missing, `Triggers with polling: true missing from POLLING_PROVIDERS`).toEqual([]) | ||
| }) | ||
| it('no POLLING_PROVIDERS entry lacks a polling: true trigger in the registry', () => { | ||
| const extra: string[] = [] | ||
| for (const provider of POLLING_PROVIDERS) { | ||
| const hasTrigger = Object.values(TRIGGER_REGISTRY).some( | ||
| (t) => t.provider === provider && t.polling === true | ||
| ) | ||
| if (!hasTrigger) { | ||
| extra.push(provider) | ||
| } | ||
| } | ||
| expect(extra, `POLLING_PROVIDERS entries with no matching polling trigger`).toEqual([]) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -35,3 +35,15 @@ export const TRIGGER_RUNTIME_SUBBLOCK_IDS: string[] = [ | ||
| * This prevents runaway errors from continuously executing failing workflows. | ||
| */ | ||
| export const MAX_CONSECUTIVE_FAILURES = 100 | ||
| /** | ||
| * Set of webhook provider names that use polling-based triggers. | ||
| * Mirrors the `polling: true` flag on TriggerConfig entries. | ||
| * Used to route execution: polling providers use the full job queue | ||
| * (Trigger.dev), non-polling providers execute inline. | ||
| */ | ||
| export const POLLING_PROVIDERS = new Set(['gmail', 'outlook', 'rss', 'imap']) | ||
| export function isPollingWebhookProvider(provider: string): boolean { | ||
| return POLLING_PROVIDERS.has(provider) | ||
| } | ||
icecrasher321 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.