@@ -201,7 +201,7 @@ export class Airtable extends Connector<Airtable> {
201201 if ( webhookId ) {
202202 try {
203203 const api = await this . getAPI ( baseId ) ;
204- await api . deleteWebhook ( baseId , webhookId ) ;
204+ if ( api ) await api . deleteWebhook ( baseId , webhookId ) ;
205205 } catch ( error ) {
206206 console . warn ( "Failed to delete Airtable webhook:" , error ) ;
207207 }
@@ -219,7 +219,9 @@ export class Airtable extends Connector<Airtable> {
219219 // ---- Detection + Backfill ----
220220
221221 async detectAndSync ( baseId : string , initialSync : boolean ) : Promise < void > {
222+ if ( ! ( await this . get < boolean > ( `sync_enabled_${ baseId } ` ) ) ) return ;
222223 const api = await this . getAPI ( baseId ) ;
224+ if ( ! api ) return ;
223225 const me = await api . whoami ( ) ;
224226 await this . set < ViewerInfo > ( `viewer_${ baseId } ` , {
225227 id : me . id ,
@@ -328,6 +330,8 @@ export class Airtable extends Connector<Airtable> {
328330 }
329331
330332 async syncBatch ( baseId : string ) : Promise < void > {
333+ if ( ! ( await this . get < boolean > ( `sync_enabled_${ baseId } ` ) ) ) return ;
334+
331335 const state = await this . get < SyncState > ( `sync_state_${ baseId } ` ) ;
332336 if ( ! state ) return ;
333337
@@ -345,6 +349,7 @@ export class Airtable extends Connector<Airtable> {
345349
346350 const table = detected [ state . tableIndex ] ;
347351 const api = await this . getAPI ( baseId ) ;
352+ if ( ! api ) return ;
348353 const page = await api . listRecords ( baseId , table . tableId , {
349354 offset : state . offset ,
350355 pageSize : 50 ,
@@ -567,6 +572,7 @@ export class Airtable extends Connector<Airtable> {
567572 // ---- Webhooks ----
568573
569574 async setupWebhook ( baseId : string ) : Promise < void > {
575+ if ( ! ( await this . get < boolean > ( `sync_enabled_${ baseId } ` ) ) ) return ;
570576 try {
571577 const webhookUrl = await this . tools . network . createWebhook (
572578 { } ,
@@ -586,6 +592,7 @@ export class Airtable extends Connector<Airtable> {
586592 const tableIds = detected . map ( ( d ) => d . tableId ) ;
587593
588594 const api = await this . getAPI ( baseId ) ;
595+ if ( ! api ) return ;
589596 // Airtable allows one notification scope per webhook; register one
590597 // webhook per base covering all detected tables. The
591598 // recordChangeScope filter only accepts a single table, so when we
@@ -665,6 +672,7 @@ export class Airtable extends Connector<Airtable> {
665672 if ( ! viewer || detected . length === 0 ) return ;
666673
667674 const api = await this . getAPI ( baseId ) ;
675+ if ( ! api ) return ;
668676 let cursor = ( await this . get < number > ( `webhook_cursor_${ baseId } ` ) ) ?? 1 ;
669677 const tableMap = new Map ( detected . map ( ( d ) => [ d . tableId , d ] as const ) ) ;
670678
@@ -734,14 +742,16 @@ export class Airtable extends Connector<Airtable> {
734742 // ---- Reconciliation (catches comment edits/deletes webhooks miss) ----
735743
736744 async reconcileComments ( baseId : string ) : Promise < void > {
737- const enabled = await this . get < boolean > ( `sync_enabled_${ baseId } ` ) ;
738- if ( ! enabled ) return ;
745+ // Stop the self-scheduling loop once the channel is disabled.
746+ // Returning without rescheduling lets the recurring task die out,
747+ // rather than spamming retries after teardown.
748+ if ( ! ( await this . get < boolean > ( `sync_enabled_${ baseId } ` ) ) ) return ;
739749
740- try {
750+ const api = await this . getAPI ( baseId ) ;
751+ if ( api ) {
741752 const tracked = ( await this . get < TrackedRecord [ ] > ( `tracked_records_${ baseId } ` ) ) ?? [ ] ;
742753 const detected = ( await this . get < DetectedTable [ ] > ( `task_tables_${ baseId } ` ) ) ?? [ ] ;
743754 const tableMap = new Map ( detected . map ( ( d ) => [ d . tableId , d ] as const ) ) ;
744- const api = await this . getAPI ( baseId ) ;
745755
746756 for ( const rec of tracked ) {
747757 const table = tableMap . get ( rec . tableId ) ;
@@ -755,12 +765,15 @@ export class Airtable extends Connector<Airtable> {
755765 console . warn ( "Reconcile failed for record" , rec . recordId , error ) ;
756766 }
757767 }
758- } finally {
759- const cb = await this . callback ( this . reconcileComments , baseId ) ;
760- await this . runTask ( cb , {
761- runAt : new Date ( Date . now ( ) + RECONCILE_INTERVAL_MS ) ,
762- } ) ;
763768 }
769+
770+ // Reschedule only while the channel is still enabled. Re-check after
771+ // the reconcile pass in case onChannelDisabled fired during the run.
772+ if ( ! ( await this . get < boolean > ( `sync_enabled_${ baseId } ` ) ) ) return ;
773+ const cb = await this . callback ( this . reconcileComments , baseId ) ;
774+ await this . runTask ( cb , {
775+ runAt : new Date ( Date . now ( ) + RECONCILE_INTERVAL_MS ) ,
776+ } ) ;
764777 }
765778
766779 // ---- Write-backs ----
@@ -792,6 +805,7 @@ export class Airtable extends Connector<Airtable> {
792805 }
793806
794807 const api = await this . getAPI ( baseId ) ;
808+ if ( ! api ) return ;
795809 try {
796810 await api . patchRecord ( baseId , tableId , recordId , update ) ;
797811 } catch ( error ) {
@@ -810,6 +824,7 @@ export class Airtable extends Connector<Airtable> {
810824 if ( text . length === 0 ) return ;
811825
812826 const api = await this . getAPI ( baseId ) ;
827+ if ( ! api ) return ;
813828 try {
814829 const comment = await api . createComment ( baseId , tableId , recordId , {
815830 text : this . translateMentionsOutbound ( text ) ,
@@ -834,9 +849,15 @@ export class Airtable extends Connector<Airtable> {
834849
835850 // ---- Helpers ----
836851
837- private async getAPI ( baseId : string ) : Promise < AirtableAPI > {
852+ /**
853+ * Returns an authenticated Airtable API client, or null when the channel
854+ * has no usable token (disconnected, revoked, or not yet refreshed).
855+ * Callers must bail silently on null rather than throwing, so scheduled
856+ * tasks don't retry-spam after the user disables the channel.
857+ */
858+ private async getAPI ( baseId : string ) : Promise < AirtableAPI | null > {
838859 const token = await this . tools . integrations . get ( baseId ) ;
839- if ( ! token ) throw new Error ( "No Airtable authentication token available" ) ;
860+ if ( ! token ) return null ;
840861 return new AirtableAPI ( token . token ) ;
841862 }
842863}
0 commit comments