@iebh/tera-fy 1.0.9 → 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/README.md +9 -4
- package/api.md +445 -431
- package/dist/terafy.js +2 -2
- package/dist/terafy.js.map +4 -4
- package/docs/assets/anchor.js +350 -0
- package/docs/assets/bass-addons.css +12 -0
- package/docs/assets/bass.css +544 -0
- package/docs/assets/fonts/EOT/SourceCodePro-Bold.eot +0 -0
- package/docs/assets/fonts/EOT/SourceCodePro-Regular.eot +0 -0
- package/docs/assets/fonts/LICENSE.txt +93 -0
- package/docs/assets/fonts/OTF/SourceCodePro-Bold.otf +0 -0
- package/docs/assets/fonts/OTF/SourceCodePro-Regular.otf +0 -0
- package/docs/assets/fonts/TTF/SourceCodePro-Bold.ttf +0 -0
- package/docs/assets/fonts/TTF/SourceCodePro-Regular.ttf +0 -0
- package/docs/assets/fonts/WOFF/OTF/SourceCodePro-Bold.otf.woff +0 -0
- package/docs/assets/fonts/WOFF/OTF/SourceCodePro-Regular.otf.woff +0 -0
- package/docs/assets/fonts/WOFF/TTF/SourceCodePro-Bold.ttf.woff +0 -0
- package/docs/assets/fonts/WOFF/TTF/SourceCodePro-Regular.ttf.woff +0 -0
- package/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Bold.otf.woff2 +0 -0
- package/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Regular.otf.woff2 +0 -0
- package/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Bold.ttf.woff2 +0 -0
- package/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2 +0 -0
- package/docs/assets/fonts/source-code-pro.css +23 -0
- package/docs/assets/github.css +123 -0
- package/docs/assets/site.js +168 -0
- package/docs/assets/split.css +15 -0
- package/docs/assets/split.js +782 -0
- package/docs/assets/style.css +147 -0
- package/docs/index.html +3636 -0
- package/{index.html → docs/playground.html} +48 -12
- package/documentation.yml +12 -0
- package/lib/terafy.client.js +294 -8
- package/lib/terafy.server.js +229 -13
- package/package.json +13 -7
- package/plugins/vue2.js +185 -0
- package/plugins/{vue.js → vue3.js} +35 -13
- package/utils/mixin.js +18 -0
package/lib/terafy.server.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {cloneDeep} from 'lodash-es';
|
|
1
|
+
import {cloneDeep, set as pathSet} from 'lodash-es';
|
|
2
|
+
import {diffApply, jsonPatchPathConverter as jsPatchConverter} from 'just-diff-apply';
|
|
2
3
|
import {nanoid} from 'nanoid';
|
|
4
|
+
import mixin from '#utils/mixin';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Server-side functions available to the Tera-Fy client library
|
|
@@ -15,13 +17,25 @@ export default class TeraFyServer {
|
|
|
15
17
|
*
|
|
16
18
|
* @type {Object}
|
|
17
19
|
* @property {Boolean} devMode Operate in devMode - i.e. force outer refresh when encountering an existing TeraFy instance
|
|
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
|
|
18
21
|
* @property {String} restrictOrigin URL to restrict communications to
|
|
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_*`
|
|
19
24
|
*/
|
|
20
25
|
settings = {
|
|
26
|
+
devMode: false,
|
|
21
27
|
restrictOrigin: '*',
|
|
28
|
+
subscribeTimeout: 2000,
|
|
29
|
+
projectId: null,
|
|
30
|
+
serverMode: 0,
|
|
22
31
|
};
|
|
23
32
|
|
|
24
|
-
|
|
33
|
+
static SERVERMODE_NONE = 0;
|
|
34
|
+
static SERVERMODE_EMBEDDED = 1;
|
|
35
|
+
static SERVERMODE_WINDOW = 2;
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
// Contexts - createContext(), getClientContext(), messageEvent, senderRpc() {{{
|
|
25
39
|
/**
|
|
26
40
|
* Create a context based on a shallow copy of this instance + additional functionality for the incoming MessageEvent
|
|
27
41
|
* This is used by acceptMessage to provide a means to reply / send messages to the originator
|
|
@@ -31,10 +45,10 @@ export default class TeraFyServer {
|
|
|
31
45
|
* @returns {Object} A context, which is this instance extended with additional properties
|
|
32
46
|
*/
|
|
33
47
|
createContext(e) {
|
|
34
|
-
//
|
|
35
|
-
return
|
|
48
|
+
// Construct wrapper for sendRaw for this client
|
|
49
|
+
return mixin(this, {
|
|
36
50
|
messageEvent: e,
|
|
37
|
-
sendRaw(message) {
|
|
51
|
+
sendRaw(message) {
|
|
38
52
|
let payload;
|
|
39
53
|
try {
|
|
40
54
|
payload = {
|
|
@@ -43,8 +57,7 @@ export default class TeraFyServer {
|
|
|
43
57
|
};
|
|
44
58
|
e.source.postMessage(payload, this.settings.restrictOrigin);
|
|
45
59
|
} catch (e) {
|
|
46
|
-
this.debug('ERROR', '
|
|
47
|
-
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});
|
|
48
61
|
throw e;
|
|
49
62
|
}
|
|
50
63
|
},
|
|
@@ -52,6 +65,58 @@ export default class TeraFyServer {
|
|
|
52
65
|
}
|
|
53
66
|
|
|
54
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
|
+
|
|
55
120
|
/**
|
|
56
121
|
* MessageEvent context
|
|
57
122
|
* Only available if the context was created via `createContext()`
|
|
@@ -81,7 +146,7 @@ export default class TeraFyServer {
|
|
|
81
146
|
}
|
|
82
147
|
// }}}
|
|
83
148
|
|
|
84
|
-
// Messages - handshake(), sendRaw(), acceptMessage(), requestFocus() {{{
|
|
149
|
+
// Messages - handshake(), send(), sendRaw(), setServerMode(), acceptMessage(), requestFocus(), emitClients() {{{
|
|
85
150
|
|
|
86
151
|
/**
|
|
87
152
|
* Return basic server information as a form of validation
|
|
@@ -124,6 +189,7 @@ export default class TeraFyServer {
|
|
|
124
189
|
|
|
125
190
|
/**
|
|
126
191
|
* Send raw message content to the client
|
|
192
|
+
* Unlike send() this method does not expect any response
|
|
127
193
|
*
|
|
128
194
|
* @param {Object} message Message object to send
|
|
129
195
|
* @param {Window} Window context to dispatch the message via if its not the same as the regular window
|
|
@@ -135,13 +201,31 @@ export default class TeraFyServer {
|
|
|
135
201
|
TERA: 1,
|
|
136
202
|
...cloneDeep(message), // Need to clone to resolve promise nasties
|
|
137
203
|
};
|
|
138
|
-
this.debug('INFO', 'Parent
|
|
204
|
+
this.debug('INFO', 'Parent send', message, '<=>', payload);
|
|
139
205
|
(sendVia || globalThis.parent).postMessage(payload, this.settings.restrictOrigin);
|
|
140
206
|
} catch (e) {
|
|
141
207
|
this.debug('ERROR', 'Attempted to dispatch payload server->client', payload);
|
|
142
208
|
this.debug('ERROR', 'Message compose server->client:', e);
|
|
143
209
|
}
|
|
210
|
+
}
|
|
144
211
|
|
|
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
|
+
}
|
|
145
229
|
}
|
|
146
230
|
|
|
147
231
|
|
|
@@ -211,6 +295,24 @@ export default class TeraFyServer {
|
|
|
211
295
|
.then(()=> cb.call(this))
|
|
212
296
|
.finally(()=> this.senderRpc('toggleFocus', false))
|
|
213
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
|
+
}
|
|
214
316
|
// }}}
|
|
215
317
|
|
|
216
318
|
// Session / User - getUser() {{{
|
|
@@ -237,7 +339,7 @@ export default class TeraFyServer {
|
|
|
237
339
|
$auth.promise(),
|
|
238
340
|
$subscriptions.promise(),
|
|
239
341
|
])
|
|
240
|
-
.then(()=>
|
|
342
|
+
.then(()=> $auth.user.id ? {
|
|
241
343
|
id: $auth.user.id,
|
|
242
344
|
email: $auth.user.email,
|
|
243
345
|
name: [
|
|
@@ -245,7 +347,7 @@ export default class TeraFyServer {
|
|
|
245
347
|
$auth.user.family_name,
|
|
246
348
|
].filter(Boolean).join(' '),
|
|
247
349
|
isSubscribed: $subscriptions.isSubscribed,
|
|
248
|
-
})
|
|
350
|
+
} : null)
|
|
249
351
|
}
|
|
250
352
|
|
|
251
353
|
// }}}
|
|
@@ -403,7 +505,7 @@ export default class TeraFyServer {
|
|
|
403
505
|
|
|
404
506
|
// }}}
|
|
405
507
|
|
|
406
|
-
// Project State - getProjectState(),
|
|
508
|
+
// Project State - getProjectState(), setProjectState(), saveProjectState(), replaceProjectState() {{{
|
|
407
509
|
|
|
408
510
|
/**
|
|
409
511
|
* Return the current, full snapshot state of the active project
|
|
@@ -427,14 +529,128 @@ export default class TeraFyServer {
|
|
|
427
529
|
}
|
|
428
530
|
|
|
429
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Set a nested value within the project state
|
|
534
|
+
* Paths can be any valid Lodash.set() value such as:
|
|
535
|
+
*
|
|
536
|
+
* - Dotted notation - e.g. `foo.bar.1.baz`
|
|
537
|
+
* - Array path segments e.g. `['foo', 'bar', 1, 'baz']`
|
|
538
|
+
*
|
|
539
|
+
*
|
|
540
|
+
* @param {String|Array<String>} path The sub-path within the project state to set
|
|
541
|
+
* @param {*} value The value to set
|
|
542
|
+
*
|
|
543
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
544
|
+
* @param {Boolean} [options.save=true] Save the changes to the server immediately, disable to queue up multiple writes
|
|
545
|
+
* @param {Boolean} [options.sync=false] Wait for the server to acknowledge the write, you almost never need to do this
|
|
546
|
+
*
|
|
547
|
+
* @returns {Promise} A promise which resolves when the operation has synced with the server
|
|
548
|
+
*/
|
|
549
|
+
setProjectState(path, value, options) {
|
|
550
|
+
let settings = {
|
|
551
|
+
save: true,
|
|
552
|
+
sync: false,
|
|
553
|
+
...options,
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
if (!app.service('$projects').active) throw new Error('No active project');
|
|
557
|
+
|
|
558
|
+
pathSet(app.service('$projects').active, path, value)
|
|
559
|
+
|
|
560
|
+
return (
|
|
561
|
+
this.save && this.sync ? this.saveProjectState()
|
|
562
|
+
: this.save ? void this.saveProjectState()
|
|
563
|
+
: (()=> { throw new Error('setProjectState({sync: true, save: false}) makes no sense') })()
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Force-Save the currently active project state
|
|
570
|
+
*
|
|
571
|
+
* @returns {Promise} A promise which resolves when the operation has completed
|
|
572
|
+
*/
|
|
573
|
+
saveProjectState() {
|
|
574
|
+
if (!app.service('$projects').active) throw new Error('No active project');
|
|
575
|
+
|
|
576
|
+
// TODO: Would be nice if we compared against a sanity hash or something before just clobbering
|
|
577
|
+
this.debug('FIXME: Force saving projects is not yet supported - this should occur in realtime anyway');
|
|
578
|
+
return Promise.resolve();
|
|
579
|
+
}
|
|
580
|
+
|
|
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() {{{
|
|
430
600
|
/**
|
|
431
601
|
* Apply a computed `just-diff` patch to the current project state
|
|
602
|
+
*
|
|
603
|
+
* @param {Object} Patch to apply
|
|
604
|
+
* @returns {Promise} A promise which resolves when the operation has completed
|
|
432
605
|
*/
|
|
433
606
|
applyProjectStatePatch(patch) {
|
|
434
|
-
|
|
607
|
+
if (!app.service('$projects').active) throw new Error('No active project to patch');
|
|
608
|
+
this.debug('Applying', patch.length, 'project state patches', {patch});
|
|
609
|
+
diffApply(app.service('$projects').active, patch, jsPatchConverter);
|
|
610
|
+
|
|
611
|
+
return Promise.resolve();
|
|
435
612
|
}
|
|
436
613
|
|
|
437
614
|
|
|
615
|
+
/**
|
|
616
|
+
* Subscribe to project state changes
|
|
617
|
+
* This will dispatch an RPC call to the source object `applyProjectStatePatchLocal()` function with the patch
|
|
618
|
+
* If the above call fails the subscriber is assumed as dead and unsubscribed from the polling list
|
|
619
|
+
*
|
|
620
|
+
* @returns {Promise<Function>} A promise which resolves when a subscription has been created, call the resulting function to unsubscribe
|
|
621
|
+
*/
|
|
622
|
+
subscribeProjectState() {
|
|
623
|
+
if (!this.messageEvent) throw new Error('senderRpc() can only be used if given a context from `createContext()`');
|
|
624
|
+
|
|
625
|
+
let subscriber = {
|
|
626
|
+
id: nanoid(),
|
|
627
|
+
origin: this.messageEvent.origin,
|
|
628
|
+
sendPatch: patch => new Promise((resolve, reject) => {
|
|
629
|
+
let senderTimeout = setTimeout(()=> {
|
|
630
|
+
reject(`Timed out sending to project-state subscriber "${subscriber.origin}"`);
|
|
631
|
+
}, this.subscribeTimeout);
|
|
632
|
+
|
|
633
|
+
return this.senderRpc.call(this, 'applyProjectStatePatchLocal', patch)
|
|
634
|
+
.then(()=> {
|
|
635
|
+
clearTimeout(senderTimeout);
|
|
636
|
+
resolve()
|
|
637
|
+
})
|
|
638
|
+
.catch(e => {
|
|
639
|
+
subscriber.unsubscribe();
|
|
640
|
+
reject(`Rejected calling RPC:applyProjectStatePatchLocal() with project-state subscriber "${subscriber.origin}" -`, e)
|
|
641
|
+
|
|
642
|
+
})
|
|
643
|
+
}),
|
|
644
|
+
unsubscribe: ()=> {
|
|
645
|
+
this.debug('Unsubscribing project-state subscriber', subscriber.origin);
|
|
646
|
+
this.projectStateSubscribers = this.projectStateSubscribers.filter(ps => ps.id != subscriber.id);
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
// Append to subscriber list
|
|
651
|
+
this.projectStateSubscribers.push(subscriber)
|
|
652
|
+
}
|
|
653
|
+
|
|
438
654
|
|
|
439
655
|
/**
|
|
440
656
|
* Subscribers to server project state changes
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
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=.",
|
|
7
7
|
"build": "concurrently 'npm:build:*'",
|
|
8
8
|
"build:client": "esbuild --platform=browser --format=esm --bundle lib/terafy.client.js --outfile=dist/terafy.js --minify --sourcemap",
|
|
9
|
-
"build:docs": "
|
|
9
|
+
"build:docs:api": "documentation build lib/terafy.client.js --format html --config documentation.yml --output docs/",
|
|
10
|
+
"build:docs:markdown": "documentation build lib/terafy.client.js --format md --markdown-toc --output api.md",
|
|
10
11
|
"lint": "eslint ."
|
|
11
12
|
},
|
|
12
13
|
"type": "module",
|
|
13
14
|
"imports": {
|
|
14
|
-
"#terafy": "./lib/terafy.client.js"
|
|
15
|
+
"#terafy": "./lib/terafy.client.js",
|
|
16
|
+
"#utils/*": "./utils/*.js"
|
|
15
17
|
},
|
|
16
18
|
"exports": {
|
|
17
19
|
".": {
|
|
@@ -59,19 +61,19 @@
|
|
|
59
61
|
"node": ">=18"
|
|
60
62
|
},
|
|
61
63
|
"peerDependencies": {
|
|
62
|
-
"just-diff": "^6.0.2",
|
|
63
64
|
"lodash-es": "^4.17.21",
|
|
64
65
|
"nanoid": "^5.0.2"
|
|
65
66
|
},
|
|
66
67
|
"optionalDependencies": {
|
|
68
|
+
"just-diff": "^6.0.2",
|
|
69
|
+
"just-diff-apply": "^5.5.0",
|
|
67
70
|
"vue": "^3.3.7"
|
|
68
71
|
},
|
|
69
72
|
"devDependencies": {
|
|
70
73
|
"@momsfriendlydevco/eslint-config": "^1.0.7",
|
|
71
74
|
"concurrently": "^8.2.2",
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"jsdoc-to-markdown": "^8.0.0"
|
|
75
|
+
"documentation": "^14.0.2",
|
|
76
|
+
"esbuild": "^0.19.5"
|
|
75
77
|
},
|
|
76
78
|
"eslintConfig": {
|
|
77
79
|
"extends": "@momsfriendlydevco",
|
|
@@ -83,5 +85,9 @@
|
|
|
83
85
|
"ecmaVersion": 13,
|
|
84
86
|
"sourceType": "module"
|
|
85
87
|
}
|
|
88
|
+
},
|
|
89
|
+
"dependencies": {
|
|
90
|
+
"@momsfriendlydevco/supabase-reactive": "^1.0.7",
|
|
91
|
+
"mitt": "^3.0.1"
|
|
86
92
|
}
|
|
87
93
|
}
|
package/plugins/vue2.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {cloneDeep} from 'lodash-es';
|
|
2
|
+
import TeraFyPluginBase from './base.js';
|
|
3
|
+
import Vue from 'vue';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Vue2 observables plugin
|
|
7
|
+
* Provides the `bindProjectState()` function for Vue based projects
|
|
8
|
+
*
|
|
9
|
+
* This function is expected to be included via the `terafy.use(MODULE, OPTIONS)` syntax rather than directly
|
|
10
|
+
*
|
|
11
|
+
* @class TeraFyPluginVue
|
|
12
|
+
*
|
|
13
|
+
* @example Implementation within a Vue2 project `src/main.js`:
|
|
14
|
+
* // Include the main Tera-Fy core
|
|
15
|
+
* import TeraFy from '@iebh/tera-fy';
|
|
16
|
+
* import TerafyVue from '@iebh/tera-fy/plugins/vue2';
|
|
17
|
+
* let terafy = new TeraFy()
|
|
18
|
+
* .set('devMode', true) // Uncomment this line if you want TeraFy to be chatty
|
|
19
|
+
* .set('siteUrl', 'http://localhost:8000/embed') // Uncomment this line if running TERA locally
|
|
20
|
+
* .use(TerafyVue) // Add the Vue plugin
|
|
21
|
+
*
|
|
22
|
+
* // Include after app boot
|
|
23
|
+
* const app = new Vue({ ... })
|
|
24
|
+
* app.$mount("#app");
|
|
25
|
+
* await terafy.init({app});
|
|
26
|
+
*/
|
|
27
|
+
export default class TeraFyPluginVue2 extends TeraFyPluginBase {
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Return a Vue Observable object that can be read/written which whose changes will transparently be written back to the TERA server instance
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
33
|
+
* @param {VueComponent} [options.component] Component to use to bind $watch events
|
|
34
|
+
* @param {String} [options.componentKey] Key within the component to attach the state. Defaults to a random string
|
|
35
|
+
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
36
|
+
* @param {String|Boolean} [options.bindKey='project'] If set, creates the binding also as the specified key within the main Tera object, if falsy just returns the observable
|
|
37
|
+
* @param {Boolean} [options.write=true] Allow local reactivity to writes - send these to the server
|
|
38
|
+
*
|
|
39
|
+
* @returns {Promie<VueObservable<Object>>} A Vue.Observable object representing the project state
|
|
40
|
+
*/
|
|
41
|
+
bindProjectState(options) {
|
|
42
|
+
let settings = {
|
|
43
|
+
component: null,
|
|
44
|
+
componentKey: null,
|
|
45
|
+
autoRequire: true,
|
|
46
|
+
write: true,
|
|
47
|
+
...options,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return Promise.resolve()
|
|
51
|
+
.then(()=> Promise.all([
|
|
52
|
+
// Fetch initial state {{{
|
|
53
|
+
this.getProjectState({
|
|
54
|
+
autoRequire: settings.autoRequire,
|
|
55
|
+
}),
|
|
56
|
+
// }}}
|
|
57
|
+
// Allocate component[componentKey] to stash our observable {{{
|
|
58
|
+
(()=> {
|
|
59
|
+
if (settings.componentKey) return; // Already allocated by user
|
|
60
|
+
for (let x = 0; x < 50; x++) {
|
|
61
|
+
let key = `terafy_${x}`;
|
|
62
|
+
if (!Object.hasOwnProperty(settings.component, key)) { // eslint-disable-line
|
|
63
|
+
settings.componentKey = key;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
throw new Error('Unable to find unique key to allocate against Vue2 component');
|
|
68
|
+
})(),
|
|
69
|
+
// }}}
|
|
70
|
+
]))
|
|
71
|
+
.then(([snapshot]) => {
|
|
72
|
+
this.debug('Got project snapshot', snapshot);
|
|
73
|
+
|
|
74
|
+
// Create initial Observable
|
|
75
|
+
let stateObservable = Vue.observable(snapshot);
|
|
76
|
+
|
|
77
|
+
// Allocate to component
|
|
78
|
+
settings.component[settings.componentKey] = stateObservable;
|
|
79
|
+
|
|
80
|
+
// Watch for remote changes and update
|
|
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
|
+
}
|
|
88
|
+
|
|
89
|
+
// Watch for local writes and react
|
|
90
|
+
if (settings.write) {
|
|
91
|
+
if (!settings.component) throw new Error('bindProjectState requires a VueComponent specified as `component`');
|
|
92
|
+
|
|
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
|
|
96
|
+
let oldVal = cloneDeep(snapshot);
|
|
97
|
+
|
|
98
|
+
settings.component.$watch(
|
|
99
|
+
settings.componentKey,
|
|
100
|
+
newVal => {
|
|
101
|
+
if (skipUpdate > 0) {
|
|
102
|
+
skipUpdate--;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.createProjectStatePatch(newVal, oldVal);
|
|
107
|
+
oldVal = cloneDeep(snapshot);
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
deep: true,
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Return Vue Reactive
|
|
116
|
+
return stateObservable;
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* List of available projects for the current session
|
|
123
|
+
* @type {VueReactive<Array<Object>>}
|
|
124
|
+
*/
|
|
125
|
+
projects = Vue.observable([]);
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The bound, reactive state of a Vue project
|
|
130
|
+
* When loaded this represents the state of a project as an object
|
|
131
|
+
* @type {Object}
|
|
132
|
+
*/
|
|
133
|
+
state = null;
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Install into Vue@2
|
|
138
|
+
*
|
|
139
|
+
* @param {Object} [options] Additional options to mutate behaviour (defaults to the main teraFy settings)
|
|
140
|
+
* @param {String} [options.globalName='$tera'] Global property to allocate this service as within Vue2
|
|
141
|
+
* @param {Boolean} [options.subscribeState=true] Setup `vm.$tera.state` as a live binding on init
|
|
142
|
+
* @param {Boolean} [options.subscribeList=true] Setup `vm.$tera.projects` as a list of accesible projects on init
|
|
143
|
+
* @param {Objecct} [options.stateOptions] Options passed to `bindProjectState()` when setting up the main state
|
|
144
|
+
*
|
|
145
|
+
* @returns {Promise} A Promise which will resolve when the init process has completed
|
|
146
|
+
*/
|
|
147
|
+
init(options) {
|
|
148
|
+
let settings = {
|
|
149
|
+
globalName: '$tera',
|
|
150
|
+
subscribeState: true,
|
|
151
|
+
subscribeProjects: true,
|
|
152
|
+
stateOptions: {
|
|
153
|
+
write: true,
|
|
154
|
+
},
|
|
155
|
+
...options,
|
|
156
|
+
};
|
|
157
|
+
if (!this.settings.app) throw new Error('Need to specify the root level Vue2 app during init');
|
|
158
|
+
settings.stateOptions.app = this.settings.app;
|
|
159
|
+
|
|
160
|
+
// Make this module available globally
|
|
161
|
+
if (settings.globalName)
|
|
162
|
+
Vue.prototype[settings.globalName] = this;
|
|
163
|
+
|
|
164
|
+
// Bind `state` to the active project
|
|
165
|
+
// Initialize state to null
|
|
166
|
+
this.state = null;
|
|
167
|
+
|
|
168
|
+
// this.statePromisable becomes the promise we are waiting on to resolve
|
|
169
|
+
return Promise.resolve()
|
|
170
|
+
.then(()=> Promise.all([
|
|
171
|
+
// Bind available project and wait on it
|
|
172
|
+
settings.subscribeState && this.bindProjectState({
|
|
173
|
+
...settings.stateOptions,
|
|
174
|
+
component: this.settings.app.$root,
|
|
175
|
+
})
|
|
176
|
+
.then(state => this.state = state)
|
|
177
|
+
.then(()=> this.debug('INFO', 'Loaded project state', this.state)),
|
|
178
|
+
|
|
179
|
+
// Fetch available projects
|
|
180
|
+
settings.subscribeProjects && this.getProjects()
|
|
181
|
+
.then(projects => this.projects = Vue.observable(projects))
|
|
182
|
+
.then(()=> this.debug('INFO', 'Loaded projects', this.projects)),
|
|
183
|
+
]))
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import TeraFyPluginBase from './base.js';
|
|
2
|
-
import {diff} from 'just-diff';
|
|
3
2
|
import {reactive, watch} from 'vue';
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -9,6 +8,20 @@ import {reactive, watch} from 'vue';
|
|
|
9
8
|
* This function is expected to be included via the `terafy.use(MODULE, OPTIONS)` syntax rather than directly
|
|
10
9
|
*
|
|
11
10
|
* @class TeraFyPluginVue
|
|
11
|
+
*
|
|
12
|
+
* @example Implementation within a Vue3 / Vite project within `src/main.js`:
|
|
13
|
+
* import TeraFy from '@iebh/tera-fy';
|
|
14
|
+
* import TerafyVue from '@iebh/tera-fy/plugins/vue';
|
|
15
|
+
* let terafy = new TeraFy()
|
|
16
|
+
* .set('devMode', import.meta.env.DEV)
|
|
17
|
+
* .set('siteUrl', 'http://localhost:8000/embed') // Uncomment this line if running TERA locally
|
|
18
|
+
* .use(TerafyVue) // Add the Vue plugin
|
|
19
|
+
*
|
|
20
|
+
* terafy.init(); // Initialize everything
|
|
21
|
+
*
|
|
22
|
+
* app.use(terafy.vuePlugin({
|
|
23
|
+
* globalName: '$tera', // Install as vm.$tera into every component
|
|
24
|
+
* }));
|
|
12
25
|
*/
|
|
13
26
|
export default class TeraFyPluginVue extends TeraFyPluginBase {
|
|
14
27
|
|
|
@@ -17,41 +30,49 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
|
|
|
17
30
|
*
|
|
18
31
|
* @param {Object} [options] Additional options to mutate behaviour
|
|
19
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
|
|
20
34
|
* @param {Boolean} [options.write=true] Allow local reactivity to writes - send these to the server
|
|
21
|
-
* @param {Array<String>} Paths to subscribe to e.g. ['/users/'],
|
|
22
35
|
*
|
|
23
36
|
* @returns {Promie<Reactive<Object>>} A reactive object representing the project state
|
|
24
37
|
*/
|
|
25
38
|
bindProjectState(options) {
|
|
26
39
|
let settings = {
|
|
27
40
|
autoRequire: true,
|
|
41
|
+
read: true,
|
|
28
42
|
write: true,
|
|
29
43
|
...options,
|
|
30
44
|
};
|
|
31
45
|
|
|
32
46
|
return Promise.resolve()
|
|
33
|
-
.then(()=> settings.autoRequire && this.requireProject())
|
|
34
47
|
.then(()=> this.getProjectState({
|
|
35
|
-
autoRequire:
|
|
36
|
-
paths: settings.paths,
|
|
48
|
+
autoRequire: settings.autoRequire ,
|
|
37
49
|
}))
|
|
38
50
|
.then(snapshot => {
|
|
39
|
-
this.debug('
|
|
51
|
+
this.debug('Fetched project snapshot', snapshot);
|
|
40
52
|
|
|
41
53
|
// Create initial reactive
|
|
42
54
|
let stateReactive = reactive(snapshot);
|
|
43
55
|
|
|
44
56
|
// Watch for remote changes and update
|
|
45
|
-
//
|
|
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
|
+
}
|
|
46
64
|
|
|
47
65
|
// Watch for local writes and react
|
|
48
66
|
if (settings.write) {
|
|
49
67
|
watch(
|
|
50
68
|
stateReactive,
|
|
51
69
|
(newVal, oldVal) => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
70
|
+
if (skipUpdate > 0) {
|
|
71
|
+
skipUpdate--;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.createProojectStatePatch(newVal, oldVal);
|
|
55
76
|
},
|
|
56
77
|
{
|
|
57
78
|
deep: true,
|
|
@@ -121,6 +142,8 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
|
|
|
121
142
|
let settings = {
|
|
122
143
|
autoInit: true,
|
|
123
144
|
globalName: '$tera',
|
|
145
|
+
subscribeState: true,
|
|
146
|
+
subscribeProjects: true,
|
|
124
147
|
stateOptions: {
|
|
125
148
|
write: true,
|
|
126
149
|
},
|
|
@@ -136,13 +159,12 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
|
|
|
136
159
|
.then(()=> settings.autoInit && $tera.init())
|
|
137
160
|
.then(()=> Promise.all([
|
|
138
161
|
// Bind available project and wait on it
|
|
139
|
-
$tera.bindProjectState(settings.stateOptions)
|
|
162
|
+
settings.subscribeState && $tera.bindProjectState(settings.stateOptions)
|
|
140
163
|
.then(state => $tera.state = state)
|
|
141
164
|
.then(()=> $tera.debug('INFO', 'Loaded project state', $tera.state)),
|
|
142
165
|
|
|
143
166
|
// Fetch available projects
|
|
144
|
-
|
|
145
|
-
$tera.getProjects()
|
|
167
|
+
settings.subscribeProjects && $tera.getProjects()
|
|
146
168
|
.then(projects => $tera.projects = reactive(projects))
|
|
147
169
|
.then(()=> $tera.debug('INFO', 'Loaded projects', $tera.projects)),
|
|
148
170
|
]))
|