vrpc 3.2.1 → 3.3.1
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/index.js +2 -1
- package/package.json +14 -3
- package/vrpc/VrpcPersistor.js +239 -0
package/index.js
CHANGED
|
@@ -3,5 +3,6 @@ module.exports = {
|
|
|
3
3
|
VrpcNative: require('./vrpc/VrpcNative'),
|
|
4
4
|
VrpcClient: require('./vrpc/VrpcClient'),
|
|
5
5
|
VrpcAdapter: require('./vrpc/VrpcAdapter'),
|
|
6
|
-
VrpcAgent: require('./vrpc/VrpcAgent')
|
|
6
|
+
VrpcAgent: require('./vrpc/VrpcAgent'),
|
|
7
|
+
VrpcPersistor: require('./vrpc/VrpcPersistor')
|
|
7
8
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vrpc",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"description": "Variadic Remote Procedure Calls",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
"build:addon": "ln -sf binding.gyp.no-auto binding.gyp && node-gyp rebuild && rm binding.gyp",
|
|
13
13
|
"build:browser": "./node_modules/.bin/webpack --stats-error-details",
|
|
14
14
|
"build:doc": "./node_modules/.bin/jsdoc2md -f vrpc/VrpcAdapter.js vrpc/VrpcAgent.js vrpc/VrpcClient.js vrpc/VrpcNative.js --param-list-format=list --separators > docs/api.md",
|
|
15
|
-
"test": "npm run test:adapter && npm run test:native && npm run test:agent && npm run test:client && npm run test:production",
|
|
15
|
+
"test": "npm run test:adapter && npm run test:native && npm run test:agent && npm run test:client && npm run test:production && npm run test:persistor",
|
|
16
16
|
"test:adapter": "./node_modules/.bin/mocha tests/adapter/*.js --exit",
|
|
17
17
|
"test:native": "./node_modules/.bin/mocha tests/native/*.js --exit",
|
|
18
18
|
"test:agent": "tests/agent/test.sh",
|
|
19
19
|
"test:client": "tests/client/test.sh",
|
|
20
20
|
"test:performance": "tests/performance/test.sh",
|
|
21
|
+
"test:persistor": "./node_modules/.bin/mocha tests/persistor/*.js --exit",
|
|
21
22
|
"test:production": "./node_modules/.bin/mocha tests/production/lifeCycleTest.js --timeout 30000 --exit && tests/production/test.sh"
|
|
22
23
|
},
|
|
23
24
|
"bin": {
|
|
@@ -46,9 +47,11 @@
|
|
|
46
47
|
},
|
|
47
48
|
"homepage": "https://vrpc.io",
|
|
48
49
|
"devDependencies": {
|
|
50
|
+
"@heisenware/storage": "^1.2.2",
|
|
49
51
|
"chai": "^4.2.0",
|
|
50
52
|
"crypto-browserify": "^3.12.0",
|
|
51
53
|
"dockerode": "^3.2.0",
|
|
54
|
+
"fs-extra": "^11.3.1",
|
|
52
55
|
"jsdoc-to-markdown": "^7.0.1",
|
|
53
56
|
"mocha": "^9.1.3",
|
|
54
57
|
"os-browserify": "^0.3.0",
|
|
@@ -73,5 +76,13 @@
|
|
|
73
76
|
"mqtt": "^4.2.6",
|
|
74
77
|
"nanoid": "^3.1.23",
|
|
75
78
|
"process": "^0.11.10"
|
|
79
|
+
},
|
|
80
|
+
"peerDependencies": {
|
|
81
|
+
"@heisenware/storage": "^1.2.2"
|
|
82
|
+
},
|
|
83
|
+
"peerDependenciesMeta": {
|
|
84
|
+
"@heisenware/storage": {
|
|
85
|
+
"optional": true
|
|
86
|
+
}
|
|
76
87
|
}
|
|
77
|
-
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/*
|
|
2
|
+
__/\\\________/\\\____/\\\\\\\\\______/\\\\\\\\\\\\\_________/\\\\\\\\\_
|
|
3
|
+
__\/\\\_______\/\\\__/\\\///////\\\___\/\\\/////////\\\____/\\\////////__
|
|
4
|
+
__\//\\\______/\\\__\/\\\_____\/\\\___\/\\\_______\/\\\__/\\\/___________
|
|
5
|
+
___\//\\\____/\\\___\/\\\\\\\\\\\/____\/\\\\\\\\\\\\\/__/\\\_____________
|
|
6
|
+
____\//\\\__/\\\____\/\\\//////\\\____\/\\\/////////___\/\\\_____________
|
|
7
|
+
_____\//\\\/\\\_____\/\\\____\//\\\___\/\\\____________\//\\\____________
|
|
8
|
+
______\//\\\\\______\/\\\_____\//\\\__\/\\\_____________\///\\\__________
|
|
9
|
+
_______\//\\\_______\/\\\______\//\\\_\/\\\_______________\////\\\\\\\\\_
|
|
10
|
+
________\///________\///________\///__\///___________________\/////////__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
Non-intrusively adapts code and provides access in form of asynchronous remote
|
|
14
|
+
procedure calls (RPC).
|
|
15
|
+
Author: Dr. Burkhard C. Heisen (https://github.com/heisenware/vrpc)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
Licensed under the MIT License <http://opensource.org/licenses/MIT>.
|
|
19
|
+
Copyright (c) 2018 - 2022 Dr. Burkhard C. Heisen <burkhard.heisen@heisenware.com>.
|
|
20
|
+
|
|
21
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
22
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
23
|
+
in the Software without restriction, including without limitation the rights
|
|
24
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
25
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
26
|
+
furnished to do so, subject to the following conditions:
|
|
27
|
+
|
|
28
|
+
The above copyright notice and this permission notice shall be included in all
|
|
29
|
+
copies or substantial portions of the Software.
|
|
30
|
+
|
|
31
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
32
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
33
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
34
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
35
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
36
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
37
|
+
SOFTWARE.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const VrpcAdapter = require('./VrpcAdapter')
|
|
41
|
+
const Storage = require('@heisenware/storage')
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Provides a persistence layer for VRPC instances.
|
|
45
|
+
*
|
|
46
|
+
* This class automatically saves the constructor arguments of newly created
|
|
47
|
+
* instances and re-creates them when the application restarts. It also listens
|
|
48
|
+
* for an 'update' event on instances to persist their state after creation.
|
|
49
|
+
*
|
|
50
|
+
* @requires @heisenware/storage - This peer dependency must be installed.
|
|
51
|
+
*/
|
|
52
|
+
class VrpcPersistor {
|
|
53
|
+
/**
|
|
54
|
+
* Creates an instance of VrpcPersistor.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} options Configuration options for the persistor.
|
|
57
|
+
* @param {VrpcAgent} options.agentInstance The VRPC agent whose instances should be persisted.
|
|
58
|
+
* @param {object} [options.log] Optional logger object (e.g., console) with info, warn, and error methods.
|
|
59
|
+
* @param {string} [options.dir] Optional directory for storage. Defaults to a path derived from the agent's name.
|
|
60
|
+
*/
|
|
61
|
+
constructor ({ agentInstance, log, dir }) {
|
|
62
|
+
this._agentInstance = agentInstance
|
|
63
|
+
|
|
64
|
+
// Define a no-operation function for the default logger.
|
|
65
|
+
const noop = () => {}
|
|
66
|
+
// Create a default logger that does nothing.
|
|
67
|
+
const defaultLogger = { info: noop, warn: noop, error: noop }
|
|
68
|
+
// Merge the user-provided logger with the default.
|
|
69
|
+
// This makes the log property optional and prevents crashes if methods are missing.
|
|
70
|
+
this._log = Object.assign({}, defaultLogger, log)
|
|
71
|
+
|
|
72
|
+
this._dir =
|
|
73
|
+
dir ||
|
|
74
|
+
`/shared/extensions/${agentInstance._agent
|
|
75
|
+
.toLocaleLowerCase()
|
|
76
|
+
.replace(/[^a-zA-Z0-9]/g, '-')}`
|
|
77
|
+
|
|
78
|
+
this._storage = new Storage({ log: this._log, dir: this._dir })
|
|
79
|
+
this._isInitialized = this._init()
|
|
80
|
+
|
|
81
|
+
this._log.info(
|
|
82
|
+
`[VrpcPersistor] Persistence layer enabled. Storage path: ${this._dir}`
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Restores all persisted instances from storage.
|
|
88
|
+
*
|
|
89
|
+
* It attempts to recreate each instance using its saved className and args.
|
|
90
|
+
* If an instance fails to restore after several retries with exponential backoff,
|
|
91
|
+
* it is considered "broken" and removed from storage to prevent startup loops.
|
|
92
|
+
*/
|
|
93
|
+
async restore () {
|
|
94
|
+
await this._isInitialized
|
|
95
|
+
const allIds = this._storage.keys()
|
|
96
|
+
if (allIds.length === 0) {
|
|
97
|
+
this._log.info('[VrpcPersistor] No instances to restore.')
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this._log.info(
|
|
102
|
+
`[VrpcPersistor] Found ${allIds.length} persisted instance(s) to restore.`
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
let failedIds = []
|
|
106
|
+
const restorationPromises = allIds.map(async id => {
|
|
107
|
+
try {
|
|
108
|
+
const { className, args } = await this._storage.getItem(id)
|
|
109
|
+
this._log.info(
|
|
110
|
+
`[VrpcPersistor] Restoring instance: ${id} (${className})`
|
|
111
|
+
)
|
|
112
|
+
this._agentInstance.create({ className, args, instance: id })
|
|
113
|
+
} catch (err) {
|
|
114
|
+
this._log.warn(
|
|
115
|
+
`[VrpcPersistor] Could not restore ${id}: ${err.message}. Will retry.`
|
|
116
|
+
)
|
|
117
|
+
failedIds.push(id)
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
await Promise.all(restorationPromises)
|
|
121
|
+
|
|
122
|
+
// Retry failed instances
|
|
123
|
+
let trial = 0
|
|
124
|
+
const MAX_TRIALS = 5
|
|
125
|
+
while (failedIds.length > 0 && trial++ < MAX_TRIALS) {
|
|
126
|
+
this._log.info(
|
|
127
|
+
`[VrpcPersistor] Retrying ${failedIds.length} failed instance(s), attempt ${trial}.`
|
|
128
|
+
)
|
|
129
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * trial)) // Exponential backoff
|
|
130
|
+
const stillFailing = []
|
|
131
|
+
for (const id of failedIds) {
|
|
132
|
+
try {
|
|
133
|
+
const { className, args } = await this._storage.getItem(id)
|
|
134
|
+
this._agentInstance.create({ className, args, instance: id })
|
|
135
|
+
this._log.info(
|
|
136
|
+
`[VrpcPersistor] Successfully restored ${id} on retry.`
|
|
137
|
+
)
|
|
138
|
+
} catch (err) {
|
|
139
|
+
stillFailing.push(id)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
failedIds = stillFailing
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Cleanup instances that could not be restored
|
|
146
|
+
if (failedIds.length > 0) {
|
|
147
|
+
this._log.warn(
|
|
148
|
+
'[VrpcPersistor] The following instances could not be restored and will be removed:'
|
|
149
|
+
)
|
|
150
|
+
const deletionPromises = failedIds.map(id => {
|
|
151
|
+
this._log.warn(` - ${id}`)
|
|
152
|
+
return this._storage.removeItem(id)
|
|
153
|
+
})
|
|
154
|
+
try {
|
|
155
|
+
await Promise.all(deletionPromises)
|
|
156
|
+
} catch (err) {
|
|
157
|
+
this._log.error(
|
|
158
|
+
`[VrpcPersistor] Failed to delete broken instances: ${err.message}`
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Initializes the persistor by attaching listeners to VRPC adapter events.
|
|
166
|
+
* @private
|
|
167
|
+
*/
|
|
168
|
+
async _init () {
|
|
169
|
+
try {
|
|
170
|
+
// Persist new instance creation
|
|
171
|
+
VrpcAdapter.on('create', async ({ instance, className, args }) => {
|
|
172
|
+
this._log.info(
|
|
173
|
+
`[VrpcPersistor] Persisting new instance: ${instance} (${className})`
|
|
174
|
+
)
|
|
175
|
+
try {
|
|
176
|
+
await this._persist(instance, className, args)
|
|
177
|
+
|
|
178
|
+
// Listen for 'update' events on the newly created object to persist changes
|
|
179
|
+
const obj = VrpcAdapter.getInstance(instance)
|
|
180
|
+
// Ensure the object is an event emitter
|
|
181
|
+
if (obj && typeof obj.on === 'function') {
|
|
182
|
+
obj.on('update', data => {
|
|
183
|
+
this._log.info(
|
|
184
|
+
`[VrpcPersistor] Persisting update for: ${instance} (${className})`
|
|
185
|
+
)
|
|
186
|
+
// The convention here is that the 'update' event data can be used
|
|
187
|
+
// as constructor arguments upon restoration.
|
|
188
|
+
this._persist(instance, className, [data]).catch(err =>
|
|
189
|
+
this._log.warn(
|
|
190
|
+
`[VrpcPersistor] Failed persisting update of ${instance} (${className}): ${err.message}`
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
this._log.warn(
|
|
197
|
+
`[VrpcPersistor] Failed persisting new instance ${instance} (${className}): ${err.message}`
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
// Persist instance deletion
|
|
203
|
+
VrpcAdapter.on('delete', ({ instance, className }) => {
|
|
204
|
+
this._log.info(
|
|
205
|
+
`[VrpcPersistor] Deleting persisted instance: ${instance} (${className})`
|
|
206
|
+
)
|
|
207
|
+
this._delete(instance).catch(err =>
|
|
208
|
+
this._log.warn(
|
|
209
|
+
`[VrpcPersistor] Failed to delete persisted instance ${instance}: ${err.message}`
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
})
|
|
213
|
+
} catch (err) {
|
|
214
|
+
this._log.error(
|
|
215
|
+
`[VrpcPersistor] Could not initialize persistence layer: ${err.message}`
|
|
216
|
+
)
|
|
217
|
+
// Propagate the error to fail fast if initialization is not possible
|
|
218
|
+
throw err
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Saves an instance's details to storage.
|
|
224
|
+
* @private
|
|
225
|
+
*/
|
|
226
|
+
async _persist (id, className, args) {
|
|
227
|
+
await this._storage.setItem(id, { className, args }, { folder: className })
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Removes an instance from storage.
|
|
232
|
+
* @private
|
|
233
|
+
*/
|
|
234
|
+
async _delete (id) {
|
|
235
|
+
await this._storage.removeItem(id)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = VrpcPersistor
|