Skip to content

Commit 7fa1b39

Browse files
committed
add basic structure and logic
0 parents  commit 7fa1b39

File tree

9 files changed

+365
-0
lines changed

9 files changed

+365
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
lib

example/main-process.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import child_process from 'child_process'
2+
import IPOS from '../lib/main.js'
3+
4+
const sharedObject = IPOS.new()
5+
sharedObject.create('exampleArray', [])
6+
sharedObject.exampleArray.push('hello')
7+
8+
const subProcess = child_process.spawn('node', ['sub-process.js'], {
9+
stdio: ['inherit', 'inherit', 'inherit', 'ipc']
10+
})
11+
12+
sharedObject.addProcess(subProcess)
13+
sharedObject.create('exampleObject', {})
14+
sharedObject.exampleObject.from = 'the other side'
15+
16+
console.log(sharedObject.exampleArray, sharedObject.exampleObject)

example/sub-process.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import IPOS from '../lib/main.js'
2+
3+
const sharedObject = IPOS.new()
4+
console.log(sharedObject.exampleArray)

package-lock.json

Lines changed: 50 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "inter-process-object-sharing",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"type": "module",
7+
"scripts": {
8+
"build" : "tsc",
9+
"watch" : "tsc --watch"
10+
},
11+
"keywords": [],
12+
"author": "",
13+
"license": "ISC",
14+
"devDependencies": {
15+
"@types/node": "^18.7.18",
16+
"typescript": "^4.8.3"
17+
}
18+
}

src/array.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
export default class InterceptedArray<T> extends Array {
2+
private callback
3+
4+
// intercepts only methods that change the object
5+
constructor(callback: (object: object, method: string, ...args: any) => void) {
6+
super();
7+
this.callback = callback
8+
}
9+
10+
copyWithin(target: number, start: number, end?: number): this {
11+
this.callback(this, 'copyWithin', target, start, end)
12+
return super.copyWithin(target, start, end)
13+
}
14+
15+
fill(value: T, start?: number, end?: number): this {
16+
this.callback(this, 'fill', value, start, end)
17+
return super.fill(value, start, end)
18+
}
19+
20+
pop(): T | undefined {
21+
this.callback(this, 'pop')
22+
return super.pop()
23+
}
24+
25+
push(...items: T[]): number {
26+
this.callback(this, 'push', ...items)
27+
return super.push(...items)
28+
}
29+
30+
reverse(): T[] {
31+
this.callback(this, 'reverse')
32+
return super.reverse()
33+
}
34+
35+
shift(): T | undefined {
36+
this.callback(this, 'shift')
37+
return super.shift()
38+
}
39+
40+
sort(compareFn?: (a: T, b: T) => number): this {
41+
this.callback(this, 'sort', compareFn)
42+
return super.sort(compareFn)
43+
}
44+
45+
splice(start: number, deleteCount?: number): T[] {
46+
this.callback(this, 'splice', start, deleteCount)
47+
return super.splice(start, deleteCount)
48+
}
49+
50+
unshift(...items: T[]): number {
51+
this.callback(this, 'unshift', ...items)
52+
return super.unshift(...items)
53+
}
54+
}

