diff --git a/services/connector/src/shims/abort-controller.spec.ts b/services/connector/src/shims/abort-controller.spec.ts new file mode 100644 index 000000000..e23229a9d --- /dev/null +++ b/services/connector/src/shims/abort-controller.spec.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest' +import { + AbortController, + AbortSignal, + default as DefaultExport, +} from './abort-controller.js' + +describe('abort-controller shim', () => { + it('re-exports the native global AbortController', () => { + expect(AbortController).toBe(globalThis.AbortController) + expect(DefaultExport).toBe(globalThis.AbortController) + }) + + it('re-exports the native global AbortSignal', () => { + expect(AbortSignal).toBe(globalThis.AbortSignal) + }) + + it('produces a signal whose prototype name is AbortSignal', () => { + const signal = new AbortController().signal + const proto = Object.getPrototypeOf(signal) + expect(proto?.constructor?.name).toBe('AbortSignal') + }) +}) diff --git a/services/connector/src/shims/abort-controller.ts b/services/connector/src/shims/abort-controller.ts new file mode 100644 index 000000000..4ebaf1d36 --- /dev/null +++ b/services/connector/src/shims/abort-controller.ts @@ -0,0 +1,19 @@ +/** + * Runtime shim replacing the bundled `abort-controller` polyfill with Node's + * native global implementations. + * + * grammY's Node shim imports `abort-controller` because it supports old Node + * runtimes. Our connector bundle externalizes nothing, so esbuild would inline + * that polyfill. esbuild renames its `AbortSignal` class to avoid a collision + * with the native global, and node-fetch v2 (v2.7.0) gates every request on + * the signal prototype's constructor name being exactly "AbortSignal". The + * renamed class no longer matches, so every grammY HTTP call fails with + * "Expected signal to be an instanceof AbortSignal". + * + * Rather than ship a legacy polyfill (the runtime is Node >= 22), alias + * `abort-controller` to this module so grammY hands node-fetch the native + * `AbortSignal`, whose constructor keeps its real name. + */ +export const AbortController = globalThis.AbortController +export const AbortSignal = globalThis.AbortSignal +export default globalThis.AbortController diff --git a/services/connector/tsup.config.ts b/services/connector/tsup.config.ts index e9640b8e5..40188eeb7 100644 --- a/services/connector/tsup.config.ts +++ b/services/connector/tsup.config.ts @@ -1,3 +1,4 @@ +import { resolve } from 'node:path' import { defineConfig } from 'tsup' export default defineConfig({ @@ -15,5 +16,14 @@ export default defineConfig({ outExtension: () => ({ js: '.cjs' }), esbuildOptions: (options) => { options.conditions = ['openalice-source', ...(options.conditions ?? [])] + // grammY's Node shim imports the legacy `abort-controller` polyfill. + // Bundling it makes esbuild rename its AbortSignal class, which breaks + // node-fetch@2.7.0's `constructor.name === "AbortSignal"` check and fails + // every grammY call. Point it at Node's native global instead (runtime is + // Node >= 22). See src/shims/abort-controller.ts. + options.alias = { + ...(options.alias ?? {}), + 'abort-controller': resolve(import.meta.dirname, 'src/shims/abort-controller.ts'), + } }, })