From 9b1c0df8a5fc1d747fd6b22061422ea3d6dcee65 Mon Sep 17 00:00:00 2001 From: Joshua Johnson Date: Tue, 12 May 2026 14:27:47 -0500 Subject: [PATCH] fix: load harper lazily in CacheHandler to avoid worker conflicts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Next.js cacheHandler module is loaded by Next.js itself (via `require()` on the `cacheHandler` config path), not by Harper. With turbopack, that load happens inside a build worker thread. Importing `harper` at the top of CacheHandler.cts ran harper's module initialization in that worker thread, which tried to register native worker hooks process-wide — conflicting with the same registration already done by the Harper main process. The result was a stream of "Worker creator already registered" uncaught exceptions; the HTTP worker kept restarting until Harper gave up (`Thread has been restarted undefined times and will not be restarted`). Same load path also fires with webpack — Next.js's build init requires the cacheHandler path directly via Node's `require()`, which bypasses webpack `externals` and `serverExternalPackages`. With `harper` not present in the customer's node_modules, the build fails with MODULE_NOT_FOUND on `node_modules/@harperfast/nextjs/node_modules/harper`. Switch to the same pattern `plugin.ts` uses: import `harper` for types only, and read `databases` from `globalThis` at call time. The module now loads cleanly in any context, and methods short-circuit (return null / no-op) if `databases` isn't available — which is the correct fallback when there is no Harper runtime to talk to. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/CacheHandler.cts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/CacheHandler.cts b/src/CacheHandler.cts index 39db41a..ab9565d 100644 --- a/src/CacheHandler.cts +++ b/src/CacheHandler.cts @@ -8,7 +8,20 @@ import type { SetIncrementalResponseCacheContext, } from 'next/dist/server/response-cache/index.d.ts'; -import { databases } from 'harper'; +// Type-only import: erased at compile time so `harper` is never resolved when this +// module is loaded from a non-Harper context (e.g. Next.js's build worker, which +// requires the cacheHandler path directly via Node's `require`, bypassing webpack +// externals and serverExternalPackages). +import type { databases as DatabasesType } from 'harper'; + +// `databases` is a Harper-provided global registered when the plugin is loaded +// in-process by Harper. Resolve it lazily so loading this file in a build worker +// does not pull in the harper runtime — which would otherwise try to register +// native worker hooks a second time and crash with "Worker creator already +// registered", restarting the HTTP worker until Harper gives up. +function getDatabases(): typeof DatabasesType | undefined { + return (globalThis as { databases?: typeof DatabasesType }).databases; +} export default class HarperCacheHandler implements CacheHandler { constructor() {} @@ -17,6 +30,9 @@ export default class HarperCacheHandler implements CacheHandler { key: string, _ctx: GetIncrementalFetchCacheContext | GetIncrementalResponseCacheContext ): Promise { + const databases = getDatabases(); + if (!databases) return null; + const table = databases.harperfast_nextjs.nextjs_isr_cache; const record = await table.get(key); if (!record) return null; @@ -36,6 +52,9 @@ export default class HarperCacheHandler implements CacheHandler { data: IncrementalCacheValue | null, _ctx: SetIncrementalFetchCacheContext | SetIncrementalResponseCacheContext ): Promise { + const databases = getDatabases(); + if (!databases) return; + const table = databases.harperfast_nextjs.nextjs_isr_cache; await table.put(key, { data,