diff --git a/package-lock.json b/package-lock.json index 032238a..5449b17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@biomejs/biome": "^1.9.4", "@ianvs/prettier-plugin-sort-imports": "^4.7.0", "@types/node": "^25.3.3", - "poku": "^4.1.0", + "poku": "4.2.0", "prettier": "^3.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3" @@ -28,7 +28,7 @@ "url": "https://github.com/pokujs/shared-resources?sponsor=1" }, "peerDependencies": { - "poku": "canary" + "poku": "^4.1.0" } }, "node_modules/@babel/code-frame": { @@ -227,9 +227,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -247,9 +244,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -267,9 +261,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -287,9 +278,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -982,9 +970,9 @@ "license": "ISC" }, "node_modules/poku": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/poku/-/poku-4.1.0.tgz", - "integrity": "sha512-gHyR0sE1zZ7qDowChiToZjQ75Dwqf0JDA3cHh5hVD8K00HOnVW4nr9XlximThE/AyenlxVatSEuiLfwYFcJS7w==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/poku/-/poku-4.2.0.tgz", + "integrity": "sha512-GygMGFGgEJ9kfs6Z+QPg/ODs9OF3oGHN8+hYIxtBox3pwYISO+Vu660vH1e+YzjpGoaoy2o5y6YwE1tX5yZx3Q==", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index f8a12d0..7608738 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@biomejs/biome": "^1.9.4", "@ianvs/prettier-plugin-sort-imports": "^4.7.0", "@types/node": "^25.3.3", - "poku": "^4.1.0", + "poku": "4.2.0", "prettier": "^3.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3" diff --git a/src/index.ts b/src/index.ts index 2e4c61d..552294b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,8 @@ import type { SharedResourcesConfig } from './types.js'; import { configureCodecs, globalRegistry, + resetSharedResourcesRuntime, + setExecutionMode, setupSharedResourceIPC, } from './shared-resources.js'; @@ -15,11 +17,18 @@ export const sharedResources = (config?: SharedResourcesConfig): PokuPlugin => { onTestProcess(child) { setupSharedResourceIPC(child); }, + setup(context) { + const { isolation } = context.configs; + + setExecutionMode(isolation === 'none' ? 'in-process' : 'process'); + }, async teardown() { const entries = Object.values(globalRegistry); for (const entry of entries) if (entry.onDestroy) await entry.onDestroy(entry.state); + + resetSharedResourcesRuntime(); }, }; }; diff --git a/src/shared-resources.ts b/src/shared-resources.ts index b05e57e..2ab2352 100644 --- a/src/shared-resources.ts +++ b/src/shared-resources.ts @@ -12,6 +12,7 @@ import type { ResourceContext, SendIPCMessageOptions, SharedResourceEntry, + SharedResourceExecutionMode, } from './types.js'; import process from 'node:process'; import { pathToFileURL } from 'node:url'; @@ -29,6 +30,7 @@ const isWindows = process.platform === 'win32'; const resourceRegistry = new ResourceRegistry(); const moduleCounters = new Map(); +let executionMode: SharedResourceExecutionMode = 'process'; export const SHARED_RESOURCE_MESSAGE_TYPES = { REQUEST_RESOURCE: 'shared_resources_requestResource', @@ -39,6 +41,19 @@ export const SHARED_RESOURCE_MESSAGE_TYPES = { export const globalRegistry = resourceRegistry.getRegistry(); +export const setExecutionMode = (mode: SharedResourceExecutionMode) => { + executionMode = mode; +}; + +export const getExecutionMode = () => executionMode; + +export const resetSharedResourcesRuntime = () => { + resourceRegistry.clear(); + resourceRegistry.setIsRegistering(false); + moduleCounters.clear(); + executionMode = 'process'; +}; + const create = ( factory: () => T, options?: { @@ -72,10 +87,19 @@ const use = async ( ): Promise> => { const { name } = context; - // Parent Process (Host) - if (!process.send || resourceRegistry.getIsRegistering()) { + // In-process execution always resolves resources directly from the local registry. + if ( + executionMode === 'in-process' || + !process.send || + resourceRegistry.getIsRegistering() + ) { const existing = resourceRegistry.get(name); if (existing) { + if (executionMode === 'in-process') + return constructInProcessResource( + existing.state as Record + ) as MethodsToRPC; + return existing.state as MethodsToRPC; } @@ -87,6 +111,11 @@ const use = async ( | undefined, }); + if (executionMode === 'in-process') + return constructInProcessResource( + state as Record + ) as MethodsToRPC; + return state as MethodsToRPC; } @@ -148,6 +177,12 @@ export const sendIPCMessage = ( }; const requestResource = async (name: string, module: string) => { + if (executionMode === 'in-process') { + throw new Error( + 'Cannot request shared resources through IPC while running in in-process mode.' + ); + } + const requestId = `${name}-${Date.now()}-${Math.random()}`; const response = await sendIPCMessage({ @@ -183,6 +218,12 @@ const remoteProcedureCall = async ( method: string, args: unknown[] ) => { + if (executionMode === 'in-process') { + throw new Error( + 'Cannot run shared resource RPCs through IPC while running in in-process mode.' + ); + } + const requestId = `${name}-${method}-${Date.now()}-${Math.random()}`; const response = await sendIPCMessage({ @@ -332,6 +373,8 @@ export const setupSharedResourceIPC = ( child: IPCEventEmitter | ChildProcess, registry: Record = globalRegistry ): void => { + if (executionMode === 'in-process') return; + child.on('message', async (message: IPCMessage) => { if (message.type === SHARED_RESOURCE_MESSAGE_TYPES.REQUEST_RESOURCE) await handleRequestResource(message, registry, child); @@ -497,6 +540,17 @@ const constructSharedResourceWithRPCs = ( }); }; +const constructInProcessResource = (target: Record) => + new Proxy(target, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + + if (typeof value !== 'function') return value; + + return async (...args: unknown[]) => value.apply(target, args); + }, + }); + export const resource = { create, use, diff --git a/src/types.ts b/src/types.ts index fa4dd63..d606373 100644 --- a/src/types.ts +++ b/src/types.ts @@ -142,3 +142,4 @@ export type SharedResourcesConfig = { // biome-ignore lint/suspicious/noExplicitAny: see ArgCodec codecs?: ArgCodec[]; }; +export type SharedResourceExecutionMode = 'process' | 'in-process'; diff --git a/test/integration/shared-resources.test.ts b/test/integration/shared-resources.test.ts index a1c350d..b8370f3 100644 --- a/test/integration/shared-resources.test.ts +++ b/test/integration/shared-resources.test.ts @@ -8,6 +8,19 @@ describe('Shared Resources', async () => { noExit: true, plugins: [sharedResources()], concurrency: 0, + timeout: 10000, + }); + + assert.strictEqual(code, 0, 'Exit Code needs to be 0'); + }); + + await it('Runs on isolation: none', async () => { + const code = await poku('test/__fixtures__/parallel', { + noExit: true, + plugins: [sharedResources()], + isolation: 'none', + concurrency: 0, + timeout: 10000, }); assert.strictEqual(code, 0, 'Exit Code needs to be 0'); diff --git a/test/unit/execution-mode.test.ts b/test/unit/execution-mode.test.ts new file mode 100644 index 0000000..c7a0fb7 --- /dev/null +++ b/test/unit/execution-mode.test.ts @@ -0,0 +1,56 @@ +import { assert, test } from 'poku'; +import { + getExecutionMode, + resetSharedResourcesRuntime, + setExecutionMode, + setupSharedResourceIPC, +} from '../../src/shared-resources.js'; + +test('Execution mode', async () => { + await test('should switch between process and in-process mode', () => { + setExecutionMode('in-process'); + assert.strictEqual( + getExecutionMode(), + 'in-process', + 'Mode should be in-process' + ); + + setExecutionMode('process'); + assert.strictEqual(getExecutionMode(), 'process', 'Mode should be process'); + }); + + await test('should skip IPC setup when running in-process', () => { + setExecutionMode('in-process'); + + let listenerCalls = 0; + const child = { + on() { + listenerCalls++; + }, + send() { + return true; + }, + }; + + setupSharedResourceIPC(child as never); + + assert.strictEqual( + listenerCalls, + 0, + 'No message listener should be attached in in-process mode' + ); + + setExecutionMode('process'); + }); + + await test('should reset execution mode to process', () => { + setExecutionMode('in-process'); + resetSharedResourcesRuntime(); + + assert.strictEqual( + getExecutionMode(), + 'process', + 'Reset should restore process mode' + ); + }); +});