Skip to content

Commit ed728c6

Browse files
committed
added setting and deleting messaging; new interception
1 parent 0bb0eec commit ed728c6

File tree

7 files changed

+144
-135
lines changed

7 files changed

+144
-135
lines changed

example/sub-process.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ import IPOS from '../lib/main.js'
22

33
const sharedObject = await IPOS.new()
44
console.log('sharedObject.exampleArray', sharedObject.exampleArray)
5-
// console.log(sharedObject.exampleArray[0], /*...sharedObject.exampleObject.entries()*/)
5+
setTimeout(() =>
6+
console.log(sharedObject.exampleArray[0], /*...*/sharedObject.exampleObject/*.entries()*/),
7+
10)

src/init-child.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,23 @@
1-
import IPOSMessaging from './messaging.js'
21
import IPOS from './main.js'
32

43
export default function initChild(this: IPOS) {
54
let resolve: Function
65
const promise = new Promise(res => resolve = res)
76

8-
this.messaging = new IPOSMessaging(process)
9-
this.messaging.listenForType('sync', message => {
7+
this.messaging?.listenForType('sync', message => {
108
if (!message.fields) return
11-
Object.entries(
12-
IPOS.deserialize(JSON.parse(message.fields))
13-
)
9+
Object.entries(message.fields)
1410
.map(([key, value]: [string, any]) => {
1511
this.createStealthy(key, value)
1612
})
13+
this.messaging?.send('sync_ok')
1714
resolve()
1815
})
1916
// register with parent process
20-
this.messaging.send('register')
21-
this.messaging.listenForType('update', message => {
22-
if (!message.do || !message.on) return
23-
this.get(message.on)[message.do](...(message.with ?? []))
24-
})
17+
this.messaging?.send('register')
18+
19+
if (this.messaging)
20+
this.mountListeners(this.messaging)
2521

2622
return promise
2723
}

src/intercept.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
1-
import InterceptedArray from './intercept/array.js'
2-
31
export default function intercept(value: object, interceptCallback: (object: object, method: string, ...args: any) => void): object {
4-
if (Array.isArray(value)) {
5-
value = InterceptedArray.new(value, interceptCallback)
6-
}
7-
/*Object.getOwnPropertyNames(Object.getPrototypeOf(value))
8-
.filter(methodName => !(methodName.startsWith('__') && methodName.endsWith('__')))
9-
.forEach(methodName => {
10-
if (!value[methodName]) return
11-
const method = value[methodName]
12-
value[methodName] = function (...args: any) {
13-
// interception
14-
console.log(methodName)
15-
method.call(value, ...args)
2+
const arrayMutatingMethods = ['copyWithin', 'fill', 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift']
3+
const objectMutatingMethods: string[] = []
4+
const mapMutatingMethods = ['clear', 'delete', 'set']
5+
const setMutatingMethods = ['add', 'clear', 'delete']
6+
const functionMutatingMethods: string[] = []
7+
const mutatingMethods = new Map()
8+
mutatingMethods.set(Array, arrayMutatingMethods)
9+
mutatingMethods.set({}.constructor, objectMutatingMethods)
10+
mutatingMethods.set(Map, mapMutatingMethods)
11+
mutatingMethods.set(Set, setMutatingMethods)
12+
mutatingMethods.set(Function, functionMutatingMethods)
13+
14+
if (!mutatingMethods.has(value.constructor))
15+
return value
16+
return new Proxy(value, {
17+
get(target, name: string) {
18+
if (Reflect.has(target, name) && mutatingMethods.get(value.constructor).includes(name)) {
19+
const method = Reflect.get(target, name)
20+
return (...args: any) => {
21+
interceptCallback(value, name, ...args)
22+
method.call(value, ...args)
23+
}
24+
} else {
25+
return Reflect.get(target, name)
1626
}
17-
})
18-
*/
19-
return value
27+
}
28+
})
2029
}

src/intercept/array.ts

Lines changed: 0 additions & 63 deletions
This file was deleted.

src/main.ts

