Skip to content

Commit 0ec46b4

Browse files
committed
add support for class de-/serialistion + tests #1
1 parent 0d31680 commit 0ec46b4

File tree

5 files changed

+142
-8
lines changed

5 files changed

+142
-8
lines changed

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,61 @@ See [`example/main-process.js`](https://github.com/drinking-code/inter-process-o
4545
and [`example/sub-process.js`](https://github.com/drinking-code/inter-process-object-sharing/blob/main/example/sub-process.js)
4646
.
4747

48+
## A note on class instances
49+
50+
To synchronise class instances, you first have to register the class with ipos _on each process_ (on which the class
51+
instance does not yet exist) before the synchronisation happens. That means if you want to connect two IPOS instances,
52+
and the instance on the main process has a class instance somewhere inside a field, this class type must be registered
53+
on the subprocess, before calling `IPOS.new()`. For IPOS to be able to transmit the class instance it has to have
54+
methods to serialise (turn the class instance into either a string, a number, an object, an array, a map, or a set) and
55+
deserialize (turn the serialised value back into a class instance). IPOS will look for `.serialize()`, or `.stringify()`
56+
for serialisation and `.from()` for de-serialisation, but you can specify custom methods / functions. Here is an
57+
example:
58+
59+
```javascript
60+
// example-class.js
61+
62+
export class Example {
63+
data;
64+
65+
constructor(data) {
66+
this.data = data
67+
}
68+
69+
serialize() {
70+
return this.data
71+
}
72+
73+
static from(data) {
74+
return new Example(data)
75+
}
76+
}
77+
```
78+
79+
```javascript
80+
// main-process.js
81+
82+
import IPOS from 'ipos'
83+
84+
const exampleInstance = new Example('myValue')
85+
const ipos = IPOS.new()
86+
ipos.create('myClassInstance', exampleInstance)
87+
const subProcess = child_process.spawn('node', ['sub-process.js'], {
88+
stdio: ['inherit', 'inherit', 'inherit', 'ipc']
89+
})
90+
await sharedObject.addProcess(subProcess)
91+
```
92+
93+
```javascript
94+
// sub-process.js
95+
96+
import IPOS from 'ipos'
97+
import {Example} from './example-class.js'
98+
99+
const ipos = IPOS.registerClass(Example)
100+
const ipos = await IPOS.new()
101+
```
102+
48103
## `IPOS()`
49104

50105
The main class. Don't use the `new` keyword (when creating an instance in a subprocess). Instead, use the

src/__test__/synchronize.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('Synchronising fields', () =>
1717
await Promise.all([
1818
// @ts-ignore Argument of type 'subProcessIPCLoopback' is not assignable to parameter of type 'ChildProcess'
1919
main_ipos.addProcess(sub_process),
20-
(async () => sub_ipos = await IPOS.new())()
20+
expect((async () => sub_ipos = await IPOS.new())()).resolves.not.toThrow()
2121
])
2222

2323
// @ts-ignore Variable 'sub_ipos' is used before being assigned.

src/__test__/testData.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
1+
import IPOS from '../main'
2+
3+
class TestClass {
4+
data: any
5+
6+
constructor(data: any) {
7+
this.data = data
8+
}
9+
10+
stringify() {
11+
return this.data
12+
}
13+
14+
static from(data: any) {
15+
return new TestClass(data)
16+
}
17+
}
18+
19+
IPOS.registerClass(TestClass)
20+
121
export const examples = {
222
'string': 'myString',
323
'number': 42,
@@ -12,6 +32,7 @@ export const examples = {
1232
})),
1333
'set': new Set(['myItem', 42]),
1434
'function': (a: number, b: number) => a + b,
35+
'class': new TestClass('myClass')
1536
}
1637

1738
// exemplary; don't iterate every possible method, just do one direct value assignment and one method

src/main.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {ChildProcess} from 'child_process'
22
import initChild from './init-child.js'
33
import IPOSMessaging, {iposMessagingMessage, iposMessagingType} from './messaging.js'
44
import intercept from './intercept.js'
5+
import {classes, deSerialize, SerializableType, SerializedType} from "./serialize";
56

67
export default class IPOS {
78
private readonly fields: Map<string, any>
@@ -58,6 +59,15 @@ export default class IPOS {
5859
return this.proxy
5960
}
6061

62+
static registerClass(
63+
constructor: Function,
64+
serialize?: (value?: object) => SerializableType,
65+
deserialize?: (value: SerializedType) => object
66+
) {
67+
classes.set(constructor.name, constructor)
68+
deSerialize.set(constructor, {serialize, deserialize})
69+
}
70+
6171
/****************** MESSAGING *******************/
6272
protected mountListeners(messaging: IPOSMessaging) {
6373
messaging.listenForType('update', (message) => this.performUpdate(message))

src/serialize.ts

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,47 @@
1+
function isNativeObject(value: object) {
2+
return /^\[object .+?Constructor]$/.test(Object.prototype.toString.call(value))
3+
}
4+
5+
export const classes: Map<string, Function> = new Map()
6+
export const deSerialize: Map<Function, { serialize?: (value?: object) => SerializableType, deserialize?: (value: SerializedType) => object }> = new Map()
7+
8+
export type SerializedType =
9+
string
10+
| number
11+
| boolean
12+
| null
13+
| undefined
14+
| Array<SerializedType>
15+
| { [k: string]: SerializedType }
16+
17+
export type SerializableType =
18+
string
19+
| number
20+
| boolean
21+
| null
22+
| undefined
23+
| Function
24+
| Array<SerializableType>
25+
| { [k: string]: SerializableType }
26+
| Set<SerializableType>
27+
| Map<SerializableType, SerializableType>
28+
129
export function serialize(value: any): any | void {
230
// todo: handle other builtins
331
if (['string', 'number', 'boolean'].includes(typeof value) || !value) {
432
return value
5-
} else if (typeof value === 'function') {
6-
return {
7-
$$iposType: 'Function',
8-
data: value.toString()
9-
}
1033
} else if (Array.isArray(value)) {
1134
return value.map(v => serialize(v))
1235
} else if (value.constructor === {}.constructor || value.valueOf().constructor === {}.constructor) {
1336
return Object.fromEntries(
1437
Array.from(Object.entries(value))
1538
.map(([key, value]) => [key, serialize(value)])
1639
)
40+
} else if (typeof value === 'function') {
41+
return {
42+
$$iposType: 'Function',
43+
data: value.toString()
44+
}
1745
} else if (value instanceof Set) {
1846
return {
1947
$$iposType: 'Set',
@@ -28,12 +56,21 @@ export function serialize(value: any): any | void {
2856
.map(([key, value]) => [key, serialize(value)])
2957
)
3058
}
59+
} else if (isNativeObject(value)) {
60+
throw new Error(`Could not serialise: \`${value.constructor.name}\`.`)
3161
} else {
32-
if (!value.stringify && !value.serialize)
62+
const serializeMethod = deSerialize.get(value.constructor)?.serialize ?? value.stringify ?? value.serialize
63+
if (!serializeMethod)
3364
throw new Error(
3465
`Class: \`${value.constructor.name}\` must have methods to serialize and deserialize objects. (\`.stringify()\`, \`.serialize()\`)`
3566
)
36-
// return value.toString()
67+
68+
return {
69+
$$iposType: value.constructor.name,
70+
data: serialize(
71+
serializeMethod.call(value, value)
72+
)
73+
}
3774
}
3875
}
3976

@@ -64,6 +101,17 @@ export function deserialize(value: string | number | Array<any> | { $$iposType?:
64101
Array.from(Object.entries(value.data))
65102
.map(deserialize)
66103
)
104+
} else if (classes.has(value.$$iposType)) {
105+
const constructor = classes.get(value.$$iposType) as Function
106+
const deserializeMethod = deSerialize.get(constructor)?.deserialize ??
107+
(constructor as unknown as { from: Function })?.from
108+
109+
if (!deserializeMethod)
110+
throw new Error(`Did not recognize type \`${value.$$iposType}\`. Did you register it in the child process?`)
111+
112+
return deserializeMethod(deserialize(value.data))
113+
} else {
114+
throw new Error(`Did not recognize type \`${value.$$iposType}\`. Did you register it in the child process?`)
67115
}
68116
} else
69117
console.warn('I don\'t know', value)

0 commit comments

Comments
 (0)