src/main.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import {deserialize, serialize} from 'v8'
2+
import {ChildProcess} from 'child_process'
3+
import InterceptedArray from './array.js'
4+
import IPOSMessaging from './messaging.js'
5+
6+
export default class IPOS {
7+
private fields: Map<string, object>
8+
private fieldsReverseMap: Map<object, string>
9+
private processes: Set<ChildProcess>
10+
private processMessagingMap: Map<ChildProcess, IPOSMessaging>
11+
private readonly proxy
12+
private messaging?: IPOSMessaging;
13+
14+
static new(): IPOS {
15+
return new IPOS()
16+
}
17+
18+
constructor() {
19+
this.fields = new Map()
20+
this.fieldsReverseMap = new Map()
21+
this.processes = new Set()
22+
this.processMessagingMap = new Map()
23+
24+
// proxy makes all "target.fields" available as "actual" fields
25+
this.proxy = new Proxy(this, {
26+
get(target, name: string) {
27+
if (Reflect.has(target, name)) {
28+
return Reflect.get(target, name)
29+
} else if (target.fields.has(name)) {
30+
return target.fields.get(name)
31+
}
32+
}
33+
})
34+
// was called on child process
35+
if (process.send) {
36+
this.messaging?.listenForType('sync', message => {
37+
console.log(message)
38+
if (message.fields)
39+
this.fields = deserialize(message.fields)
40+
})
41+
42+
// register with parent process
43+
this.messaging = new IPOSMessaging(process)
44+
this.messaging.send('register')
45+
}
46+
return this.proxy
47+
}
48+
49+
public create(key: string, value: object): void {
50+
if (Array.isArray(value)) {
51+
value = new InterceptedArray((object, method, ...args) =>
52+
this.sendMethodCall(object, method, ...args)
53+
)
54+
}
55+
/*Object.getOwnPropertyNames(Object.getPrototypeOf(value))
56+
.filter(methodName => !(methodName.startsWith('__') && methodName.endsWith('__')))
57+
.forEach(methodName => {
58+
if (!value[methodName]) return
59+
const method = value[methodName]
60+
value[methodName] = function (...args: any) {
61+
// interception
62+
console.log(methodName)
63+
method.call(value, ...args)
64+
}
65+
})
66+
*/
67+
this.fields.set(key, value)
68+
this.fieldsReverseMap.set(value, key)
69+
// todo: send update message
70+
}
71+
72+
public delete(key: string): boolean {
73+
return this.fields.delete(key)
74+
// todo: send update message
75+
}
76+
77+
public addProcess(process: ChildProcess) {
78+
if (!process.send)
79+
throw new Error(`Process must have an ipc channel. Activate by passing "stdio: [<stdin>, <stdout>, <stderr>, 'ipc']" as an option.`)
80+
const messaging = new IPOSMessaging(process)
81+
82+
let registered = false
83+
messaging.listenForType('register', () => {
84+
if (registered) return
85+
registered = true
86+
87+
this.processes.add(process)
88+
this.processMessagingMap.set(process, messaging)
89+
this.syncProcess(process)
90+
})
91+
}
92+
93+
private syncProcess(process: ChildProcess) {
94+
console.log('sending sync')
95+
this.processMessagingMap.get(process)?.send('sync', {
96+
field: serialize(this.fields)
97+
})
98+
}
99+
100+
private sendMethodCall(object: object, method: string, ...args: any) {
101+
this.processMessagingMap.forEach(processMessaging => {
102+
processMessaging.send('update', {
103+
do: method,
104+
on: this.fieldsReverseMap.get(object),
105+
with: serialize(args)
106+
})
107+
})
108+
}
109+
110+
// serializes types, that "JSON.stringify()" doesn't properly handle
111+
/*private static serialize(value: any): any | void {
112+
// todo: handle other builtins
113+
if (['string', 'number'].includes(typeof value)) {
114+
return value
115+
} else if (typeof value === 'function') {
116+
return value.toString()
117+
} else if (Array.isArray(value)) {
118+
return value.map(serialize)
119+
} else if (value.constructor === {}.constructor) {
120+
return Object.fromEntries(
121+
Array.from(
122+
Object.entries(value)
123+
.map(([key, value]) =>
124+
[key, this.serialize(value)]
125+
)
126+
)
127+
)
128+
} else {
129+
if (!value.stringify && !value.serialize)
130+
throw new Error(
131+
`Class: \`${value.constructor.name}\` must have methods to serialize and deserialize objects. (\`.stringify()\`, \`.serialize()\`)`
132+
)
133+
// return value.toString()
134+
}
135+
}*/
136+
}

src/messaging.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {ChildProcess} from 'child_process'
2+
3+
type iposMessagingType = 'ready' | 'register' | 'update' | 'sync'
4+
type iposMessagingCallback = (message: iposMessagingMessage) => (any | void)
5+
type iposMessagingMessage = {
6+
protocol: 'ipos',
7+
type: iposMessagingType,
8+
fields?: Buffer
9+
}
10+
11+
const mustHaveSendError = new Error(`Process must have a \`.send()\` method.`)
12+
13+
export default class IPOSMessaging {
14+
private listeners: Map<iposMessagingType | 'any', Array<iposMessagingCallback>>
15+
private process: ChildProcess | NodeJS.Process
16+
17+
constructor(process: ChildProcess | NodeJS.Process) {
18+
this.listeners = new Map()
19+
if (!process.send) throw mustHaveSendError
20+
this.process = process
21+
this.process.on('message', (message: iposMessagingMessage) => {
22+
try {
23+
if (message.protocol !== 'ipos')
24+
return
25+
26+
if (message.type === 'ready') {
27+
this.send('register')
28+
return
29+
}
30+
31+
if (this.listeners.has('any')) {
32+
this.listeners.get('any')
33+
?.forEach(callback => callback(message))
34+
}
35+
if (this.listeners.has(message.type)) {
36+
this.listeners.get(message.type)
37+
?.forEach(callback => callback(message))
38+
}
39+
} catch (e) {
40+
// not a message from ipos
41+
}
42+
})
43+
44+
// if the current process is a parent process
45+
if (process instanceof ChildProcess) {
46+
// send a "ready" message to receive another "register" (if an instance is initiated)
47+
this.send('ready')
48+
}
49+
}
50+
51+
send(type: iposMessagingType, data?: {}) {
52+
if (!this.process.send) throw mustHaveSendError
53+
this.process.send({
54+
protocol: 'ipos',
55+
type,
56+
...data
57+
})
58+
}
59+
60+
listenForType(type: iposMessagingType | 'any', callback: iposMessagingCallback) {
61+
let callbacks: Array<iposMessagingCallback> = this.listeners.get(type) ?? []
62+
callbacks.push(callback)
63+
this.listeners.set(type, callbacks)
64+
}
65+
66+
listenForAll(callback: iposMessagingCallback) {
67+
this.listenForType('any', callback)
68+
}
69+
}

tsconfig.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"compilerOptions": {
3+
"lib": ["ESNext"],
4+
"module": "esnext",
5+
"target": "esnext",
6+
7+
"declaration": true,
8+
9+
"outDir": "./lib",
10+
"strict": true,
11+
12+
"allowJs": true,
13+
"allowSyntheticDefaultImports": true
14+
},
15+
"include": ["src"],
16+
"exclude": ["node_modules"]
17+
}

0 commit comments

Comments
 (0)