Skip to main content
Version: v4.x.x

πŸͺ’ Shared Resources

Share state, servers, database connections, and more between parallel test files β€” no duplicated setup, no conflicts.

Plugin
History
VersionChanges
v1.1.1
Fixed execution under isolation: none by forcing in-process mode and skipping IPC wiring.
v1.1.0
Added reference mutation write-back through IPC for nested objects and special types.
Introduced the ArgCodec system with symbol tags, built-in codecs, and configurable custom codecs.
v3.0.3-canary.68e71482
Migrate Shared Resources to a dedicated plugin.
v3.0.3-canary.5b54775d
Refactored API with automatic cleanup, lazy loading, and type inference.
v3.0.3-canary.ffab4562
Improved serialization and Windows support.
v3.0.3-canary.60ff5ce2
Introduce Shared Resources.

What are Shared Resources?​

Shared Resources enable you to define a stateful resource once and access it from multiple test files and processes. Each test gets the same resource instance, allowing you to:

  • Share database connections, in-memory stores, or API clients across tests.
  • Coordinate state and assertions between parallel or sequential tests.
  • Manage setup and teardown logic in one place.

The resource system automatically handles inter-process communication, serialization, and cleanup β€” so you can focus on writing tests.

Install​

npm i -D @pokujs/shared-resources

Enabling the Plugin​

Add the plugin to your Poku configuration file:

// poku.config.ts
import { sharedResources } from '@pokujs/shared-resources';
import { defineConfig } from 'poku';

export default defineConfig({
plugins: [sharedResources()],
});

You can also register custom codecs at plugin setup time:

// poku.config.ts
import { sharedResources } from '@pokujs/shared-resources';
import { defineConfig } from 'poku';

export default defineConfig({
plugins: [
sharedResources({
codecs: [
// custom ArgCodec values
],
}),
],
});

Basic Usage​

1. Define a Resource Context​

Create a resource file using resource.create():

// counter.ts
import { resource } from '@pokujs/shared-resources';

export const CounterContext = resource.create(() => ({
count: 0,
increment() {
this.count++;
return this.count;
},
getCount() {
return this.count;
},
}));

resource.create() automatically detects the file path of your resource module. It takes a factory function as its first argument β€” a function that returns the resource object with methods.

Troubleshooting: If resource.create() fails to detect the module path automatically, you can set module explicitly as a fallback:

export const CounterContext = resource.create(
() => ({
/* ... */
}),
{ module: __filename } // or import.meta.url for ESM
);

2. Access the Resource in Tests​

Use resource.use() to get the resource:

import { resource } from '@pokujs/shared-resources';
import { assert, test } from 'poku';
import { CounterContext } from './counter';

test('increment counter', async () => {
const counter = await resource.use(CounterContext);

const current = await counter.getCount();
assert.equal(current, 0);

await counter.increment();
assert.equal(await counter.getCount(), 1);
});

3. Methods are Remote Procedure Calls​

All methods on your resource become async RPCs that work across processes. State is automatically synchronized:

test('shared state across tests', async () => {
const counter = await resource.use(CounterContext);

// This gets the current state from the resource
const value = await counter.getCount();
assert.equal(value, 1); // reflects changes from other test

// This mutates the shared state
await counter.increment();
});

On isolation: 'none', the plugin automatically switches to in-process mode. In this mode, methods are still exposed as async functions, but calls execute directly in the same process (no IPC).

Execution Modes​

The plugin chooses the execution mode from Poku isolation settings:

  • isolation: 'process' (or other process-based isolation) uses IPC mode.
  • isolation: 'none' uses in-process mode and skips IPC wiring.

This means Shared Resources work consistently in both modes without additional configuration.

Real-World Example​

Here's a complete example of two tests sharing a counter:

counter.ts

import { resource } from '@pokujs/shared-resources';

export const CounterContext = resource.create(() => ({
count: 0,
increment() {
this.count++;
return this.count;
},
getCount() {
return this.count;
},
}));

test-a.test.ts

import { resource } from '@pokujs/shared-resources';
import { assert, test } from 'poku';
import { CounterContext } from './counter';

test('Test A: Increment Counter', async () => {
const counter = await resource.use(CounterContext);

const current = await counter.getCount();
assert.strictEqual(current, 0, 'Should start at 0');

await counter.increment();
assert.strictEqual(
await counter.getCount(),
1,
'Should be 1 after increment'
);
});

test-b.test.ts

import { resource } from '@pokujs/shared-resources';
import { assert, test, waitForExpectedResult } from 'poku';
import { CounterContext } from './counter';

test('Test B: Verify Counter State', async () => {
const counter = await resource.use(CounterContext);

// If you need the outcome of Test A to be ready before proceeding,
// It is recommended to wait for the expected state to avoid flakiness.
await waitForExpectedResult(counter.getCount, 1);

await counter.increment();
assert.strictEqual(
await counter.getCount(),
2,
'Should be 2 after increment'
);
});

When these tests run, they share the same counter instance. Test B sees the state changes made by Test A.

Advanced Usage​

Resource Lifecycle​

To perform an action when the resource is destroyed, use the onDestroy option:

import { resource } from '@pokujs/shared-resources';
import { createConnection } from 'mysql2/promise';

export const DatabaseContext = resource.create(
() =>
createConnection({
host: 'localhost',
user: 'root',
database: 'test',
}),
{
onDestroy: (connection) => connection.end(),
}
);
import type { RowDataPacket } from 'mysql2/promise';
import { resource } from '@pokujs/shared-resources';
import { assert, test } from 'poku';
import { DatabaseContext } from './db.js';

type QueryResult = RowDataPacket & {
total: number;
};

await test('Connection', async () => {
const connection = await resource.use(DatabaseContext);
const [rows] = await connection.execute<QueryResult[]>(
'SELECT 1 + 1 AS total'
);

assert.strictEqual(rows[0].total, 2);
});
tip

Resources can be initialized asynchronously.

Important Notes​

Serialization​

Arguments and return values are serialized through a codec pipeline.

Built-in codecs support:

  • undefined
  • bigint
  • Date
  • Map
  • Set
  • Arrays and objects (including nested combinations)

Class instances are serialized as own enumerable data properties by default. When values cross process boundaries, prototype chains are not reconstructed automatically.

If you need custom reconstruction behavior, register an ArgCodec.

Custom Codecs (ArgCodec)​

Use codecs for custom types that cannot be represented faithfully by default JSON semantics.

import type { ArgCodec } from '@pokujs/shared-resources';
import { resource } from '@pokujs/shared-resources';

class Token {
constructor(public value: string) {}
}

const tokenCodec: ArgCodec<Token> = {
tag: Symbol.for('example:Token'),
is: (v): v is Token => v instanceof Token,
encode: (v) => ({ value: v.value }),
decode: (v) => new Token((v as { value: string }).value),
};

resource.configure({
codecs: [tokenCodec],
});

resource.configure({ codecs }) is the safest place to register codecs because resource modules are evaluated in both parent and child processes.

sharedResources({ codecs }) configures codecs in the parent process. Use both only when your setup requires it.

Lazy Loading​

Resources are created on-demand the first time you call resource.use(). If a test never accesses a resource, it's never created.