@iebh/tera-fy 1.0.10 → 1.0.11
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/lib/terafy.client.js +68 -3
- package/lib/terafy.server.js +123 -9
- package/package.json +7 -2
- package/plugins/vue2.js +15 -3
- package/plugins/vue3.js +15 -2
- package/utils/mixin.js +18 -0
package/lib/terafy.client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {diff, jsonPatchPathConverter as jsPatchConverter} from 'just-diff';
|
|
2
2
|
import {cloneDeep} from 'lodash-es';
|
|
3
|
+
import Mitt from 'mitt';
|
|
3
4
|
import {nanoid} from 'nanoid';
|
|
4
5
|
|
|
5
6
|
/* globals globalThis */
|
|
@@ -30,6 +31,13 @@ export default class TeraFy {
|
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Event emitter subscription endpoint
|
|
36
|
+
* @type {Mitt}
|
|
37
|
+
*/
|
|
38
|
+
events = Mitt();
|
|
39
|
+
|
|
40
|
+
|
|
33
41
|
/**
|
|
34
42
|
* DOMElements for this TeraFy instance
|
|
35
43
|
*
|
|
@@ -61,9 +69,12 @@ export default class TeraFy {
|
|
|
61
69
|
// Projects
|
|
62
70
|
'bindProject', 'getProject', 'getProjects', 'setActiveProject', 'requireProject', 'selectProject',
|
|
63
71
|
|
|
64
|
-
// Project
|
|
65
|
-
'getProjectState', '
|
|
66
|
-
|
|
72
|
+
// Project State
|
|
73
|
+
'getProjectState', 'setProjectState', 'saveProjectState', 'replaceProjectState',
|
|
74
|
+
|
|
75
|
+
// Project State Patching + Subscribing
|
|
76
|
+
'applyProjectStatePatch', 'subscribeProjectState',
|
|
77
|
+
// bindProjectState() - See individual plugins
|
|
67
78
|
|
|
68
79
|
// Project files
|
|
69
80
|
'getProjectFiles',
|
|
@@ -190,6 +201,13 @@ export default class TeraFy {
|
|
|
190
201
|
response: e.toString(),
|
|
191
202
|
});
|
|
192
203
|
})
|
|
204
|
+
} else if (message?.action == 'event') {
|
|
205
|
+
return Promise.resolve()
|
|
206
|
+
.then(()=> this.events.emit(message.event, ...message.payload))
|
|
207
|
+
.catch(e => {
|
|
208
|
+
console.warn(`TERA-FY client threw while handling emitted event "${message.event}"`, {message});
|
|
209
|
+
throw e;
|
|
210
|
+
})
|
|
193
211
|
} else if (message?.id) {
|
|
194
212
|
this.debug(`Ignoring message ID ${message.id} - was meant for someone else?`);
|
|
195
213
|
} else {
|
|
@@ -273,6 +291,11 @@ export default class TeraFy {
|
|
|
273
291
|
this.injectStylesheet(),
|
|
274
292
|
this.injectMethods(),
|
|
275
293
|
]))
|
|
294
|
+
.then(()=> this.rpc('setServerMode', // Tell server what mode its in
|
|
295
|
+
this.settings.mode == 'child' ? 'embedded'
|
|
296
|
+
: this.settings.mode == 'parent' ? 'window'
|
|
297
|
+
: (()=> { throw(`Unknown server mode "${this.settings.mode}"`) })()
|
|
298
|
+
))
|
|
276
299
|
.then(()=> Promise.all( // Init all plugins (with this outer module as the context)
|
|
277
300
|
this.plugins.map(plugin =>
|
|
278
301
|
plugin.init.call(context)
|
|
@@ -317,6 +340,8 @@ export default class TeraFy {
|
|
|
317
340
|
injectComms() { return new Promise(resolve => {
|
|
318
341
|
switch (this.settings.mode) {
|
|
319
342
|
case 'child':
|
|
343
|
+
this.debug('Injecting TERA site as iFrame child');
|
|
344
|
+
|
|
320
345
|
this.dom.el = document.createElement('div')
|
|
321
346
|
this.dom.el.id = 'tera-fy';
|
|
322
347
|
this.dom.el.classList.toggle('dev-mode', this.settings.devMode);
|
|
@@ -336,6 +361,7 @@ export default class TeraFy {
|
|
|
336
361
|
this.dom.el.append(this.dom.iframe);
|
|
337
362
|
break;
|
|
338
363
|
case 'parent':
|
|
364
|
+
this.debug('Using TERA site stack parent');
|
|
339
365
|
resolve();
|
|
340
366
|
break;
|
|
341
367
|
default:
|
|
@@ -637,6 +663,45 @@ export default class TeraFy {
|
|
|
637
663
|
*/
|
|
638
664
|
|
|
639
665
|
|
|
666
|
+
/**
|
|
667
|
+
* Set a nested value within the project state
|
|
668
|
+
* Paths can be any valid Lodash.set() value such as:
|
|
669
|
+
*
|
|
670
|
+
* - Dotted notation - e.g. `foo.bar.1.baz`
|
|
671
|
+
* - Array path segments e.g. `['foo', 'bar', 1, 'baz']`
|
|
672
|
+
*
|
|
673
|
+
*
|
|
674
|
+
* @function setProjectState
|
|
675
|
+
* @param {String|Array<String>} path The sub-path within the project state to set
|
|
676
|
+
* @param {*} value The value to set
|
|
677
|
+
*
|
|
678
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
679
|
+
* @param {Boolean} [options.save=true] Save the changes to the server immediately, disable to queue up multiple writes
|
|
680
|
+
* @param {Boolean} [options.sync=false] Wait for the server to acknowledge the write, you almost never need to do this
|
|
681
|
+
*
|
|
682
|
+
* @returns {Promise} A promise which resolves when the operation has synced with the server
|
|
683
|
+
*/
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Force-Save the currently active project state
|
|
688
|
+
*
|
|
689
|
+
* @function saveProjectState
|
|
690
|
+
* @returns {Promise} A promise which resolves when the operation has completed
|
|
691
|
+
*/
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Overwrite the entire project state with a new object
|
|
696
|
+
* You almost never want to use this function directly, see `setProjectState(path, value)` for a nicer wrapper
|
|
697
|
+
*
|
|
698
|
+
* @function replaceProjectState
|
|
699
|
+
* @see setProjectState()
|
|
700
|
+
* @param {Object} newState The new state to replace the current state with
|
|
701
|
+
* @returns {Promise} A promise which resolves when the operation has completed
|
|
702
|
+
*/
|
|
703
|
+
|
|
704
|
+
|
|
640
705
|
/**
|
|
641
706
|
* Apply a computed `just-diff` patch to the current project state
|
|
642
707
|
*
|
package/lib/terafy.server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {cloneDeep, set as pathSet} from 'lodash-es';
|
|
2
2
|
import {diffApply, jsonPatchPathConverter as jsPatchConverter} from 'just-diff-apply';
|
|
3
3
|
import {nanoid} from 'nanoid';
|
|
4
|
+
import mixin from '#utils/mixin';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Server-side functions available to the Tera-Fy client library
|
|
@@ -19,15 +20,22 @@ export default class TeraFyServer {
|
|
|
19
20
|
* @property {Number} subscribeTimeout Acceptable timeout period for subscribers to acklowledge a project change event, failing to respond will result in the subscriber being removed from the available subscriber list
|
|
20
21
|
* @property {String} restrictOrigin URL to restrict communications to
|
|
21
22
|
* @property {String} projectId The project to use as the default reference when calling various APIs
|
|
23
|
+
* @property {Number} The current server mode matching `SERVERMODE_*`
|
|
22
24
|
*/
|
|
23
25
|
settings = {
|
|
24
26
|
devMode: false,
|
|
25
27
|
restrictOrigin: '*',
|
|
26
28
|
subscribeTimeout: 2000,
|
|
27
29
|
projectId: null,
|
|
30
|
+
serverMode: 0,
|
|
28
31
|
};
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
static SERVERMODE_NONE = 0;
|
|
34
|
+
static SERVERMODE_EMBEDDED = 1;
|
|
35
|
+
static SERVERMODE_WINDOW = 2;
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
// Contexts - createContext(), getClientContext(), messageEvent, senderRpc() {{{
|
|
31
39
|
/**
|
|
32
40
|
* Create a context based on a shallow copy of this instance + additional functionality for the incoming MessageEvent
|
|
33
41
|
* This is used by acceptMessage to provide a means to reply / send messages to the originator
|
|
@@ -37,10 +45,10 @@ export default class TeraFyServer {
|
|
|
37
45
|
* @returns {Object} A context, which is this instance extended with additional properties
|
|
38
46
|
*/
|
|
39
47
|
createContext(e) {
|
|
40
|
-
//
|
|
41
|
-
return
|
|
48
|
+
// Construct wrapper for sendRaw for this client
|
|
49
|
+
return mixin(this, {
|
|
42
50
|
messageEvent: e,
|
|
43
|
-
sendRaw(message) {
|
|
51
|
+
sendRaw(message) {
|
|
44
52
|
let payload;
|
|
45
53
|
try {
|
|
46
54
|
payload = {
|
|
@@ -49,8 +57,7 @@ export default class TeraFyServer {
|
|
|
49
57
|
};
|
|
50
58
|
e.source.postMessage(payload, this.settings.restrictOrigin);
|
|
51
59
|
} catch (e) {
|
|
52
|
-
this.debug('ERROR', '
|
|
53
|
-
this.debug('ERROR', 'Attempted to dispatch payload server(via reply)->client', payload);
|
|
60
|
+
this.debug('ERROR', 'Attempted to dispatch payload server(via reply)->client', {payload, e});
|
|
54
61
|
throw e;
|
|
55
62
|
}
|
|
56
63
|
},
|
|
@@ -58,6 +65,58 @@ export default class TeraFyServer {
|
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Create a new client context from the server to the client even if the client hasn't requested the communication
|
|
70
|
+
* This function is used to send unsolicited communications from the server->client in contrast to createContext() which _replies_ from client->server->client
|
|
71
|
+
*
|
|
72
|
+
* @returns {Object} A context, which is this instance extended with additional properties
|
|
73
|
+
*/
|
|
74
|
+
getClientContext() {
|
|
75
|
+
switch (this.settings.serverMode) {
|
|
76
|
+
case TeraFyServer.SERVERMODE_NONE:
|
|
77
|
+
throw new Error('Client has not yet initiated communication');
|
|
78
|
+
case TeraFyServer.SERVERMODE_EMBEDDED:
|
|
79
|
+
// Server is inside an iFrame so we need to send messages to the window parent
|
|
80
|
+
return mixin(this, {
|
|
81
|
+
sendRaw(message) {
|
|
82
|
+
let payload;
|
|
83
|
+
try {
|
|
84
|
+
payload = {
|
|
85
|
+
TERA: 1,
|
|
86
|
+
...cloneDeep(message), // Need to clone to resolve promise nasties
|
|
87
|
+
};
|
|
88
|
+
window.parent.postMessage(payload, this.settings.restrictOrigin);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
this.debug('ERROR', 'Attempted to dispatch payload server(iframe)->cient(top level window)', {payload, e});
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
case TeraFyServer.SERVERMODE_WINDOW:
|
|
96
|
+
// Server is the top-level window so we need to send messages to an embedded iFrame
|
|
97
|
+
debugger; // FIXME: THIS IS ALL UNTESTED
|
|
98
|
+
let iFrame = document.querySelector('iframe#tera-fy');
|
|
99
|
+
if (!iFrame) throw new Error('Cannot locate TERA-FY client iFrame');
|
|
100
|
+
|
|
101
|
+
return mixin(this, {
|
|
102
|
+
sendRaw(message) {
|
|
103
|
+
let payload;
|
|
104
|
+
try {
|
|
105
|
+
payload = {
|
|
106
|
+
TERA: 1,
|
|
107
|
+
...cloneDeep(message), // Need to clone to resolve promise nasties
|
|
108
|
+
};
|
|
109
|
+
iFrame.postMessage(payload, this.settings.restrictOrigin);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
this.debug('ERROR', 'Attempted to dispatch payload server(top level window)->cient(iframe)', {payload, e});
|
|
112
|
+
throw e;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
61
120
|
/**
|
|
62
121
|
* MessageEvent context
|
|
63
122
|
* Only available if the context was created via `createContext()`
|
|
@@ -87,7 +146,7 @@ export default class TeraFyServer {
|
|
|
87
146
|
}
|
|
88
147
|
// }}}
|
|
89
148
|
|
|
90
|
-
// Messages - handshake(), sendRaw(), acceptMessage(), requestFocus() {{{
|
|
149
|
+
// Messages - handshake(), send(), sendRaw(), setServerMode(), acceptMessage(), requestFocus(), emitClients() {{{
|
|
91
150
|
|
|
92
151
|
/**
|
|
93
152
|
* Return basic server information as a form of validation
|
|
@@ -130,6 +189,7 @@ export default class TeraFyServer {
|
|
|
130
189
|
|
|
131
190
|
/**
|
|
132
191
|
* Send raw message content to the client
|
|
192
|
+
* Unlike send() this method does not expect any response
|
|
133
193
|
*
|
|
134
194
|
* @param {Object} message Message object to send
|
|
135
195
|
* @param {Window} Window context to dispatch the message via if its not the same as the regular window
|
|
@@ -141,13 +201,31 @@ export default class TeraFyServer {
|
|
|
141
201
|
TERA: 1,
|
|
142
202
|
...cloneDeep(message), // Need to clone to resolve promise nasties
|
|
143
203
|
};
|
|
144
|
-
this.debug('INFO', 'Parent
|
|
204
|
+
this.debug('INFO', 'Parent send', message, '<=>', payload);
|
|
145
205
|
(sendVia || globalThis.parent).postMessage(payload, this.settings.restrictOrigin);
|
|
146
206
|
} catch (e) {
|
|
147
207
|
this.debug('ERROR', 'Attempted to dispatch payload server->client', payload);
|
|
148
208
|
this.debug('ERROR', 'Message compose server->client:', e);
|
|
149
209
|
}
|
|
210
|
+
}
|
|
211
|
+
|
|
150
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Setter to translate between string inputs and the server modes in SERVERMODE_*
|
|
215
|
+
*
|
|
216
|
+
* @param {String} mode The server mode to set to
|
|
217
|
+
*/
|
|
218
|
+
setServerMode(mode) {
|
|
219
|
+
switch (mode) {
|
|
220
|
+
case 'embedded':
|
|
221
|
+
this.settings.serverMode = TeraFyServer.SERVERMODE_EMBEDDED;
|
|
222
|
+
break;
|
|
223
|
+
case 'window':
|
|
224
|
+
this.settings.serverMode = TeraFyServer.SERVERMODE_WINDOW;
|
|
225
|
+
break;
|
|
226
|
+
default:
|
|
227
|
+
throw new Error(`Unsupported server mode "${mode}"`);
|
|
228
|
+
}
|
|
151
229
|
}
|
|
152
230
|
|
|
153
231
|
|
|
@@ -217,6 +295,24 @@ export default class TeraFyServer {
|
|
|
217
295
|
.then(()=> cb.call(this))
|
|
218
296
|
.finally(()=> this.senderRpc('toggleFocus', false))
|
|
219
297
|
}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Emit messages down into all connected clients
|
|
302
|
+
* Note that emitted messages have no response - they are sent to clients only with no return value
|
|
303
|
+
*
|
|
304
|
+
* @param {String} event The event name to emit
|
|
305
|
+
* @param {*} [args...] Optional event payload to send
|
|
306
|
+
* @returns {Promise} A promise which resolves when the transmission has completed
|
|
307
|
+
*/
|
|
308
|
+
emitClients(event, ...args) {
|
|
309
|
+
return this.getClientContext().sendRaw({
|
|
310
|
+
action: 'event',
|
|
311
|
+
id: nanoid(),
|
|
312
|
+
event,
|
|
313
|
+
payload: args,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
220
316
|
// }}}
|
|
221
317
|
|
|
222
318
|
// Session / User - getUser() {{{
|
|
@@ -409,7 +505,7 @@ export default class TeraFyServer {
|
|
|
409
505
|
|
|
410
506
|
// }}}
|
|
411
507
|
|
|
412
|
-
// Project State - getProjectState(), setProjectState(), saveProjectState(),
|
|
508
|
+
// Project State - getProjectState(), setProjectState(), saveProjectState(), replaceProjectState() {{{
|
|
413
509
|
|
|
414
510
|
/**
|
|
415
511
|
* Return the current, full snapshot state of the active project
|
|
@@ -483,6 +579,24 @@ export default class TeraFyServer {
|
|
|
483
579
|
}
|
|
484
580
|
|
|
485
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Overwrite the entire project state with a new object
|
|
584
|
+
* You almost never want to use this function directly, see `setProjectState(path, value)` for a nicer wrapper
|
|
585
|
+
*
|
|
586
|
+
* @see setProjectState()
|
|
587
|
+
* @param {Object} newState The new state to replace the current state with
|
|
588
|
+
* @returns {Promise} A promise which resolves when the operation has completed
|
|
589
|
+
*/
|
|
590
|
+
replaceProjectState(newState) {
|
|
591
|
+
if (!app.service('$projects').active) throw new Error('No active project');
|
|
592
|
+
if (typeof newState != 'object') throw new Error('Only project state objects are accepted');
|
|
593
|
+
|
|
594
|
+
Object.assign(app.service('$projects').active, newState);
|
|
595
|
+
return this.saveProjectState();
|
|
596
|
+
}
|
|
597
|
+
// }}}
|
|
598
|
+
|
|
599
|
+
// Project State Patching + Subscribing - applyProjectStatePatch(), subscribeProjectState() {{{
|
|
486
600
|
/**
|
|
487
601
|
* Apply a computed `just-diff` patch to the current project state
|
|
488
602
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iebh/tera-fy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "TERA website worker",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "esbuild --platform=browser --format=esm --bundle lib/terafy.client.js --outfile=dist/terafy.js --minify --sourcemap --serve --servedir=.",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"type": "module",
|
|
14
14
|
"imports": {
|
|
15
|
-
"#terafy": "./lib/terafy.client.js"
|
|
15
|
+
"#terafy": "./lib/terafy.client.js",
|
|
16
|
+
"#utils/*": "./utils/*.js"
|
|
16
17
|
},
|
|
17
18
|
"exports": {
|
|
18
19
|
".": {
|
|
@@ -84,5 +85,9 @@
|
|
|
84
85
|
"ecmaVersion": 13,
|
|
85
86
|
"sourceType": "module"
|
|
86
87
|
}
|
|
88
|
+
},
|
|
89
|
+
"dependencies": {
|
|
90
|
+
"@momsfriendlydevco/supabase-reactive": "^1.0.7",
|
|
91
|
+
"mitt": "^3.0.1"
|
|
87
92
|
}
|
|
88
93
|
}
|
package/plugins/vue2.js
CHANGED
|
@@ -78,19 +78,31 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
|
|
|
78
78
|
settings.component[settings.componentKey] = stateObservable;
|
|
79
79
|
|
|
80
80
|
// Watch for remote changes and update
|
|
81
|
-
//
|
|
81
|
+
let skipUpdate = 0; // How many subsequent WRITE operations to ignore (set when reading)
|
|
82
|
+
if (settings.read) {
|
|
83
|
+
this.events.on(`update:projects/${stateReactive.id}`, newState => {
|
|
84
|
+
skipUpdate++; // Skip next update as we're updating our own state anyway
|
|
85
|
+
Object.assign(stateReactive, newState);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
82
88
|
|
|
83
89
|
// Watch for local writes and react
|
|
84
90
|
if (settings.write) {
|
|
85
91
|
if (!settings.component) throw new Error('bindProjectState requires a VueComponent specified as `component`');
|
|
86
92
|
|
|
87
|
-
// NOTE: The below $watch function returns two copies of the new value of the observed
|
|
88
|
-
// of what changed ourselves by initalizing against the
|
|
93
|
+
// NOTE: The below $watch function returns two copies of the new value of the observed
|
|
94
|
+
// so we have to keep track of what changed ourselves by initalizing against the
|
|
95
|
+
// snapshot
|
|
89
96
|
let oldVal = cloneDeep(snapshot);
|
|
90
97
|
|
|
91
98
|
settings.component.$watch(
|
|
92
99
|
settings.componentKey,
|
|
93
100
|
newVal => {
|
|
101
|
+
if (skipUpdate > 0) {
|
|
102
|
+
skipUpdate--;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
94
106
|
this.createProjectStatePatch(newVal, oldVal);
|
|
95
107
|
oldVal = cloneDeep(snapshot);
|
|
96
108
|
},
|
package/plugins/vue3.js
CHANGED
|
@@ -30,6 +30,7 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
|
|
|
30
30
|
*
|
|
31
31
|
* @param {Object} [options] Additional options to mutate behaviour
|
|
32
32
|
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
33
|
+
* @param {Boolean} [options.read=true] Allow remote reactivity - update the local state when the server changes
|
|
33
34
|
* @param {Boolean} [options.write=true] Allow local reactivity to writes - send these to the server
|
|
34
35
|
*
|
|
35
36
|
* @returns {Promie<Reactive<Object>>} A reactive object representing the project state
|
|
@@ -37,6 +38,7 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
|
|
|
37
38
|
bindProjectState(options) {
|
|
38
39
|
let settings = {
|
|
39
40
|
autoRequire: true,
|
|
41
|
+
read: true,
|
|
40
42
|
write: true,
|
|
41
43
|
...options,
|
|
42
44
|
};
|
|
@@ -46,19 +48,30 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
|
|
|
46
48
|
autoRequire: settings.autoRequire ,
|
|
47
49
|
}))
|
|
48
50
|
.then(snapshot => {
|
|
49
|
-
this.debug('
|
|
51
|
+
this.debug('Fetched project snapshot', snapshot);
|
|
50
52
|
|
|
51
53
|
// Create initial reactive
|
|
52
54
|
let stateReactive = reactive(snapshot);
|
|
53
55
|
|
|
54
56
|
// Watch for remote changes and update
|
|
55
|
-
//
|
|
57
|
+
let skipUpdate = 0; // How many subsequent WRITE operations to ignore (set when reading)
|
|
58
|
+
if (settings.read) {
|
|
59
|
+
this.events.on(`update:projects/${stateReactive.id}`, newState => {
|
|
60
|
+
skipUpdate++; // Skip next update as we're updating our own state anyway
|
|
61
|
+
Object.assign(stateReactive, newState);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
56
64
|
|
|
57
65
|
// Watch for local writes and react
|
|
58
66
|
if (settings.write) {
|
|
59
67
|
watch(
|
|
60
68
|
stateReactive,
|
|
61
69
|
(newVal, oldVal) => {
|
|
70
|
+
if (skipUpdate > 0) {
|
|
71
|
+
skipUpdate--;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
62
75
|
this.createProojectStatePatch(newVal, oldVal);
|
|
63
76
|
},
|
|
64
77
|
{
|
package/utils/mixin.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shallow-copy a object instance and inject new properties into the result
|
|
3
|
+
*
|
|
4
|
+
* Rather ugly shallow-copy-of instance hack from https://stackoverflow.com/a/44782052/1295040
|
|
5
|
+
* Keeps the original object instance and overrides the given object of assignments
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} instance Original object class instance to mixin
|
|
8
|
+
* @param {Object} assignments Additional object properties to mix
|
|
9
|
+
* @returns {Object} A shallow copy of the input instance extended with the assignments
|
|
10
|
+
*/
|
|
11
|
+
export default function mixin(instance, assignments) {
|
|
12
|
+
let output = Object.assign(
|
|
13
|
+
Object.create(Object.getPrototypeOf(instance)),
|
|
14
|
+
instance,
|
|
15
|
+
assignments,
|
|
16
|
+
);
|
|
17
|
+
return output;
|
|
18
|
+
}
|