@wordpress/worker-threads 1.0.1-next.v.202602200903.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE.md +788 -0
- package/README.md +132 -0
- package/build/index.cjs +37 -0
- package/build/index.cjs.map +7 -0
- package/build/main-thread.cjs +76 -0
- package/build/main-thread.cjs.map +7 -0
- package/build/types.cjs +31 -0
- package/build/types.cjs.map +7 -0
- package/build/worker-thread.cjs +49 -0
- package/build/worker-thread.cjs.map +7 -0
- package/build-module/index.mjs +10 -0
- package/build-module/index.mjs.map +7 -0
- package/build-module/main-thread.mjs +52 -0
- package/build-module/main-thread.mjs.map +7 -0
- package/build-module/types.mjs +6 -0
- package/build-module/types.mjs.map +7 -0
- package/build-module/worker-thread.mjs +26 -0
- package/build-module/worker-thread.mjs.map +7 -0
- package/build-types/index.d.ts +54 -0
- package/build-types/index.d.ts.map +1 -0
- package/build-types/main-thread.d.ts +41 -0
- package/build-types/main-thread.d.ts.map +1 -0
- package/build-types/types.d.ts +20 -0
- package/build-types/types.d.ts.map +1 -0
- package/build-types/worker-thread.d.ts +33 -0
- package/build-types/worker-thread.d.ts.map +1 -0
- package/package.json +58 -0
- package/src/index.ts +57 -0
- package/src/main-thread.ts +123 -0
- package/src/test/main-thread.test.ts +131 -0
- package/src/test/worker-thread.test.ts +118 -0
- package/src/types.ts +23 -0
- package/src/worker-thread.ts +67 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exposes an object's methods to be called from the main thread.
|
|
3
|
+
*
|
|
4
|
+
* This function should be called in the worker script to make methods
|
|
5
|
+
* available for RPC calls. Only methods (functions) on the object will
|
|
6
|
+
* be exposed; other properties are ignored.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // worker.ts
|
|
11
|
+
* import { expose } from '@wordpress/worker-threads/worker';
|
|
12
|
+
*
|
|
13
|
+
* const api = {
|
|
14
|
+
* async processImage(buffer: ArrayBuffer): Promise<ArrayBuffer> {
|
|
15
|
+
* // ... processing logic
|
|
16
|
+
* return resultBuffer;
|
|
17
|
+
* },
|
|
18
|
+
*
|
|
19
|
+
* async calculateSum(a: number, b: number): Promise<number> {
|
|
20
|
+
* return a + b;
|
|
21
|
+
* }
|
|
22
|
+
* };
|
|
23
|
+
*
|
|
24
|
+
* expose(api);
|
|
25
|
+
*
|
|
26
|
+
* // Export the type for use with wrap() on main thread
|
|
27
|
+
* export type WorkerAPI = typeof api;
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @param target - Object containing methods to expose to the main thread.
|
|
31
|
+
*/
|
|
32
|
+
export declare function expose<T extends object>(target: T): void;
|
|
33
|
+
//# sourceMappingURL=worker-thread.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-thread.d.ts","sourceRoot":"","sources":["../src/worker-thread.ts"],"names":[],"mappings":"AAyBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,MAAM,CAAE,CAAC,SAAS,MAAM,EAAI,MAAM,EAAE,CAAC,GAAI,IAAI,CAU5D"}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wordpress/worker-threads",
|
|
3
|
+
"version": "1.0.1-next.v.202602200903.0+06edfb869",
|
|
4
|
+
"description": "Utilities for type-safe Web Worker communication with RPC.",
|
|
5
|
+
"author": "The WordPress Contributors",
|
|
6
|
+
"license": "GPL-2.0-or-later",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"wordpress",
|
|
9
|
+
"worker",
|
|
10
|
+
"web-worker",
|
|
11
|
+
"rpc",
|
|
12
|
+
"threads"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/worker-threads/README.md",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/WordPress/gutenberg.git",
|
|
18
|
+
"directory": "packages/worker-threads"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/WordPress/gutenberg/issues"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.12.0",
|
|
25
|
+
"npm": ">=8.19.2"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"src",
|
|
29
|
+
"build",
|
|
30
|
+
"build-module",
|
|
31
|
+
"build-types",
|
|
32
|
+
"*.md"
|
|
33
|
+
],
|
|
34
|
+
"main": "build/index.cjs",
|
|
35
|
+
"module": "build-module/index.mjs",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./build-types/index.d.ts",
|
|
39
|
+
"import": "./build-module/index.mjs",
|
|
40
|
+
"default": "./build/index.cjs"
|
|
41
|
+
},
|
|
42
|
+
"./worker": {
|
|
43
|
+
"types": "./build-types/worker-thread.d.ts",
|
|
44
|
+
"import": "./build-module/worker-thread.mjs",
|
|
45
|
+
"default": "./build/worker-thread.cjs"
|
|
46
|
+
},
|
|
47
|
+
"./package.json": "./package.json"
|
|
48
|
+
},
|
|
49
|
+
"types": "build-types",
|
|
50
|
+
"sideEffects": false,
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"comctx": "^1.4.3"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"gitHead": "6e225a6505fdc8e407305851c7a68b781dee8118"
|
|
58
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main entry point for @wordpress/worker-threads.
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for type-safe Web Worker communication
|
|
5
|
+
* using an RPC (Remote Procedure Call) pattern. It allows you to call
|
|
6
|
+
* methods on a worker as if they were local async functions.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* Main thread usage:
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { wrap, terminate } from '@wordpress/worker-threads';
|
|
12
|
+
* import type { WorkerAPI } from './worker';
|
|
13
|
+
*
|
|
14
|
+
* const worker = new Worker(new URL('./worker.js', import.meta.url));
|
|
15
|
+
* const api = wrap<WorkerAPI>(worker);
|
|
16
|
+
*
|
|
17
|
+
* const result = await api.processData(data);
|
|
18
|
+
*
|
|
19
|
+
* terminate(api);
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Worker thread usage:
|
|
23
|
+
* ```typescript
|
|
24
|
+
* import { expose } from '@wordpress/worker-threads';
|
|
25
|
+
*
|
|
26
|
+
* const api = {
|
|
27
|
+
* async processData(data: ArrayBuffer): Promise<ArrayBuffer> {
|
|
28
|
+
* // ... processing
|
|
29
|
+
* return result;
|
|
30
|
+
* }
|
|
31
|
+
* };
|
|
32
|
+
*
|
|
33
|
+
* expose(api);
|
|
34
|
+
* export type WorkerAPI = typeof api;
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Wraps a Worker to provide a type-safe RPC interface.
|
|
40
|
+
*/
|
|
41
|
+
export { wrap } from './main-thread';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Terminates a wrapped worker and cleans up resources.
|
|
45
|
+
*/
|
|
46
|
+
export { terminate } from './main-thread';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Exposes an object's methods to be called from the main thread.
|
|
50
|
+
* This should be called in the worker script.
|
|
51
|
+
*/
|
|
52
|
+
export { expose } from './worker-thread';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Type that converts all methods to async versions.
|
|
56
|
+
*/
|
|
57
|
+
export type { Remote } from './types';
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
defineProxy,
|
|
6
|
+
type Adapter,
|
|
7
|
+
type SendMessage,
|
|
8
|
+
type OnMessage,
|
|
9
|
+
} from 'comctx';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Internal dependencies
|
|
13
|
+
*/
|
|
14
|
+
import { WORKER_SYMBOL, type Remote, type WithWorker } from './types';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Adapter for injecting (main thread calling worker).
|
|
18
|
+
*/
|
|
19
|
+
class WorkerInjectAdapter implements Adapter {
|
|
20
|
+
private worker: Worker;
|
|
21
|
+
|
|
22
|
+
constructor( worker: Worker ) {
|
|
23
|
+
this.worker = worker;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
sendMessage: SendMessage = ( message, transfer ) => {
|
|
27
|
+
this.worker.postMessage( message, transfer );
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
onMessage: OnMessage = ( callback ) => {
|
|
31
|
+
const handler = ( event: MessageEvent ) => callback( event.data );
|
|
32
|
+
this.worker.addEventListener( 'message', handler );
|
|
33
|
+
return () => this.worker.removeEventListener( 'message', handler );
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* WeakMap to store workers for each remote proxy.
|
|
39
|
+
*/
|
|
40
|
+
const remoteWorkers = new WeakMap< object, Worker >();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Wraps a Worker to provide a type-safe RPC interface.
|
|
44
|
+
*
|
|
45
|
+
* The returned proxy object allows calling methods on the worker as if they
|
|
46
|
+
* were local async functions. Each method call is automatically serialized,
|
|
47
|
+
* sent to the worker, and the result is returned as a Promise.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const worker = new Worker(new URL('./worker.js', import.meta.url));
|
|
52
|
+
* const api = wrap<MyWorkerAPI>(worker);
|
|
53
|
+
*
|
|
54
|
+
* // Call worker methods as async functions
|
|
55
|
+
* const result = await api.processData(data);
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @param worker - The Worker instance to wrap.
|
|
59
|
+
* @return A proxy object with all exposed methods as async functions.
|
|
60
|
+
*/
|
|
61
|
+
export function wrap< T extends object >( worker: Worker ): Remote< T > {
|
|
62
|
+
// Create the inject function using defineProxy with an empty object
|
|
63
|
+
// (the actual implementation is on the worker side).
|
|
64
|
+
const [ , inject ] = defineProxy( () => ( {} ) as T, {
|
|
65
|
+
namespace: '__wordpress_worker__',
|
|
66
|
+
heartbeatCheck: false,
|
|
67
|
+
transfer: true,
|
|
68
|
+
} );
|
|
69
|
+
|
|
70
|
+
// Create the proxy using the injector.
|
|
71
|
+
const comctxRemote = inject( new WorkerInjectAdapter( worker ) );
|
|
72
|
+
|
|
73
|
+
// Store the worker reference.
|
|
74
|
+
remoteWorkers.set( comctxRemote as object, worker );
|
|
75
|
+
|
|
76
|
+
// Create a wrapper proxy that adds WORKER_SYMBOL support.
|
|
77
|
+
const proxy = new Proxy( comctxRemote as Remote< T > & WithWorker, {
|
|
78
|
+
get( target, prop: string | symbol ) {
|
|
79
|
+
// Return the worker for the WORKER_SYMBOL.
|
|
80
|
+
if ( prop === WORKER_SYMBOL ) {
|
|
81
|
+
return worker;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Delegate all other property access to the comctx remote.
|
|
85
|
+
return Reflect.get( target, prop );
|
|
86
|
+
},
|
|
87
|
+
} );
|
|
88
|
+
|
|
89
|
+
// Store the worker for the proxy as well.
|
|
90
|
+
remoteWorkers.set( proxy, worker );
|
|
91
|
+
|
|
92
|
+
return proxy;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Terminates a wrapped worker and cleans up resources.
|
|
97
|
+
*
|
|
98
|
+
* After calling terminate, any pending calls will be rejected and the
|
|
99
|
+
* worker will be stopped.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* const api = wrap<MyWorkerAPI>(worker);
|
|
104
|
+
* // ... use the API ...
|
|
105
|
+
* terminate(api); // Clean up when done
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* @param remote - The wrapped worker proxy returned by wrap().
|
|
109
|
+
*/
|
|
110
|
+
export function terminate( remote: Remote< unknown > ): void {
|
|
111
|
+
// Get the worker from the proxy.
|
|
112
|
+
const worker = ( remote as unknown as WithWorker )[ WORKER_SYMBOL ];
|
|
113
|
+
|
|
114
|
+
if ( ! worker ) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Clean up the worker reference.
|
|
119
|
+
remoteWorkers.delete( remote as object );
|
|
120
|
+
|
|
121
|
+
// Terminate the worker.
|
|
122
|
+
worker.terminate();
|
|
123
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { wrap, terminate } from '../main-thread';
|
|
5
|
+
import { WORKER_SYMBOL } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Mock Worker class for testing.
|
|
9
|
+
* Implements addEventListener pattern used by comctx.
|
|
10
|
+
*/
|
|
11
|
+
class MockWorker {
|
|
12
|
+
postMessage = jest.fn();
|
|
13
|
+
terminate = jest.fn();
|
|
14
|
+
|
|
15
|
+
private messageListeners: Array< ( event: MessageEvent ) => void > = [];
|
|
16
|
+
|
|
17
|
+
addEventListener( type: string, handler: ( event: MessageEvent ) => void ) {
|
|
18
|
+
if ( type === 'message' ) {
|
|
19
|
+
this.messageListeners.push( handler );
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
removeEventListener(
|
|
24
|
+
type: string,
|
|
25
|
+
handler: ( event: MessageEvent ) => void
|
|
26
|
+
) {
|
|
27
|
+
if ( type === 'message' ) {
|
|
28
|
+
this.messageListeners = this.messageListeners.filter(
|
|
29
|
+
( h ) => h !== handler
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Simulate receiving a message from the worker.
|
|
36
|
+
*
|
|
37
|
+
* @param data
|
|
38
|
+
*/
|
|
39
|
+
simulateMessage( data: unknown ) {
|
|
40
|
+
for ( const handler of this.messageListeners ) {
|
|
41
|
+
handler( { data } as MessageEvent );
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe( 'main-thread', () => {
|
|
47
|
+
describe( 'wrap', () => {
|
|
48
|
+
it( 'should return a proxy object', () => {
|
|
49
|
+
const worker = new MockWorker();
|
|
50
|
+
const remote = wrap( worker as unknown as Worker );
|
|
51
|
+
|
|
52
|
+
expect( remote ).toBeDefined();
|
|
53
|
+
expect( typeof remote ).toBe( 'object' );
|
|
54
|
+
} );
|
|
55
|
+
|
|
56
|
+
it( 'should return WORKER_SYMBOL reference', () => {
|
|
57
|
+
const worker = new MockWorker();
|
|
58
|
+
const remote = wrap( worker as unknown as Worker );
|
|
59
|
+
|
|
60
|
+
expect(
|
|
61
|
+
( remote as unknown as Record< symbol, unknown > )[
|
|
62
|
+
WORKER_SYMBOL
|
|
63
|
+
]
|
|
64
|
+
).toBe( worker );
|
|
65
|
+
} );
|
|
66
|
+
|
|
67
|
+
it( 'should return functions for property access', () => {
|
|
68
|
+
const worker = new MockWorker();
|
|
69
|
+
const remote = wrap< { testMethod: () => void } >(
|
|
70
|
+
worker as unknown as Worker
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect( typeof remote.testMethod ).toBe( 'function' );
|
|
74
|
+
} );
|
|
75
|
+
|
|
76
|
+
it( 'should return a Promise from method calls', () => {
|
|
77
|
+
const worker = new MockWorker();
|
|
78
|
+
const remote = wrap< { testMethod: () => string } >(
|
|
79
|
+
worker as unknown as Worker
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const result = remote.testMethod();
|
|
83
|
+
|
|
84
|
+
expect( result ).toBeInstanceOf( Promise );
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
it( 'should handle multiple concurrent calls returning promises', async () => {
|
|
88
|
+
const worker = new MockWorker();
|
|
89
|
+
const remote = wrap< { method: ( n: number ) => number } >(
|
|
90
|
+
worker as unknown as Worker
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Make concurrent calls.
|
|
94
|
+
const promise1 = remote.method( 1 );
|
|
95
|
+
const promise2 = remote.method( 2 );
|
|
96
|
+
|
|
97
|
+
// Both should be pending promises.
|
|
98
|
+
expect( promise1 ).toBeInstanceOf( Promise );
|
|
99
|
+
expect( promise2 ).toBeInstanceOf( Promise );
|
|
100
|
+
} );
|
|
101
|
+
} );
|
|
102
|
+
|
|
103
|
+
describe( 'terminate', () => {
|
|
104
|
+
it( 'should call worker.terminate()', () => {
|
|
105
|
+
const worker = new MockWorker();
|
|
106
|
+
const remote = wrap( worker as unknown as Worker );
|
|
107
|
+
|
|
108
|
+
terminate( remote );
|
|
109
|
+
|
|
110
|
+
expect( worker.terminate ).toHaveBeenCalled();
|
|
111
|
+
} );
|
|
112
|
+
|
|
113
|
+
it( 'should handle terminate called multiple times', () => {
|
|
114
|
+
const worker = new MockWorker();
|
|
115
|
+
const remote = wrap( worker as unknown as Worker );
|
|
116
|
+
|
|
117
|
+
terminate( remote );
|
|
118
|
+
terminate( remote );
|
|
119
|
+
|
|
120
|
+
// Should not throw - worker.terminate() is idempotent.
|
|
121
|
+
expect( worker.terminate ).toHaveBeenCalled();
|
|
122
|
+
} );
|
|
123
|
+
|
|
124
|
+
it( 'should handle terminate on non-wrapped object', () => {
|
|
125
|
+
const notWrapped = {} as ReturnType< typeof wrap >;
|
|
126
|
+
|
|
127
|
+
// Should not throw.
|
|
128
|
+
expect( () => terminate( notWrapped ) ).not.toThrow();
|
|
129
|
+
} );
|
|
130
|
+
} );
|
|
131
|
+
} );
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Store the original self.
|
|
2
|
+
const originalSelf = globalThis.self;
|
|
3
|
+
|
|
4
|
+
// Mock self for worker context.
|
|
5
|
+
let mockPostMessage: jest.Mock;
|
|
6
|
+
let messageListeners: Array< ( event: MessageEvent ) => void > = [];
|
|
7
|
+
|
|
8
|
+
function setupMockSelf() {
|
|
9
|
+
mockPostMessage = jest.fn();
|
|
10
|
+
messageListeners = [];
|
|
11
|
+
|
|
12
|
+
// Override self with addEventListener pattern (matching comctx usage).
|
|
13
|
+
const mockSelf = {
|
|
14
|
+
postMessage: mockPostMessage,
|
|
15
|
+
addEventListener: (
|
|
16
|
+
type: string,
|
|
17
|
+
handler: ( event: MessageEvent ) => void
|
|
18
|
+
) => {
|
|
19
|
+
if ( type === 'message' ) {
|
|
20
|
+
messageListeners.push( handler );
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
removeEventListener: (
|
|
24
|
+
type: string,
|
|
25
|
+
handler: ( event: MessageEvent ) => void
|
|
26
|
+
) => {
|
|
27
|
+
if ( type === 'message' ) {
|
|
28
|
+
messageListeners = messageListeners.filter(
|
|
29
|
+
( h ) => h !== handler
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
Object.defineProperty( globalThis, 'self', {
|
|
36
|
+
value: mockSelf,
|
|
37
|
+
writable: true,
|
|
38
|
+
configurable: true,
|
|
39
|
+
} );
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function restoreSelf() {
|
|
43
|
+
Object.defineProperty( globalThis, 'self', {
|
|
44
|
+
value: originalSelf,
|
|
45
|
+
writable: true,
|
|
46
|
+
configurable: true,
|
|
47
|
+
} );
|
|
48
|
+
messageListeners = [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe( 'worker-thread', () => {
|
|
52
|
+
beforeEach( () => {
|
|
53
|
+
setupMockSelf();
|
|
54
|
+
jest.resetModules();
|
|
55
|
+
} );
|
|
56
|
+
|
|
57
|
+
afterEach( () => {
|
|
58
|
+
restoreSelf();
|
|
59
|
+
} );
|
|
60
|
+
|
|
61
|
+
describe( 'expose', () => {
|
|
62
|
+
it( 'should set up message handler', async () => {
|
|
63
|
+
const { expose } = await import( '../worker-thread' );
|
|
64
|
+
const api = { method: jest.fn() };
|
|
65
|
+
|
|
66
|
+
expose( api );
|
|
67
|
+
|
|
68
|
+
expect( messageListeners.length ).toBeGreaterThan( 0 );
|
|
69
|
+
} );
|
|
70
|
+
|
|
71
|
+
it( 'should register handlers for all methods on target', async () => {
|
|
72
|
+
const { expose } = await import( '../worker-thread' );
|
|
73
|
+
const method1 = jest.fn();
|
|
74
|
+
const method2 = jest.fn();
|
|
75
|
+
const api = {
|
|
76
|
+
method1,
|
|
77
|
+
method2,
|
|
78
|
+
notAFunction: 'string value',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// expose() should complete without error.
|
|
82
|
+
expect( () => expose( api ) ).not.toThrow();
|
|
83
|
+
} );
|
|
84
|
+
|
|
85
|
+
it( 'should only expose functions, not other properties', async () => {
|
|
86
|
+
const { expose } = await import( '../worker-thread' );
|
|
87
|
+
const api = {
|
|
88
|
+
validMethod: jest.fn(),
|
|
89
|
+
stringProp: 'not a function',
|
|
90
|
+
numberProp: 42,
|
|
91
|
+
objectProp: { nested: true },
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Should not throw.
|
|
95
|
+
expect( () => expose( api ) ).not.toThrow();
|
|
96
|
+
} );
|
|
97
|
+
|
|
98
|
+
it( 'should handle empty object', async () => {
|
|
99
|
+
const { expose } = await import( '../worker-thread' );
|
|
100
|
+
const api = {};
|
|
101
|
+
|
|
102
|
+
// Should not throw.
|
|
103
|
+
expect( () => expose( api ) ).not.toThrow();
|
|
104
|
+
} );
|
|
105
|
+
|
|
106
|
+
it( 'should handle object with async methods', async () => {
|
|
107
|
+
const { expose } = await import( '../worker-thread' );
|
|
108
|
+
const api = {
|
|
109
|
+
asyncMethod: async () => {
|
|
110
|
+
return 'async result';
|
|
111
|
+
},
|
|
112
|
+
syncMethod: () => 'sync result',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
expect( () => expose( api ) ).not.toThrow();
|
|
116
|
+
} );
|
|
117
|
+
} );
|
|
118
|
+
} );
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a type to its "remote" version where all methods become async.
|
|
3
|
+
*
|
|
4
|
+
* This type transformation ensures that when calling methods on a wrapped
|
|
5
|
+
* worker, the return types are properly wrapped in Promises.
|
|
6
|
+
*/
|
|
7
|
+
export type Remote< T > = {
|
|
8
|
+
[ K in keyof T ]: T[ K ] extends ( ...args: infer A ) => infer R
|
|
9
|
+
? ( ...args: A ) => Promise< Awaited< R > >
|
|
10
|
+
: never;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Internal symbol used to store the worker reference on the proxy.
|
|
15
|
+
*/
|
|
16
|
+
export const WORKER_SYMBOL = Symbol( 'worker' );
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Interface for objects that have an associated worker.
|
|
20
|
+
*/
|
|
21
|
+
export interface WithWorker {
|
|
22
|
+
[ WORKER_SYMBOL ]: Worker;
|
|
23
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
defineProxy,
|
|
6
|
+
type Adapter,
|
|
7
|
+
type SendMessage,
|
|
8
|
+
type OnMessage,
|
|
9
|
+
} from 'comctx';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Adapter for providing (worker exposing methods to main thread).
|
|
13
|
+
*/
|
|
14
|
+
class WorkerProvideAdapter implements Adapter {
|
|
15
|
+
sendMessage: SendMessage = ( message, transfer ) => {
|
|
16
|
+
self.postMessage( message, { transfer } );
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
onMessage: OnMessage = ( callback ) => {
|
|
20
|
+
const handler = ( event: MessageEvent ) => callback( event.data );
|
|
21
|
+
self.addEventListener( 'message', handler );
|
|
22
|
+
return () => self.removeEventListener( 'message', handler );
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Exposes an object's methods to be called from the main thread.
|
|
28
|
+
*
|
|
29
|
+
* This function should be called in the worker script to make methods
|
|
30
|
+
* available for RPC calls. Only methods (functions) on the object will
|
|
31
|
+
* be exposed; other properties are ignored.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* // worker.ts
|
|
36
|
+
* import { expose } from '@wordpress/worker-threads/worker';
|
|
37
|
+
*
|
|
38
|
+
* const api = {
|
|
39
|
+
* async processImage(buffer: ArrayBuffer): Promise<ArrayBuffer> {
|
|
40
|
+
* // ... processing logic
|
|
41
|
+
* return resultBuffer;
|
|
42
|
+
* },
|
|
43
|
+
*
|
|
44
|
+
* async calculateSum(a: number, b: number): Promise<number> {
|
|
45
|
+
* return a + b;
|
|
46
|
+
* }
|
|
47
|
+
* };
|
|
48
|
+
*
|
|
49
|
+
* expose(api);
|
|
50
|
+
*
|
|
51
|
+
* // Export the type for use with wrap() on main thread
|
|
52
|
+
* export type WorkerAPI = typeof api;
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @param target - Object containing methods to expose to the main thread.
|
|
56
|
+
*/
|
|
57
|
+
export function expose< T extends object >( target: T ): void {
|
|
58
|
+
// Create the provide function using defineProxy with the target.
|
|
59
|
+
const [ provide ] = defineProxy( () => target, {
|
|
60
|
+
namespace: '__wordpress_worker__',
|
|
61
|
+
heartbeatCheck: false,
|
|
62
|
+
transfer: true,
|
|
63
|
+
} );
|
|
64
|
+
|
|
65
|
+
// Start providing the target through the adapter.
|
|
66
|
+
provide( new WorkerProvideAdapter() );
|
|
67
|
+
}
|