From bdc32bd41d30e9cbf4a30b28b349f05d7e585fcf Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Tue, 9 Jul 2024 09:16:39 +0100 Subject: [PATCH] fix(internal): Use TypeScript's built-in file watcher for SDL loader --- .changeset/silent-eyes-boil.md | 5 +++ packages/internal/src/loaders/sdl.ts | 55 +++++++++++++++++++--------- 2 files changed, 43 insertions(+), 17 deletions(-) create mode 100644 .changeset/silent-eyes-boil.md diff --git a/.changeset/silent-eyes-boil.md b/.changeset/silent-eyes-boil.md new file mode 100644 index 00000000..55d75925 --- /dev/null +++ b/.changeset/silent-eyes-boil.md @@ -0,0 +1,5 @@ +--- +"@gql.tada/internal": patch +--- + +Fix SDL-file schema watcher missing changed file events on macOS under certain write conditions. diff --git a/packages/internal/src/loaders/sdl.ts b/packages/internal/src/loaders/sdl.ts index 5ed2db13..f7bc4c8b 100644 --- a/packages/internal/src/loaders/sdl.ts +++ b/packages/internal/src/loaders/sdl.ts @@ -1,3 +1,4 @@ +import ts from 'typescript'; import type { IntrospectionQuery } from 'graphql'; import { buildSchema, buildClientSchema, executeSync } from 'graphql'; import { CombinedError } from '@urql/core'; @@ -17,7 +18,7 @@ interface LoadFromSDLConfig { export function loadFromSDL(config: LoadFromSDLConfig): SchemaLoader { const subscriptions = new Set(); - let controller: AbortController | null = null; + let abort: (() => void) | null = null; let result: SchemaLoaderResult | null = null; const load = async (): Promise => { @@ -60,21 +61,43 @@ export function loadFromSDL(config: LoadFromSDLConfig): SchemaLoader { }; const watch = async () => { - controller = new AbortController(); - const watcher = fs.watch(config.file, { - signal: controller.signal, - persistent: false, - }); - try { - for await (const _event of watcher) { - if ((result = await load())) { - for (const subscriber of subscriptions) subscriber(result); + if (ts.sys.watchFile) { + const watcher = ts.sys.watchFile( + config.file, + async () => { + try { + if ((result = await load())) { + for (const subscriber of subscriptions) subscriber(result); + } + } catch (_error) {} + }, + 250, + { + // NOTE: Using `ts.WatchFileKind.UseFsEvents` causes missed events just like fs.watch + // as below on macOS, as of TypeScript 5.5 and is hence avoided here + watchFile: ts.WatchFileKind.UseFsEventsOnParentDirectory, + fallbackPolling: ts.PollingWatchKind.PriorityInterval, + } + ); + abort = () => watcher.close(); + } else { + const controller = new AbortController(); + abort = () => controller.abort(); + const watcher = fs.watch(config.file, { + signal: controller.signal, + persistent: false, + }); + try { + for await (const _event of watcher) { + if ((result = await load())) { + for (const subscriber of subscriptions) subscriber(result); + } } + } catch (error: any) { + if (error.name !== 'AbortError') throw error; + } finally { + abort = null; } - } catch (error: any) { - if (error.name !== 'AbortError') throw error; - } finally { - controller = null; } }; @@ -90,9 +113,7 @@ export function loadFromSDL(config: LoadFromSDLConfig): SchemaLoader { subscriptions.add(onUpdate); return () => { subscriptions.delete(onUpdate); - if (!subscriptions.size && controller) { - controller.abort(); - } + if (!subscriptions.size && abort) abort(); }; }, async loadIntrospection() {