-
Notifications
You must be signed in to change notification settings - Fork 15
feat: provider plugin architecture + Netlify adapter #545
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
Open
seanspeaks
wants to merge
69
commits into
feature/integration-router-v2-drop-modules-router
Choose a base branch
from
claude/frigg-netlify-exploration-aY2Bh
base: feature/integration-router-v2-drop-modules-router
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 12 commits
Commits
Show all changes
69 commits
Select commit
Hold shift + click to select a range
188110e
feat: add Netlify adapter and queue provider abstraction for multi-pl…
claude 9513a42
feat: complete Netlify adapter with split functions, scheduler, DB su…
claude 992ea0c
docs: add provider plugin architecture plan
claude 2427eba
docs: add Vercel, GCP, and Docker/local provider specs to plan
claude 7dc47e9
docs: add deploy story, difficulty assessment, and tier 3 analysis
claude 35013d9
feat: complete Netlify provider to full plugin interface
claude 9c6a13d
refactor: rename @friggframework/netlify-adapter to @friggframework/p…
claude 0430c59
fix: address PR review feedback on deploy.js and scheduled-sync.js
claude 72e5463
fix: use netlify-background queue provider in worker-background.js
claude 88cf983
feat: add provider resolver to bridge appDefinition.provider to provi…
claude c870571
feat: wire CLI commands to use appDefinition.provider for provider di…
claude 7b93757
test: add provider dispatch tests for CLI commands
claude a2a8f01
fix(scheduler): prevent duplicate dispatch via PROCESSING state guard
claude 97831cd
docs: add ADR for multi-provider support architecture
claude 1176a00
fix(scheduler): lazy-load adapters to avoid eager AWS SDK import
claude 302ab9b
fix: route non-AWS providers through provider build in infrastructure.js
claude 9237e31
refactor: extract AWS adapters to provider-aws, move provider-netlify…
claude 023b098
refactor: introduce hexagonal architecture interfaces for AWS decoupling
claude 5949712
refactor!: remove lazy-loading backward compat, require explicit adap…
claude 3d89eac
fix: address review findings from AWS decoupling branch review
claude 21381ca
fix(provider-netlify): move @friggframework/core to peerDependencies
claude 3de4267
chore: update package-lock.json after provider-netlify dep change
claude 079aa2d
fix(providers): use wildcard peer dep for @friggframework/core
claude 29ef032
fix(core): remove provider-aws from core's peerDependencies
claude a8d08b4
refactor(core): eliminate hardcoded require('@friggframework/provider…
claude 74db90f
refactor(core): remove mongoose dependency, use Prisma and mongodb dr…
d-klotz e34b741
chore(deps): remove lodash.get, upgrade supertest to v7
claude 077502a
fix(provider-netlify): generate lib/ shims alongside function entry p…
claude f95ba19
fix(core): address code review findings from PR #546
d-klotz 3dadab6
fix(provider-netlify): fix ONGOING_SYNC detection and webhook route r…
claude 2eed07a
fix(provider-netlify): add @friggframework packages to esbuild externals
claude 8642b58
refactor(core): remove unused expectShallowEqualDbObject
d-klotz ce82650
refactor(tests): remove password encryption tests
d-klotz 45e06e5
refactor(core): remove unused Entity import
d-klotz c17a357
feat(core): add AWS SDK client-scheduler dependency
d-klotz a987d94
Merge pull request #546 from friggframework/refactor/remove-mongoose-…
d-klotz e7becd9
feat(core, provider-netlify): make app definition statically traceabl…
claude 5ec2975
fix(core): lazy-load MongoDB/DocumentDB implementations in all reposi…
claude 8db2e0d
fix(core): lazy-load Entity (Mongoose model) and DocumentDB implement…
claude f995374
chore: remove stale build artifact
claude 17e3f18
chore: remove stale prisma layer build artifact
claude c459392
chore: remove stale prisma layer build artifact
claude 4f3ba52
refactor(core): replace mongodb ObjectId with bson
d-klotz f9c9ccc
Merge branch 'next' into refactor/remove-mongoose-dependency
d-klotz 67ebb53
refactor(core): remove unused AWS SDK client-scheduler dependency
d-klotz aac4f40
Merge pull request #547 from friggframework/refactor/remove-mongoose-…
d-klotz 0c9482d
fix(infra): self-heal VPC subnet-route table association drift
d-klotz 1a22160
fix(infra): improve self-healing for VPC subnet associations
d-klotz 90eefb0
test(infra): add self-heal tests for VPC subnet associations
d-klotz 1435f81
test(infra): enhance fallback path tests for VPC subnets
d-klotz c8ae0ca
style(tests): format test cases for better readability
d-klotz 9740ac8
Merge pull request #548 from friggframework/fix/vpc-subnet-route-tabl…
d-klotz 9660c3e
merge: pull from next branch and resolve all conflicts
claude d336d2a
fix(core): route backend discovery through loadAppDefinition() for Ne…
claude 2b4f642
chore: sync package-lock.json
claude b75e1e2
fix: regenerate package-lock.json with missing encoding and iconv-lit…
claude cd04c85
fix: regenerate package-lock.json from next branch base
claude dba001a
fix: regenerate package-lock.json with npm 11 for CI compatibility
claude c40eca4
feat(core): lazy router initialization to eliminate require-time side…
claude 60108f0
fix(core,netlify): resolve Prisma client loading on Netlify deployments
claude ae2019f
fix(core,netlify): reduce Netlify function bundle size
claude 676b219
fix(core): remove redundant debian-openssl binaryTarget from Prisma s…
claude 69acce5
feat(core): add extension system for DB-backed OAuth credentials
claude 4f9d12a
feat(schemas,devtools): add extensions to app definition schema and v…
claude ccb5010
feat(extensions): add @friggframework/extension-db-credentials package
claude 6f62ce6
fix: add packages/extensions/* to workspaces and lerna packages
claude 5a3df8f
fix(db-credentials): allow prerelease versions of @friggframework/core
claude e256e95
fix: regenerate package-lock.json to include db-credentials workspace
claude b4ca16d
fix(db-credentials): add publishConfig and repo metadata for npm publish
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
173 changes: 173 additions & 0 deletions
173
packages/core/infrastructure/scheduler/netlify-scheduler-adapter.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| /** | ||
| * Netlify Scheduler Adapter | ||
| * | ||
| * Implements SchedulerServiceInterface for Netlify deployments. | ||
| * | ||
| * Netlify Scheduled Functions use cron syntax but are defined statically in | ||
| * netlify.toml or via the @netlify/functions schedule() helper. They cannot | ||
| * be created dynamically at runtime like EventBridge Scheduler. | ||
| * | ||
| * Strategy for one-time scheduled jobs on Netlify: | ||
| * 1. Store the schedule in the database (same as mock adapter pattern) | ||
| * 2. A Netlify Scheduled Function runs on a cron interval (e.g., every 5 min) | ||
| * 3. The cron function queries for due schedules and dispatches them | ||
| * to a background function for execution | ||
| * | ||
| * This is a "poll-and-dispatch" pattern vs EventBridge's "push" pattern. | ||
| * Trade-off: slightly less precise timing (up to cron interval delay), | ||
| * but works on any platform without cloud-specific scheduling APIs. | ||
| * | ||
| * For integrations that need the queue provider to dispatch jobs: | ||
| * - queueResourceId maps to the background function URL path | ||
| * - Payload is stored and forwarded when the cron fires | ||
| */ | ||
|
|
||
| const { SchedulerServiceInterface } = require('./scheduler-service-interface'); | ||
|
|
||
| class NetlifySchedulerAdapter extends SchedulerServiceInterface { | ||
| /** | ||
| * @param {Object} options | ||
| * @param {Object} options.repository - Repository for persisting schedules | ||
| * Must implement: save(schedule), delete(scheduleName), | ||
| * findByName(scheduleName), findDue(now) | ||
| * @param {Object} [options.queueProvider] - Queue provider for dispatching due jobs | ||
| */ | ||
| constructor(options = {}) { | ||
| super(); | ||
| this.repository = options.repository; | ||
| this.queueProvider = options.queueProvider; | ||
|
|
||
| if (!this.repository) { | ||
| console.warn( | ||
| '[NetlifyScheduler] No repository provided. ' + | ||
| 'Schedules will be stored in memory (lost on function restart). ' + | ||
| 'Provide a database-backed repository for production use.' | ||
| ); | ||
| this._inMemorySchedules = new Map(); | ||
| } | ||
| } | ||
|
|
||
| async scheduleOneTime({ | ||
| scheduleName, | ||
| scheduleAt, | ||
| queueResourceId, | ||
| payload, | ||
| }) { | ||
| if (!scheduleName) throw new Error('scheduleName is required'); | ||
| if (!scheduleAt || !(scheduleAt instanceof Date)) | ||
| throw new Error('scheduleAt must be a valid Date object'); | ||
| if (!queueResourceId) throw new Error('queueResourceId is required'); | ||
|
|
||
| const scheduleData = { | ||
| scheduleName, | ||
| scheduledAt: scheduleAt.toISOString(), | ||
| queueResourceId, | ||
| payload, | ||
| createdAt: new Date().toISOString(), | ||
| state: 'PENDING', | ||
| }; | ||
|
|
||
| if (this.repository) { | ||
| await this.repository.save(scheduleData); | ||
| } else { | ||
| this._inMemorySchedules.set(scheduleName, scheduleData); | ||
| } | ||
|
|
||
| console.log( | ||
| `[NetlifyScheduler] Scheduled: ${scheduleName} for ${scheduleAt.toISOString()}` | ||
| ); | ||
|
|
||
| return { | ||
| scheduledJobId: `netlify-schedule-${scheduleName}`, | ||
| scheduledAt: scheduleAt.toISOString(), | ||
| }; | ||
| } | ||
|
|
||
| async deleteSchedule(scheduleName) { | ||
| if (!scheduleName) throw new Error('scheduleName is required'); | ||
|
|
||
| if (this.repository) { | ||
| await this.repository.delete(scheduleName); | ||
| } else { | ||
| this._inMemorySchedules.delete(scheduleName); | ||
| } | ||
|
|
||
| console.log(`[NetlifyScheduler] Deleted schedule: ${scheduleName}`); | ||
| } | ||
|
|
||
| async getScheduleStatus(scheduleName) { | ||
| if (!scheduleName) throw new Error('scheduleName is required'); | ||
|
|
||
| let schedule; | ||
| if (this.repository) { | ||
| schedule = await this.repository.findByName(scheduleName); | ||
| } else { | ||
| schedule = this._inMemorySchedules.get(scheduleName); | ||
| } | ||
|
|
||
| if (!schedule) { | ||
| return { exists: false }; | ||
| } | ||
|
|
||
| return { | ||
| exists: true, | ||
| scheduledAt: schedule.scheduledAt, | ||
| state: schedule.state, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Process due schedules. Called by the Netlify cron function. | ||
| * | ||
| * Finds all schedules where scheduledAt <= now, dispatches them | ||
| * to the queue provider, and marks them as completed. | ||
| * | ||
| * @returns {Promise<{ processed: number, errors: number }>} | ||
| */ | ||
| async processDueSchedules() { | ||
| if (!this.repository) { | ||
| console.warn( | ||
| '[NetlifyScheduler] Cannot process due schedules without a repository' | ||
| ); | ||
| return { processed: 0, errors: 0 }; | ||
| } | ||
|
|
||
| const now = new Date(); | ||
| const dueSchedules = await this.repository.findDue(now); | ||
|
|
||
| let processed = 0; | ||
| let errors = 0; | ||
|
|
||
| for (const schedule of dueSchedules) { | ||
| try { | ||
| if (this.queueProvider) { | ||
| await this.queueProvider.send( | ||
| schedule.payload, | ||
| schedule.queueResourceId | ||
| ); | ||
| } else { | ||
| console.log( | ||
| `[NetlifyScheduler] No queue provider — logging payload for: ${schedule.scheduleName}`, | ||
| JSON.stringify(schedule.payload) | ||
| ); | ||
| } | ||
|
|
||
| await this.repository.delete(schedule.scheduleName); | ||
| processed++; | ||
| } catch (error) { | ||
| console.error( | ||
| `[NetlifyScheduler] Error processing schedule ${schedule.scheduleName}:`, | ||
| error | ||
| ); | ||
| errors++; | ||
| } | ||
| } | ||
|
|
||
| console.log( | ||
| `[NetlifyScheduler] Processed ${processed} due schedules, ${errors} errors` | ||
| ); | ||
| return { processed, errors }; | ||
| } | ||
| } | ||
|
|
||
| module.exports = { NetlifySchedulerAdapter }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| const { | ||
| resolveProvider, | ||
| determineProviderName, | ||
| providerPackageName, | ||
| KNOWN_PROVIDERS, | ||
| } = require('./resolve-provider'); | ||
|
|
||
| module.exports = { | ||
| resolveProvider, | ||
| determineProviderName, | ||
| providerPackageName, | ||
| KNOWN_PROVIDERS, | ||
| }; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Race condition: If
queueProvider.send()succeeds butrepository.delete()fails (database error, network timeout, etc.), the schedule remains in the database with state='PENDING' and will be re-dispatched on the next cron run, causing duplicate job execution.Fix: Update the schedule state to 'PROCESSING' before dispatching, then delete after success:
Spotted by Graphite

Is this helpful? React 👍 or 👎 to let us know.