Lines changed: 75 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {ChildProcess} from 'child_process'
22
import initChild from './init-child.js'
3-
import IPOSMessaging from './messaging.js'
4-
import {deserialize, serialize} from './serialize.js'
3+
import IPOSMessaging, {iposMessagingMessage, iposMessagingType} from './messaging.js'
54
import intercept from './intercept.js'
65

76
export default class IPOS {
@@ -28,6 +27,10 @@ export default class IPOS {
2827
this.fieldsReverseMap = new Map()
2928
this.processMessagingMap = new Map()
3029

30+
if (process.send) {
31+
this.messaging = new IPOSMessaging(process)
32+
}
33+
3134
// proxy makes all "target.fields" available as "actual" fields
3235
this.proxy = new Proxy(this, {
3336
get(target, name: string) {
@@ -36,38 +39,90 @@ export default class IPOS {
3639
} else if (target.fields.has(name)) {
3740
return target.fields.get(name)
3841
}
39-
}
42+
},
43+
set(target, name: string, value: any): boolean {
44+
if (Reflect.has(target, name)) {
45+
throw Error(`Cannot change inherent property \`${name}\``)
46+
} else if (!target.fields.has(name)) {
47+
throw Error(`Cannot set unknown field \`${name}\`. Initialise a field with \`.create()\``)
48+
} else {
49+
target.create(name, value)
50+
return true
51+
}
52+
},
4053
})
4154
return this.proxy
4255
}
4356

57+
protected mountListeners(messaging: IPOSMessaging) {
58+
messaging.listenForType('update', (message) => this.performUpdate(message))
59+
messaging.listenForType('set', (message) => this.performSet(message))
60+
messaging.listenForType('delete', (message) => this.performDelete(message))
61+
}
62+
63+
protected sendToAll(type: iposMessagingType, data?: {}) {
64+
this.messaging?.send(type, data)
65+
this.processMessagingMap.forEach(processMessaging => {
66+
processMessaging.send(type, data)
67+
})
68+
}
69+
70+
/********************* GET **********************/
4471
public get(key: string): any {
4572
return this.fields.get(key)
4673
}
4774

48-
// todo: also accept and update non-object values
49-
public create(key: string, value: object): void {
75+
/******************** CREATE ********************/
76+
public create(key: string, value: any): void {
5077
this.createStealthy(key, value)
51-
// todo: send update message
78+
this.sendToAll('set', {key, value})
5279
}
5380

5481
protected createStealthy(key: string, value: object): void {
55-
// console.log('create', key)
5682
if (typeof value === 'object')
5783
value = intercept(value, (object, method, ...args) =>
5884
this.sendMethodCall(object, method, ...args)
5985
)
6086

6187
this.fields.set(key, value)
6288
this.fieldsReverseMap.set(value, key)
63-
// todo: send update message
6489
}
6590

91+
protected performSet(message: iposMessagingMessage) {
92+
if (!message.key || !message.value) return
93+
this.createStealthy(message.key, message.value)
94+
}
95+
96+
/******************** UPDATE ********************/
97+
protected performUpdate(message: iposMessagingMessage) {
98+
if (!message.do || !message.on) return
99+
this.get(message.on)[message.do](...(message.with ?? []))
100+
}
101+
102+
private sendMethodCall(object: object, method: string, ...args: any) {
103+
this.sendToAll('update', {
104+
do: method,
105+
on: this.fieldsReverseMap.get(object),
106+
with: Array.from(args)
107+
})
108+
}
109+
110+
/******************** DELETE ********************/
66111
public delete(key: string): boolean {
112+
this.sendToAll('delete', {key})
113+
return this.deleteStealthy(key)
114+
}
115+
116+
public deleteStealthy(key: string): boolean {
67117
return this.fields.delete(key)
68-
// todo: send update message
69118
}
70119

120+
public performDelete(message: iposMessagingMessage) {
121+
if (!message.key) return
122+
return this.deleteStealthy(message.key)
123+
}
124+
125+
/******************* PROCESS ********************/
71126
public addProcess(process: ChildProcess): Promise<void> {
72127
if (!process.send)
73128
throw new Error(`Process must have an ipc channel. Activate by passing "stdio: [<stdin>, <stdout>, <stderr>, 'ipc']" as an option.`)
@@ -79,37 +134,23 @@ export default class IPOS {
79134
if (registered) return
80135
registered = true
81136

137+
this.mountListeners(messaging)
82138
this.processMessagingMap.set(process, messaging)
83139
this.syncProcess(process)
84-
resolve()
140+
.then(() =>
141+
resolve()
142+
)
85143
})
86144
return promise
87145
}
88146

