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 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.2.1",
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