Skip to content

Commit 300342e

Browse files
committed
Support exposing generator functions
1 parent d2f734d commit 300342e

File tree

7 files changed

+126
-4
lines changed

7 files changed

+126
-4
lines changed

src/serializers/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { MessageRelay } from "../types/common"
33
import { JsonSerializable, Serializer, SerializerImplementation } from "../types/serializers"
44
import { isSerializedCallback, DefaultCallbackSerializer } from "./callbacks"
55
import { isSerializedError, DefaultErrorSerializer } from "./errors"
6+
import { isIterator, isSerializedIterator, DefaultIteratorSerializer } from "./iterators"
67

78
export {
89
JsonSerializable,
@@ -36,6 +37,8 @@ export const DefaultSerializer = (): Serializer<JsonSerializable> => {
3637
return errorSerializer.deserialize(message, sender)
3738
} else if (isSerializedCallback(message)) {
3839
return callbackSerializer.deserialize(message, sender)
40+
} else if (isSerializedIterator(message)) {
41+
return iteratorSerializer.deserialize(message, sender)
3942
} else {
4043
return message
4144
}
@@ -45,6 +48,8 @@ export const DefaultSerializer = (): Serializer<JsonSerializable> => {
4548
return errorSerializer.serialize(input) as any as JsonSerializable
4649
} else if (isCallback(input)) {
4750
return callbackSerializer.serialize(input) as any as JsonSerializable
51+
} else if (isIterator(input)) {
52+
return iteratorSerializer.serialize(input) as any as JsonSerializable
4853
} else {
4954
return input
5055
}
@@ -53,6 +58,7 @@ export const DefaultSerializer = (): Serializer<JsonSerializable> => {
5358

5459
const callbackSerializer = DefaultCallbackSerializer(serializer)
5560
const errorSerializer = DefaultErrorSerializer()
61+
const iteratorSerializer = DefaultIteratorSerializer(serializer)
5662

5763
return serializer
5864
}

src/serializers/iterators.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import DebugLogger from "debug"
2+
import { createProxyFunction } from "../common/call-proxy"
3+
import { Callback, RemoteCallback } from "../common/callbacks"
4+
import { MessageRelay } from "../types/common"
5+
import { SerializedIterator, Serializer } from "../types/serializers"
6+
7+
const debug = DebugLogger("threads:callback:messages")
8+
9+
export const DefaultIteratorSerializer = (rootSerializer: Serializer): Serializer<SerializedIterator, Iterator<any> | AsyncIterator<any>, AsyncIterator<any>> => ({
10+
deserialize(message: SerializedIterator, origin: MessageRelay): AsyncIterator<any> & AsyncIterable<any> {
11+
const remoteNext = createProxyFunction<[], IteratorResult<any>>(origin, rootSerializer, message.next_fid, debug)
12+
const remoteCallback = RemoteCallback<() => Promise<IteratorResult<any>>>(remoteNext)
13+
14+
const next = async () => {
15+
const result = await remoteCallback()
16+
if (result.done) {
17+
remoteCallback.release()
18+
}
19+
return result
20+
}
21+
22+
const asyncIterator = {
23+
[Symbol.asyncIterator]: () => asyncIterator,
24+
next
25+
}
26+
return asyncIterator
27+
},
28+
serialize(iter: Iterator<any> | AsyncIterator<any>): SerializedIterator {
29+
const next = Callback(async () => {
30+
const result = await iter.next()
31+
if (result.done) {
32+
next.release()
33+
}
34+
return result
35+
})
36+
return {
37+
__iterator_marker: "$$iterator",
38+
next_fid: next.id
39+
}
40+
}
41+
})
42+
43+
export const isIterator = (thing: any): thing is Iterator<any> | AsyncIterator<any> =>
44+
thing && typeof thing === "object" && "next" in thing && typeof thing.next === "function"
45+
46+
export const isSerializedIterator = (thing: any): thing is SerializedIterator =>
47+
thing && typeof thing === "object" && "__iterator_marker" in thing && thing.__iterator_marker === "$$iterator"

src/types/master.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,19 @@ export type StripAsync<Type> =
2828
? ObservableBaseType
2929
: Type
3030

31+
export type AsyncifyIterator<Type> =
32+
Type extends Iterator<infer T>
33+
? AsyncIterator<T> & AsyncIterable<T>
34+
: Type extends AsyncIterator<infer T2>
35+
? AsyncIterator<T2> & AsyncIterable<T2>
36+
: Type
37+
3138
export type ModuleMethods = { [methodName: string]: (...args: any) => any }
3239

3340
export type ProxyableFunction<Args extends any[], ReturnType> =
3441
Args extends []
35-
? () => ObservablePromise<StripAsync<ReturnType>>
36-
: (...args: Args) => ObservablePromise<StripAsync<ReturnType>>
42+
? () => ObservablePromise<AsyncifyIterator<StripAsync<ReturnType>>>
43+
: (...args: Args) => ObservablePromise<AsyncifyIterator<StripAsync<ReturnType>>>
3744

3845
export type ModuleProxy<Methods extends ModuleMethods> = {
3946
[method in keyof Methods]: ProxyableFunction<Parameters<Methods[method]>, ReturnType<Methods[method]>>

src/types/serializers.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ export type JsonSerializable =
1717
| JsonSerializableObject
1818
| JsonSerializableObject[]
1919

20-
export interface Serializer<Msg = JsonSerializable, Input = any> {
21-
deserialize(message: Msg, sender: MessageRelay | null): Input
20+
export interface Serializer<Msg = JsonSerializable, Input = any, Deserialized = Input> {
21+
deserialize(message: Msg, sender: MessageRelay | null): Deserialized
2222
serialize(input: Input): Msg
2323
}
2424

@@ -38,3 +38,8 @@ export interface SerializedError {
3838
name: string
3939
stack?: string
4040
}
41+
42+
export interface SerializedIterator {
43+
__iterator_marker: "$$iterator"
44+
next_fid: number
45+
}

test/iterators.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import test from "ava"
2+
import { spawn, Callback, Thread, Worker } from "../src/index"
3+
import { AsyncGenerator } from "./workers/async-generator"
4+
import { Generator } from "./workers/generator"
5+
6+
test("can use a generator function exposed by a worker", async t => {
7+
const generate = await spawn<Generator>(new Worker("./workers/generator"))
8+
9+
try {
10+
const results: number[] = []
11+
12+
for await (const i of await generate(3)) {
13+
results.push(i)
14+
}
15+
16+
t.deepEqual(results, [1, 2, 3])
17+
} finally {
18+
await Thread.terminate(generate)
19+
}
20+
})
21+
22+
test("can use an async generator function exposed by a worker", async t => {
23+
const generate = await spawn<AsyncGenerator>(new Worker("./workers/async-generator"))
24+
25+
try {
26+
const results: number[] = []
27+
28+
for await (const i of await generate(3)) {
29+
results.push(i)
30+
}
31+
32+
t.deepEqual(results, [1, 2, 3])
33+
} finally {
34+
await Thread.terminate(generate)
35+
}
36+
})

test/workers/async-generator.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { expose } from "../../src/worker"
2+
3+
export type AsyncGenerator = (count: number) => AsyncIterator<number>
4+
5+
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
6+
7+
expose(async function* generator(count: number) {
8+
for (let i = 1; i <= count; i++) {
9+
await delay(2)
10+
yield i
11+
}
12+
})

test/workers/generator.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { expose } from "../../src/worker"
2+
3+
export type Generator = (count: number) => Iterator<number>
4+
5+
expose(function *generator(count: number) {
6+
for (let i = 1; i <= count; i++) {
7+
yield i
8+
}
9+
})

0 commit comments

Comments
 (0)