89-
private syncProcess(process: ChildProcess) {
90-
this.processMessagingMap.get(process)?.send('sync', {
91-
fields: JSON.stringify(IPOS.serialize(this.fields))
92-
})
93-
}
94-
95-
private sendMethodCall(object: object, method: string, ...args: any) {
96-
this.processMessagingMap.forEach(processMessaging => {
97-
processMessaging.send('update', {
98-
do: method,
99-
on: this.fieldsReverseMap.get(object),
100-
with: serialize(args)
101-
})
147+
private syncProcess(process: ChildProcess): Promise<void> {
148+
let resolve: Function
149+
const promise: Promise<void> = new Promise(res => resolve = res)
150+
this.processMessagingMap.get(process)?.send('sync', {fields: this.fields})
151+
this.processMessagingMap.get(process)?.listenOnceForType('sync_ok', () => {
152+
resolve()
102153
})
103-
}
104-
105-
/**
106-
* Serializes types that "JSON.stringify()" doesn't properly handle
107-
*/
108-
public static serialize(value: any): any | void {
109-
return serialize(value)
110-
}
111-
112-
public static deserialize(value: string | number | Array<any> | { $$iposType?: string, data: any }): any | void {
113-
return deserialize(value)
154+
return promise
114155
}
115156
}

src/messaging.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import {ChildProcess} from 'child_process'
2+
import {deserialize, serialize} from './serialize.js'
23

3-
type iposMessagingType = 'ready' | 'register' | 'update' | 'sync'
4+
export type iposMessagingType = 'ready' | 'register' | 'update' | 'sync' | 'sync_ok' | 'set' | 'delete'
45
type iposMessagingCallback = (message: iposMessagingMessage) => (any | void)
5-
type iposMessagingMessage = {
6+
export type iposMessagingMessage = {
67
protocol: 'ipos',
78
type: iposMessagingType,
9+
810
fields?: string,
11+
912
do?: string,
1013
on?: string,
1114
with?: Array<any>,
12-
}
15+
16+
key?: string,
17+
value: any,
18+
} & { [k: string]: string }
1319

1420
const mustHaveSendError = new Error(`Process must have a \`.send()\` method.`)
1521

@@ -31,6 +37,11 @@ export default class IPOSMessaging {
3137
return
3238
}
3339

40+
for (const property in message) {
41+
if (!message.hasOwnProperty(property)) continue
42+
message[property] = deserialize(message[property])
43+
}
44+
3445
if (this.listeners.has('any')) {
3546
this.listeners.get('any')
3647
?.forEach(callback => callback(message))
@@ -56,7 +67,10 @@ export default class IPOSMessaging {
5667
this.process.send({
5768
protocol: 'ipos',
5869
type,
59-
...data
70+
...(Object.fromEntries(
71+
Object.entries(data ?? {})
72+
.map(([key, value]) => [key, serialize(value)])
73+
))
6074
})
6175
}
6276

@@ -66,6 +80,16 @@ export default class IPOSMessaging {
6680
this.listeners.set(type, callbacks)
6781
}
6882

83+
listenOnceForType(type: iposMessagingType | 'any', callback: iposMessagingCallback) {
84+
let callbacks: Array<iposMessagingCallback> = this.listeners.get(type) ?? []
85+
const onceCallback = (message: iposMessagingMessage) => {
86+
delete callbacks[callbacks.indexOf(onceCallback)]
87+
callback(message)
88+
}
89+
callbacks.push(onceCallback)
90+
this.listeners.set(type, callbacks)
91+
}
92+
6993
listenForAll(callback: iposMessagingCallback) {
7094
this.listenForType('any', callback)
7195
}

0 commit comments

Comments
 (0)