yuzuthread 1.0.2 → 1.0.4

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/AGENTS.md CHANGED
@@ -8,6 +8,7 @@
8
8
  - 和业务无关的类型放在 src/utility/types.ts 里。如果类型太长,那么另开文件。
9
9
  - 对于写的任何一个小方法,都要写单元测试 .spec.ts 然后跑一下验证对不对。
10
10
  - 测试的时候禁止同时跑两个命令,否则可能会有冲突。
11
+ - **禁止使用 `| head` 或 `| tail` 管道命令,这会导致进程卡住。** 如果需要过滤输出,使用 `grep` 等其他工具。
11
12
 
12
13
  ## 项目目标
13
14
 
package/README.md CHANGED
@@ -123,7 +123,9 @@ console.log(value); // -> 5
123
123
 
124
124
  Use this when worker-side logic needs to call back into main-thread state or services.
125
125
 
126
- ## `typed-struct` Shared Memory
126
+ ## Shared Memory with `typed-struct`
127
+
128
+ ### Worker Class Shared Memory
127
129
 
128
130
  If a worker class inherits from a compiled `typed-struct` class, `yuzuthread` automatically:
129
131
 
@@ -154,6 +156,197 @@ console.log(instance.value); // -> 0x7f
154
156
  await instance.finalize();
155
157
  ```
156
158
 
159
+ ### Shared Constructor Parameters with `@Shared`
160
+
161
+ Use `@Shared()` to mark constructor parameters that should use shared memory:
162
+
163
+ ```ts
164
+ import { Struct } from 'typed-struct';
165
+ import { DefineWorker, WorkerMethod, Shared, initWorker } from 'yuzuthread';
166
+
167
+ const SharedDataBase = new Struct('SharedData')
168
+ .UInt32LE('counter')
169
+ .compile();
170
+
171
+ class SharedData extends SharedDataBase {
172
+ declare counter: number;
173
+ }
174
+
175
+ @DefineWorker()
176
+ class SharedParamWorker {
177
+ constructor(
178
+ @Shared(() => SharedData) public data: SharedData,
179
+ ) {}
180
+
181
+ @WorkerMethod()
182
+ increment() {
183
+ this.data.counter++;
184
+ return this.data.counter;
185
+ }
186
+ }
187
+
188
+ const data = new SharedData();
189
+ data.counter = 100;
190
+
191
+ const worker = await initWorker(SharedParamWorker, [data]);
192
+
193
+ // Main thread and worker share the same memory
194
+ await worker.increment();
195
+ console.log(worker.data.counter); // -> 101 (updated by worker)
196
+
197
+ data.counter = 200;
198
+ console.log(await worker.increment()); // -> 201 (sees main thread's change)
199
+
200
+ await worker.finalize();
201
+ ```
202
+
203
+ **How it works:**
204
+ - Parameters marked with `@Shared()` are converted to use `SharedArrayBuffer` during worker initialization
205
+ - The factory function `() => Type` is optional - if omitted, type is inferred from `design:paramtypes`
206
+ - The library calculates total memory needed (including worker class itself if typed-struct)
207
+ - Both main thread and worker thread share the same underlying memory
208
+ - Works with `Buffer`, `SharedArrayBuffer`, `typed-struct` classes, and user classes containing these
209
+
210
+ **Requirements:**
211
+ - The parameter type must contain shared memory segments (`typed-struct`, `Buffer`, or `SharedArrayBuffer`)
212
+ - If the type has no shared memory segments, an error is thrown
213
+ - Can be combined with worker class shared memory (worker class itself is typed-struct)
214
+
215
+ **Multiple shared parameters:**
216
+
217
+ ```ts
218
+ @DefineWorker()
219
+ class MultiSharedWorker {
220
+ constructor(
221
+ @Shared(() => SharedData) public data1: SharedData,
222
+ @Shared(() => SharedData) public data2: SharedData,
223
+ @Shared(() => Buffer) public buffer: Buffer,
224
+ ) {}
225
+
226
+ @WorkerMethod()
227
+ updateAll() {
228
+ this.data1.counter++;
229
+ this.data2.counter--;
230
+ this.buffer[0] = 0xff;
231
+ }
232
+ }
233
+
234
+ const data1 = new SharedData();
235
+ const data2 = new SharedData();
236
+ const buffer = Buffer.alloc(10);
237
+
238
+ const worker = await initWorker(MultiSharedWorker, [data1, data2, buffer]);
239
+ await worker.updateAll();
240
+
241
+ // All parameters are shared
242
+ console.log(data1.counter); // updated by worker
243
+ console.log(data2.counter); // updated by worker
244
+ console.log(buffer[0]); // -> 0xff
245
+ ```
246
+
247
+ ### Manual Shared Memory Conversion with `toShared()`
248
+
249
+ `toShared()` converts objects to use shared memory. **Important:** The object must contain shared memory segments to be converted:
250
+ - The class itself is a `typed-struct` class, OR
251
+ - The object has `Buffer` or `SharedArrayBuffer` fields, OR
252
+ - The object has fields (marked with `@TransportType()`) that are `typed-struct` classes
253
+
254
+ ```ts
255
+ import { Struct } from 'typed-struct';
256
+ import { toShared, TransportType } from 'yuzuthread';
257
+
258
+ // Example 1: typed-struct class
259
+ const SharedDataBase = new Struct('SharedData')
260
+ .UInt32LE('counter')
261
+ .compile();
262
+
263
+ class SharedData extends SharedDataBase {
264
+ declare counter: number;
265
+ }
266
+
267
+ const data = new SharedData();
268
+ data.counter = 42;
269
+
270
+ const sharedData = toShared(data);
271
+ // sharedData is a NEW instance using SharedArrayBuffer
272
+ console.log(sharedData.counter); // -> 42
273
+
274
+ // Example 2: Buffer
275
+ const buffer = Buffer.from('hello');
276
+ const sharedBuffer = toShared(buffer);
277
+ // sharedBuffer is a NEW Buffer backed by SharedArrayBuffer
278
+ console.log(sharedBuffer.toString()); // -> 'hello'
279
+
280
+ // Example 3: User class with typed-struct field
281
+ class Container {
282
+ @TransportType(() => SharedData)
283
+ data!: SharedData;
284
+
285
+ label: string = '';
286
+ }
287
+
288
+ const container = new Container();
289
+ container.data = new SharedData();
290
+ container.data.counter = 100;
291
+ container.label = 'test';
292
+
293
+ toShared(container); // Converts container.data in-place
294
+ // container.data is now a different instance using SharedArrayBuffer
295
+ console.log(container.data.counter); // -> 100
296
+ console.log(container.label); // -> 'test' (unchanged)
297
+
298
+ // Example 4: User class with Buffer field
299
+ class BufferContainer {
300
+ @TransportType(() => Buffer)
301
+ buffer!: Buffer;
302
+ }
303
+
304
+ const bufferContainer = new BufferContainer();
305
+ bufferContainer.buffer = Buffer.from('data');
306
+
307
+ toShared(bufferContainer); // Converts bufferContainer.buffer in-place
308
+ // bufferContainer.buffer is now backed by SharedArrayBuffer
309
+
310
+ // Example 5: Complex nested structure
311
+ class NestedContainer {
312
+ @TransportType(() => Container)
313
+ container!: Container;
314
+
315
+ @TransportType(() => Buffer)
316
+ buffer!: Buffer;
317
+ }
318
+
319
+ const nested = new NestedContainer();
320
+ nested.container = new Container();
321
+ nested.container.data = new SharedData();
322
+ nested.container.data.counter = 200;
323
+ nested.buffer = Buffer.from('nested');
324
+
325
+ toShared(nested);
326
+ // Both nested.container.data and nested.buffer are now shared
327
+ console.log(nested.container.data.counter); // -> 200
328
+ ```
329
+
330
+ **How `toShared()` works:**
331
+ - **Buffer** → Creates a `SharedArrayBuffer` copy, returns new `Buffer` instance
332
+ - **SharedArrayBuffer** → Returns as-is (already shared)
333
+ - **typed-struct classes** → Creates new instance with `SharedArrayBuffer`
334
+ - **User classes** → Recursively converts fields marked with `@TransportType()` **in-place**
335
+ - **Arrays** → Converts each element in-place
336
+ - Built-in types (Date, RegExp, etc.) → Not supported, returns as-is
337
+
338
+ **Field conversion rules:**
339
+ - Only converts fields with `@TransportType()` decorator
340
+ - Or fields with `design:type` metadata (when `emitDecoratorMetadata` is enabled)
341
+ - Skips fields with manual encoders (`@TransportEncoder()`)
342
+ - Skips built-in types
343
+
344
+ **Important notes:**
345
+ - For `typed-struct` classes and `Buffer`, `toShared()` returns a **new instance** (cannot modify in-place)
346
+ - For user classes, `toShared()` modifies fields **in-place** (the object itself is the same, but its fields may be replaced)
347
+ - The object must contain at least one shared memory segment, otherwise it's returned unchanged
348
+ - Use `@TransportType()` to mark fields that should be recursively converted
349
+
157
350
  ## Custom Class Transport
158
351
 
159
352
  By default, `worker_threads` can only pass serializable data (primitives, plain objects, `Buffer`, etc.). For custom classes, `yuzuthread` provides transport decorators to automatically serialize and deserialize instances.
@@ -309,6 +502,110 @@ Encoders support async operations:
309
502
  - **Plain objects** - passed as-is
310
503
  - **Custom classes** - require `@TransportType()` or `@TransportEncoder()`
311
504
 
505
+ ### Typed Struct Classes
506
+
507
+ Classes that extend `typed-struct` are automatically detected and handled with special transport logic. The library separates struct fields (stored in the buffer) from regular fields:
508
+
509
+ ```ts
510
+ import { Struct } from 'typed-struct';
511
+ import { DefineWorker, WorkerMethod, TransportType, initWorker } from 'yuzuthread';
512
+
513
+ const Base = new Struct('DataBase')
514
+ .UInt8('id')
515
+ .UInt32LE('counter')
516
+ .compile();
517
+
518
+ class MyData extends Base {
519
+ declare id: number;
520
+ declare counter: number;
521
+ extraField: string = '';
522
+ nestedData?: { name: string };
523
+ }
524
+
525
+ @DefineWorker()
526
+ class StructWorker {
527
+ @WorkerMethod()
528
+ @TransportType(() => MyData)
529
+ async processData(
530
+ @TransportType(() => MyData) data?: MyData,
531
+ ): Promise<MyData> {
532
+ const result = new MyData();
533
+ if (data) {
534
+ result.id = data.id;
535
+ result.counter = data.counter + 1;
536
+ result.extraField = data.extraField + ' processed';
537
+ result.nestedData = data.nestedData;
538
+ } else {
539
+ result.id = 1;
540
+ result.counter = 0;
541
+ result.extraField = 'new';
542
+ }
543
+ return result;
544
+ }
545
+ }
546
+
547
+ const worker = await initWorker(StructWorker);
548
+ const data = await worker.processData();
549
+ console.log(data.id); // -> 1
550
+ console.log(data.counter); // -> 0
551
+ console.log(data.extraField); // -> 'new'
552
+ ```
553
+
554
+ **How it works:**
555
+ - When encoding, the library dumps the struct buffer and encodes non-struct fields separately
556
+ - When decoding, it creates a new instance with the buffer, then restores non-struct fields
557
+ - All struct fields (defined by `typed-struct`) are preserved in the buffer
558
+ - Additional class fields are transported using the standard transport logic
559
+
560
+ **Expected usage pattern:**
561
+ ```ts
562
+ class SomeDataClass extends new Struct()...compile() {
563
+ // Struct fields (declare them for TypeScript)
564
+ declare structField1: number;
565
+ declare structField2: number;
566
+
567
+ // Other fields (will be transported separately)
568
+ otherField: string = '';
569
+ }
570
+ ```
571
+
572
+ **Mixed scenarios with transport decorators:**
573
+
574
+ Typed-struct classes can use `@TransportType()` and `@TransportEncoder()` on their non-struct fields:
575
+
576
+ ```ts
577
+ class ComplexData extends Base {
578
+ declare value: number; // struct field
579
+ declare count: number; // struct field
580
+
581
+ @TransportType(() => MyData)
582
+ nested?: MyData; // non-struct field with custom class
583
+
584
+ @TransportEncoder(
585
+ (date: Date) => date.toISOString(),
586
+ (str: string) => new Date(str),
587
+ )
588
+ timestamp?: Date; // non-struct field with custom encoder
589
+ }
590
+ ```
591
+
592
+ Regular classes can have typed-struct fields:
593
+
594
+ ```ts
595
+ class WrapperClass {
596
+ @TransportType(() => MyStructData)
597
+ data!: MyStructData; // typed-struct class field
598
+
599
+ label: string = ''; // regular field
600
+ }
601
+ ```
602
+
603
+ All combinations work seamlessly - the transport system automatically handles:
604
+ - Typed-struct classes with decorated non-struct fields
605
+ - Regular classes with typed-struct fields
606
+ - Arrays of any of the above
607
+ - Nested structures of any depth
608
+
312
609
  ### Notes on Transport
313
610
 
314
611
  - `@TransportType()` can be used without arguments to enable `emitDecoratorMetadata` without registering metadata
@@ -434,27 +731,53 @@ Event handlers run on the main thread and can access the main-thread instance st
434
731
  - supports async operations
435
732
  - works as `PropertyDecorator`, `MethodDecorator`, and `ParameterDecorator`
436
733
 
734
+ #### Shared Memory
735
+
736
+ - `Shared(factory?: () => Type)`
737
+ - marks constructor parameter to use shared memory
738
+ - factory function `() => Type` is optional (inferred from `design:paramtypes` if omitted)
739
+ - parameter type must contain shared memory segments (`typed-struct`, `Buffer`, `SharedArrayBuffer`)
740
+ - throws error if type has no shared memory segments
741
+ - works as `ParameterDecorator` (constructor parameters only)
742
+ - automatically converts parameter to use `SharedArrayBuffer` during worker initialization
743
+ - both main thread and worker thread share the same memory
744
+
437
745
  ### Functions
438
746
 
439
747
  - `initWorker(cls, ...args)`
440
748
  - creates a persistent worker and returns instance with `finalize(): Promise<void>` and `workerStatus(): WorkerStatus`
749
+ - automatically handles `@Shared` constructor parameters
750
+ - preserves prototype chain for custom class constructor parameters
441
751
  - `runInWorker(cls, cb, ...args)`
442
752
  - one-time worker execution with automatic finalize
753
+ - same constructor parameter handling as `initWorker`
754
+ - `toShared(obj)`
755
+ - converts object to use shared memory
756
+ - returns new instance for `typed-struct` classes
757
+ - modifies in-place for user classes (updates fields)
758
+ - creates `SharedArrayBuffer` copy for `Buffer`
759
+ - returns as-is for `SharedArrayBuffer`
760
+ - recursively processes fields with `@TransportType()` or `design:type` metadata
443
761
 
444
762
  ### Types
445
763
 
446
764
  - `WorkerStatus`
447
765
  - enum for worker status states
766
+ - values: `Initializing`, `Ready`, `InitError`, `WorkerError`, `Exited`, `Finalized`
448
767
  - `WorkerInstance<T>`
449
768
  - type for worker instance with `finalize()` and `workerStatus()` methods
450
769
  - `WorkerEventName`
451
770
  - type for worker event names, matches `Worker.on()` event parameter
771
+ - includes: `'error'`, `'exit'`, `'online'`, `'message'`, `'messageerror'`
452
772
  - `Awaitable<T>`
453
773
  - type for value that can be sync or async: `T | Promise<T>`
454
774
  - `TransportTypeFactory`
455
775
  - type for transport type factory: `() => Class | [Class]`
456
776
  - `TransportEncoderType<T, U>`
457
777
  - type for custom encoder/decoder object
778
+ - `SharedTypeFactory`
779
+ - type for shared type factory: `() => Class`
780
+ - used with `@Shared()` decorator
458
781
 
459
782
  ## Notes
460
783
 
@@ -464,3 +787,122 @@ Event handlers run on the main thread and can access the main-thread instance st
464
787
  - For `@TransportType()` to automatically infer types from TypeScript metadata, enable `emitDecoratorMetadata` in `tsconfig.json`.
465
788
  - Transport decorators work with both `@WorkerMethod()` and `@WorkerCallback()`.
466
789
  - Custom class transport preserves the prototype chain and method definitions.
790
+
791
+ ### Constructor Parameter Transport
792
+
793
+ Constructor parameters passed to `initWorker()` are automatically transported with prototype preservation:
794
+
795
+ ```ts
796
+ class Config {
797
+ @TransportType(() => Date)
798
+ createdAt: Date;
799
+
800
+ constructor(public name: string) {
801
+ this.createdAt = new Date();
802
+ }
803
+ }
804
+
805
+ @DefineWorker()
806
+ class ConfigWorker {
807
+ constructor(
808
+ public config: Config, // No decorator needed if emitDecoratorMetadata is enabled
809
+ ) {}
810
+
811
+ @WorkerMethod()
812
+ getConfigName() {
813
+ return this.config.name.toUpperCase();
814
+ }
815
+ }
816
+
817
+ const config = new Config('app');
818
+ const worker = await initWorker(ConfigWorker, [config]);
819
+
820
+ // config methods are preserved in worker
821
+ const name = await worker.getConfigName();
822
+ console.log(name); // -> "APP"
823
+ ```
824
+
825
+ - Constructor parameters with custom classes are automatically transported
826
+ - Prototype chain is preserved using `@TransportType()` or `design:paramtypes` metadata
827
+ - Works the same way as method parameters and return values
828
+ - `@Shared` parameters are converted first, then transported
829
+
830
+ ### Circular Reference Detection
831
+
832
+ The library detects and prevents circular references in both transport and shared memory:
833
+
834
+ **Transport:**
835
+ ```ts
836
+ class Node {
837
+ @TransportType(() => Node)
838
+ next?: Node;
839
+ }
840
+
841
+ const node1 = new Node();
842
+ const node2 = new Node();
843
+ node1.next = node2;
844
+ node2.next = node1; // Circular reference
845
+
846
+ await worker.processNode(node1); // Throws: "Circular reference detected"
847
+ ```
848
+
849
+ **Shared memory:**
850
+ ```ts
851
+ class CircularContainer {
852
+ @TransportType(() => SharedData)
853
+ data!: SharedData;
854
+ }
855
+
856
+ const container: any = new Container();
857
+ container.data = new SharedData();
858
+ container.self = container; // Circular reference
859
+
860
+ toShared(container); // Throws: "Circular reference detected"
861
+ ```
862
+
863
+ Circular references in type hierarchies are also detected:
864
+ ```ts
865
+ class CircularA {
866
+ @TransportType(() => CircularB)
867
+ b?: CircularB;
868
+ }
869
+
870
+ class CircularB {
871
+ @TransportType(() => CircularA)
872
+ a?: CircularA;
873
+ }
874
+
875
+ // Throws when scanning metadata or attempting to transport
876
+ ```
877
+
878
+ ### SharedArrayBuffer Support
879
+
880
+ `SharedArrayBuffer` is automatically detected and handled in transport:
881
+
882
+ ```ts
883
+ @DefineWorker()
884
+ class SharedBufferWorker {
885
+ @WorkerMethod()
886
+ incrementBuffer(
887
+ @TransportType(() => Buffer) buffer: Buffer,
888
+ ) {
889
+ buffer[0]++;
890
+ return buffer[0];
891
+ }
892
+ }
893
+
894
+ const sab = new SharedArrayBuffer(10);
895
+ const buffer = Buffer.from(sab);
896
+ buffer[0] = 100;
897
+
898
+ const worker = await initWorker(SharedBufferWorker);
899
+
900
+ // Buffer backed by SharedArrayBuffer is shared
901
+ await worker.incrementBuffer(buffer);
902
+ console.log(buffer[0]); // -> 101 (updated by worker)
903
+ ```
904
+
905
+ - `Buffer` backed by `SharedArrayBuffer` is automatically detected
906
+ - No copy is made - the same memory is shared
907
+ - Works with both `@TransportType()` and `@Shared()`
908
+ - `SharedArrayBuffer` can be passed directly as a parameter type