@iebh/tera-fy 2.0.21 → 2.2.0
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/CHANGELOG.md +38 -0
- package/api.md +68 -66
- package/dist/lib/projectFile.d.ts +182 -0
- package/dist/lib/projectFile.js +157 -0
- package/dist/lib/projectFile.js.map +1 -0
- package/dist/lib/syncro/entities.d.ts +28 -0
- package/dist/lib/syncro/entities.js +203 -0
- package/dist/lib/syncro/entities.js.map +1 -0
- package/dist/lib/syncro/keyed.d.ts +95 -0
- package/dist/lib/syncro/keyed.js +286 -0
- package/dist/lib/syncro/keyed.js.map +1 -0
- package/dist/lib/syncro/syncro.d.ts +328 -0
- package/dist/lib/syncro/syncro.js +633 -0
- package/dist/lib/syncro/syncro.js.map +1 -0
- package/dist/lib/terafy.bootstrapper.d.ts +42 -0
- package/dist/lib/terafy.bootstrapper.js +130 -0
- package/dist/lib/terafy.bootstrapper.js.map +1 -0
- package/dist/lib/terafy.client.d.ts +532 -0
- package/dist/lib/terafy.client.js +1110 -0
- package/dist/lib/terafy.client.js.map +1 -0
- package/dist/lib/terafy.proxy.d.ts +66 -0
- package/dist/lib/terafy.proxy.js +123 -0
- package/dist/lib/terafy.proxy.js.map +1 -0
- package/dist/lib/terafy.server.d.ts +607 -0
- package/dist/lib/terafy.server.js +1774 -0
- package/dist/lib/terafy.server.js.map +1 -0
- package/dist/plugin.vue2.es2019.js +30 -13
- package/dist/plugins/base.d.ts +20 -0
- package/dist/plugins/base.js +21 -0
- package/dist/plugins/base.js.map +1 -0
- package/dist/plugins/firebase.d.ts +62 -0
- package/dist/plugins/firebase.js +111 -0
- package/dist/plugins/firebase.js.map +1 -0
- package/dist/plugins/vite.d.ts +12 -0
- package/dist/plugins/vite.js +22 -0
- package/dist/plugins/vite.js.map +1 -0
- package/dist/plugins/vue2.d.ts +68 -0
- package/dist/plugins/vue2.js +96 -0
- package/dist/plugins/vue2.js.map +1 -0
- package/dist/plugins/vue3.d.ts +64 -0
- package/dist/plugins/vue3.js +96 -0
- package/dist/plugins/vue3.js.map +1 -0
- package/dist/terafy.bootstrapper.es2019.js +2 -2
- package/dist/terafy.bootstrapper.js +2 -2
- package/dist/terafy.es2019.js +2 -2
- package/dist/terafy.js +1 -1
- package/dist/utils/mixin.d.ts +11 -0
- package/dist/utils/mixin.js +15 -0
- package/dist/utils/mixin.js.map +1 -0
- package/dist/utils/pDefer.d.ts +12 -0
- package/dist/utils/pDefer.js +14 -0
- package/dist/utils/pDefer.js.map +1 -0
- package/dist/utils/pathTools.d.ts +70 -0
- package/dist/utils/pathTools.js +120 -0
- package/dist/utils/pathTools.js.map +1 -0
- package/eslint.config.js +44 -8
- package/lib/{projectFile.js → projectFile.ts} +83 -40
- package/lib/syncro/entities.ts +288 -0
- package/lib/syncro/{keyed.js → keyed.ts} +114 -57
- package/lib/syncro/{syncro.js → syncro.ts} +204 -169
- package/lib/{terafy.bootstrapper.js → terafy.bootstrapper.ts} +49 -31
- package/lib/{terafy.client.js → terafy.client.ts} +94 -86
- package/lib/{terafy.proxy.js → terafy.proxy.ts} +43 -16
- package/lib/{terafy.server.js → terafy.server.ts} +364 -223
- package/package.json +65 -26
- package/plugins/{base.js → base.ts} +3 -1
- package/plugins/{firebase.js → firebase.ts} +34 -16
- package/plugins/{vite.js → vite.ts} +3 -3
- package/plugins/{vue2.js → vue2.ts} +17 -10
- package/plugins/{vue3.js → vue3.ts} +11 -9
- package/tsconfig.json +30 -0
- package/utils/{mixin.js → mixin.ts} +1 -1
- package/utils/{pDefer.js → pDefer.ts} +10 -3
- package/utils/{pathTools.js → pathTools.ts} +11 -9
- package/lib/syncro/entities.js +0 -232
|
@@ -0,0 +1,1110 @@
|
|
|
1
|
+
import { cloneDeep } from 'lodash-es';
|
|
2
|
+
import Mitt from 'mitt';
|
|
3
|
+
import { nanoid } from 'nanoid';
|
|
4
|
+
import ProjectFile from './projectFile.js';
|
|
5
|
+
/* globals globalThis */
|
|
6
|
+
/**
|
|
7
|
+
* Main Tera-Fy Client (class singleton) to be used in a frontend browser
|
|
8
|
+
*
|
|
9
|
+
* @class TeraFy
|
|
10
|
+
*/
|
|
11
|
+
export default class TeraFy {
|
|
12
|
+
// Messages - send(), sendRaw(), rpc(), acceptMessage() {{{
|
|
13
|
+
/**
|
|
14
|
+
* Send a message + wait for a response object
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} message Message object to send
|
|
17
|
+
* @returns {Promise<*>} A promise which resolves when the operation has completed with the remote reply
|
|
18
|
+
*/
|
|
19
|
+
send(message) {
|
|
20
|
+
let id = nanoid();
|
|
21
|
+
this.acceptPostboxes[id] = {}; // Stub for the deferred promise
|
|
22
|
+
this.acceptPostboxes[id].promise = new Promise((resolve, reject) => {
|
|
23
|
+
Object.assign(this.acceptPostboxes[id], {
|
|
24
|
+
resolve, reject,
|
|
25
|
+
});
|
|
26
|
+
this.sendRaw({
|
|
27
|
+
id,
|
|
28
|
+
...message,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
return this.acceptPostboxes[id].promise;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Send raw message content to the server
|
|
35
|
+
* This function does not return or wait for a reply - use `send()` for that
|
|
36
|
+
*
|
|
37
|
+
* @param {Object} message Message object to send
|
|
38
|
+
*/
|
|
39
|
+
sendRaw(message) {
|
|
40
|
+
let payload;
|
|
41
|
+
try {
|
|
42
|
+
payload = {
|
|
43
|
+
TERA: 1,
|
|
44
|
+
id: message.id || nanoid(),
|
|
45
|
+
...cloneDeep(message), // Need to clone to resolve promise nasties
|
|
46
|
+
};
|
|
47
|
+
if (this.settings.mode == 'parent') {
|
|
48
|
+
window.parent.postMessage(payload, this.settings.restrictOrigin);
|
|
49
|
+
}
|
|
50
|
+
else if (this.settings.mode == 'child') {
|
|
51
|
+
this.dom.iframe.contentWindow.postMessage(payload, this.settings.restrictOrigin);
|
|
52
|
+
}
|
|
53
|
+
else if (this.settings.mode == 'popup') {
|
|
54
|
+
this.dom.popup.postMessage(payload, this.settings.restrictOrigin);
|
|
55
|
+
}
|
|
56
|
+
else if (this.settings.mode == 'detect') {
|
|
57
|
+
throw new Error('Call init() or detectMode() before trying to send data to determine the mode');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
throw new Error(`Unknown TERA communication mode "${this.settings.mode}"`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
this.debug('ERROR', 1, 'Message compose client->server:', e);
|
|
65
|
+
this.debug('ERROR', 1, 'Attempted to dispatch payload client->server', payload);
|
|
66
|
+
throw e;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Call an RPC function in the server instance
|
|
71
|
+
*
|
|
72
|
+
* @param {String} method The method name to call
|
|
73
|
+
* @param {...*} [args] Optional arguments to pass to the function
|
|
74
|
+
*
|
|
75
|
+
* @returns {Promise<*>} The resolved output of the server function
|
|
76
|
+
*/
|
|
77
|
+
rpc(method, ...args) {
|
|
78
|
+
return this.send({
|
|
79
|
+
action: 'rpc',
|
|
80
|
+
method,
|
|
81
|
+
args,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Accept an incoming message
|
|
86
|
+
*
|
|
87
|
+
* @param {MessageEvent} rawMessage Raw message event to process
|
|
88
|
+
*
|
|
89
|
+
* @returns {Promise} A promise which will resolve when the message has been processed
|
|
90
|
+
*/
|
|
91
|
+
acceptMessage(rawMessage) {
|
|
92
|
+
if (rawMessage.origin == window.location.origin)
|
|
93
|
+
return Promise.resolve(); // Message came from us
|
|
94
|
+
let message = rawMessage.data;
|
|
95
|
+
if (!message.TERA || !message.id)
|
|
96
|
+
return Promise.resolve(); // Ignore non-TERA signed messages
|
|
97
|
+
this.debug('INFO', 3, 'Recieved message', message);
|
|
98
|
+
if (message?.action == 'response' && this.acceptPostboxes[message.id]) { // Postbox waiting for reply
|
|
99
|
+
if (message.isError === true) {
|
|
100
|
+
this.acceptPostboxes[message.id].reject(message.response);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
this.acceptPostboxes[message.id].resolve(message.response);
|
|
104
|
+
}
|
|
105
|
+
return Promise.resolve();
|
|
106
|
+
}
|
|
107
|
+
else if (message?.action == 'rpc') {
|
|
108
|
+
return Promise.resolve()
|
|
109
|
+
.then(() => this[message.method].apply(this, message.args))
|
|
110
|
+
.then(res => this.sendRaw({
|
|
111
|
+
id: message.id,
|
|
112
|
+
action: 'response',
|
|
113
|
+
response: res,
|
|
114
|
+
}))
|
|
115
|
+
.catch(e => {
|
|
116
|
+
console.warn(`TERA-FY client threw on RPC:${message.method}:`, e);
|
|
117
|
+
this.sendRaw({
|
|
118
|
+
id: message.id,
|
|
119
|
+
action: 'response',
|
|
120
|
+
isError: true,
|
|
121
|
+
response: e ? e.toString() : e,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
else if (message?.action == 'event') {
|
|
126
|
+
return Promise.resolve()
|
|
127
|
+
.then(() => this.events.emit(message.event, ...message.payload))
|
|
128
|
+
.catch(e => {
|
|
129
|
+
console.warn(`TERA-FY client threw while handling emitted event "${message.event}"`, { message });
|
|
130
|
+
throw e;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
else if (message?.id) {
|
|
134
|
+
this.debug('INFO', 3, `Ignoring message ID ${message.id} - was meant for someone else?`);
|
|
135
|
+
return Promise.resolve();
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
this.debug('INFO', 3, 'Unexpected incoming TERA-FY CLIENT message', { message });
|
|
139
|
+
return Promise.resolve();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// }}}
|
|
143
|
+
// Project namespace - mountNamespace(), unmountNamespace() {{{
|
|
144
|
+
/**
|
|
145
|
+
* Make a namespace available locally
|
|
146
|
+
* This generally creates whatever framework flavoured reactive/observer/object is supported locally - generally with writes automatically synced with the master state
|
|
147
|
+
*
|
|
148
|
+
* @function mountNamespace
|
|
149
|
+
* @param {String} name The alias of the namespace, this should be alphanumeric + hyphens + underscores
|
|
150
|
+
*
|
|
151
|
+
* @returns {Promise<Reactive>} A promise which resolves to the reactive object
|
|
152
|
+
*/
|
|
153
|
+
mountNamespace(name) {
|
|
154
|
+
if (!/^[\w-]+$/.test(name))
|
|
155
|
+
throw new Error('Namespaces must be alphanumeric + hyphens + underscores');
|
|
156
|
+
if (this.namespaces[name])
|
|
157
|
+
return Promise.resolve(this.namespaces[name]); // Already mounted
|
|
158
|
+
return Promise.resolve()
|
|
159
|
+
.then(() => this._mountNamespace(name))
|
|
160
|
+
.then(() => this.namespaces[name] || Promise.reject(`teraFy.mountNamespace('${name}') resolved but no namespace has been mounted`));
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* @interface
|
|
164
|
+
* Actual namespace mounting function designed to be overriden by plugins
|
|
165
|
+
*
|
|
166
|
+
* @param {String} name The alias of the namespace, this should be alphanumeric + hyphens + underscores
|
|
167
|
+
*
|
|
168
|
+
* @returns {Promise} A promise which resolves when the mount operation has completed
|
|
169
|
+
*/
|
|
170
|
+
_mountNamespace(name) {
|
|
171
|
+
console.warn('teraFy._mountNamespace() has not been overriden by a TERA-fy plugin, load one to add this functionality for your preferred framework');
|
|
172
|
+
throw new Error('teraFy._mountNamespace() is not supported');
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Release a locally mounted namespace
|
|
176
|
+
* This function will remove the namespace from `namespaces`, cleaning up any memory / subscription hooks
|
|
177
|
+
*
|
|
178
|
+
* @function unmountNamespace
|
|
179
|
+
*
|
|
180
|
+
* @param {String} name The name of the namespace to unmount
|
|
181
|
+
*
|
|
182
|
+
* @returns {Promise} A promise which resolves when the operation has completed
|
|
183
|
+
*/
|
|
184
|
+
unmountNamespace(name) {
|
|
185
|
+
if (!this.namespaces[name])
|
|
186
|
+
return Promise.resolve(); // Already unmounted
|
|
187
|
+
return this._unmountNamespace(name);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* @interface
|
|
191
|
+
* Actual namespace unmounting function designed to be overriden by plugins
|
|
192
|
+
*
|
|
193
|
+
* @param {String} name The name of the namespace to unmount
|
|
194
|
+
*
|
|
195
|
+
* @returns {Promise} A promise which resolves when the operation has completed
|
|
196
|
+
*/
|
|
197
|
+
_unmountNamespace(name) {
|
|
198
|
+
console.warn('teraFy.unbindNamespace() has not been overriden by a TERA-fy plugin, load one to add this functionality for your preferred framework');
|
|
199
|
+
}
|
|
200
|
+
// }}}
|
|
201
|
+
// Init - constructor(), init(), inject*() {{{
|
|
202
|
+
/**
|
|
203
|
+
* Setup the TERA-fy client singleton
|
|
204
|
+
*
|
|
205
|
+
* @param {Object} [options] Additional options to merge into `settings` via `set`
|
|
206
|
+
*/
|
|
207
|
+
constructor(options) {
|
|
208
|
+
/**
|
|
209
|
+
* Various settings to configure behaviour
|
|
210
|
+
*
|
|
211
|
+
* @type {Object}
|
|
212
|
+
* @property {String} session Unique session signature for this instance of TeraFy, used to sign server messages, if falsy `getEntropicString(16)` is used to populate
|
|
213
|
+
* @property {Boolean} devMode Operate in Dev-Mode - i.e. force outer refresh when encountering an existing TeraFy instance + be more tolerent of weird iframe origins
|
|
214
|
+
* @property {Number} verbosity Verbosity level, the higher the more chatty TeraFY will be. Set to zero to disable all `debug()` call output
|
|
215
|
+
* @property {'detect'|'parent'|'child'|'popup'} mode How to communicate with TERA. 'parent' assumes that the parent of the current document is TERA, 'child' spawns an iFrame and uses TERA there, 'detect' tries parent and switches to `modeFallback` if communication fails
|
|
216
|
+
* @property {String} modeFallback Method to use when all method detection fails
|
|
217
|
+
* @property {Object<Object<Function>>} modeOverrides Functions to run when switching to specific modes, these are typically used to augment config. Called as `(config:Object)`
|
|
218
|
+
* @property {Number} modeTimeout How long entities have in 'detect' mode to identify themselves
|
|
219
|
+
* @property {String} siteUrl The TERA URL to connect to
|
|
220
|
+
* @property {String} restrictOrigin URL to restrict communications to
|
|
221
|
+
* @property {Array<String>} List of sandbox allowables for the embedded if in embed mode
|
|
222
|
+
* @property {Number} handshakeInterval Interval in milliseconds when sanning for a handshake
|
|
223
|
+
* @property {Number} handshakeTimeout Interval in milliseconds for when to give up trying to handshake
|
|
224
|
+
* @property {Array<String|Array<String>>} [debugPaths] List of paths (in either dotted or array notation) to enter debugging mode if a change is detected in dev mode e.g. `{debugPaths: ['foo.bar.baz']}`. This really slows down state writes so should only be used for debugging
|
|
225
|
+
*/
|
|
226
|
+
this.settings = {
|
|
227
|
+
session: null,
|
|
228
|
+
// client: 'tera-fy', // Reserved by terafy.bootstrapper.js
|
|
229
|
+
// clientType: 'esm', // Reserved by terafy.bootstrapper.js
|
|
230
|
+
devMode: false,
|
|
231
|
+
verbosity: 1,
|
|
232
|
+
mode: 'detect',
|
|
233
|
+
modeTimeout: 300,
|
|
234
|
+
modeFallback: 'child', // ENUM: 'child' (use iframes), 'popup' (use popup windows)
|
|
235
|
+
modeOverrides: {
|
|
236
|
+
child(config) {
|
|
237
|
+
if (config.siteUrl == 'https://tera-tools.com/embed') { // Only if we're using the default URL...
|
|
238
|
+
config.siteUrl = 'https://dev.tera-tools.com/embed'; // Repoint URL to dev site
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
siteUrl: 'https://tera-tools.com/embed',
|
|
243
|
+
restrictOrigin: '*',
|
|
244
|
+
frameSandbox: [
|
|
245
|
+
'allow-forms',
|
|
246
|
+
'allow-modals',
|
|
247
|
+
'allow-orientation-lock',
|
|
248
|
+
'allow-pointer-lock',
|
|
249
|
+
'allow-popups',
|
|
250
|
+
'allow-popups-to-escape-sandbox',
|
|
251
|
+
'allow-presentation',
|
|
252
|
+
'allow-same-origin',
|
|
253
|
+
'allow-scripts',
|
|
254
|
+
'allow-top-navigation',
|
|
255
|
+
],
|
|
256
|
+
handshakeInterval: 1000, // ~1s
|
|
257
|
+
handshakeTimeout: 10000, // ~10s
|
|
258
|
+
debugPaths: null, // Transformed into a Array<String> (in Lodash dotted notation) on init()
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* Event emitter subscription endpoint
|
|
262
|
+
* @type {Mitt}
|
|
263
|
+
*/
|
|
264
|
+
// @ts-ignore
|
|
265
|
+
this.events = Mitt();
|
|
266
|
+
/**
|
|
267
|
+
* DOMElements for this TeraFy instance
|
|
268
|
+
*
|
|
269
|
+
* @type {Object}
|
|
270
|
+
* @property {DOMElement} el The main tera-fy div wrapper
|
|
271
|
+
* @property {DOMElement} iframe The internal iFrame element (if `settings.mode == 'child'`)
|
|
272
|
+
* @property {Window} popup The popup window context (if `settings.mode == 'popup'`)
|
|
273
|
+
* @property {DOMElement} stylesheet The corresponding stylesheet
|
|
274
|
+
*/
|
|
275
|
+
this.dom = {
|
|
276
|
+
el: null,
|
|
277
|
+
iframe: null,
|
|
278
|
+
popup: null,
|
|
279
|
+
stylesheet: null,
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* List of function stubs mapped from the server to here
|
|
283
|
+
* This array is forms the reference of `TeraFy.METHOD()` objects to provide locally which will be mapped via `TeraFy.rpc(METHOD, ...args)`
|
|
284
|
+
*
|
|
285
|
+
* @type {Array<String>}
|
|
286
|
+
*/
|
|
287
|
+
this.methods = [
|
|
288
|
+
// Messages
|
|
289
|
+
'handshake',
|
|
290
|
+
'setServerVerbosity',
|
|
291
|
+
// Session
|
|
292
|
+
'getUser',
|
|
293
|
+
'requireUser',
|
|
294
|
+
'getCredentials',
|
|
295
|
+
// Projects
|
|
296
|
+
'bindProject',
|
|
297
|
+
'getProject',
|
|
298
|
+
'getProjects',
|
|
299
|
+
'setActiveProject',
|
|
300
|
+
'requireProject',
|
|
301
|
+
'selectProject',
|
|
302
|
+
// Project namespaces
|
|
303
|
+
// 'mountNamespace', // Handled by this library
|
|
304
|
+
// 'unmountNamespace', // Handled by this library
|
|
305
|
+
'getNamespace',
|
|
306
|
+
'setNamespace',
|
|
307
|
+
'listNamespaces',
|
|
308
|
+
// Project State
|
|
309
|
+
'getProjectState',
|
|
310
|
+
'setProjectState',
|
|
311
|
+
'setProjectStateDefaults',
|
|
312
|
+
'setProjectStateRefresh',
|
|
313
|
+
// Project files
|
|
314
|
+
// 'selectProjectFile', - Handled below (requires return collection mapped to ProjectFile)
|
|
315
|
+
// 'getProjectFiles', - Handled below (requires return collection mapped to ProjectFile)
|
|
316
|
+
// 'getProjectFile', - Handled below (requires return mapped to ProjectFile)
|
|
317
|
+
'getProjectFileContents',
|
|
318
|
+
// 'createProjectFile', - Handled below (requires return mapped to ProjectFile)
|
|
319
|
+
'deleteProjectFile',
|
|
320
|
+
'setProjectFileContents',
|
|
321
|
+
// Project Libraries
|
|
322
|
+
'selectProjectLibrary',
|
|
323
|
+
'getProjectLibrary',
|
|
324
|
+
'setProjectLibrary',
|
|
325
|
+
// Project Logging
|
|
326
|
+
'projectLog',
|
|
327
|
+
// Webpages
|
|
328
|
+
'setPage',
|
|
329
|
+
// UI
|
|
330
|
+
'uiAlert',
|
|
331
|
+
'uiConfirm',
|
|
332
|
+
'uiPanic',
|
|
333
|
+
'uiProgress',
|
|
334
|
+
'uiPrompt',
|
|
335
|
+
'uiThrow',
|
|
336
|
+
'uiSplat',
|
|
337
|
+
'uiWindow',
|
|
338
|
+
];
|
|
339
|
+
/**
|
|
340
|
+
* Loaded plugins via Use()
|
|
341
|
+
* @type {Array<TeraFyPlugin>}
|
|
342
|
+
*/
|
|
343
|
+
this.plugins = [];
|
|
344
|
+
/**
|
|
345
|
+
* Active namespaces we are subscribed to
|
|
346
|
+
* Each key is the namespace name with the value as the local reactive \ observer \ object equivelent
|
|
347
|
+
* The key string is always of the form `${ENTITY}::${ID}` e.g. `projects:1234`
|
|
348
|
+
*
|
|
349
|
+
* @type {Object<Object>}
|
|
350
|
+
*/
|
|
351
|
+
this.namespaces = {};
|
|
352
|
+
/**
|
|
353
|
+
* Listening postboxes, these correspond to outgoing message IDs that expect a response
|
|
354
|
+
*/
|
|
355
|
+
this.acceptPostboxes = {};
|
|
356
|
+
this.initPromise = null;
|
|
357
|
+
if (options)
|
|
358
|
+
this.set(options);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Initalize the TERA client singleton
|
|
362
|
+
* This function can only be called once and will return the existing init() worker Promise if its called againt
|
|
363
|
+
*
|
|
364
|
+
* @param {Object} [options] Additional options to merge into `settings` via `set`
|
|
365
|
+
* @returns {Promise<TeraFy>} An eventual promise which will resovle with this terafy instance
|
|
366
|
+
*/
|
|
367
|
+
init(options) {
|
|
368
|
+
if (options)
|
|
369
|
+
this.set(options);
|
|
370
|
+
if (this.initPromise)
|
|
371
|
+
return this.initPromise; // Aleady been called - return init promise
|
|
372
|
+
window.addEventListener('message', this.acceptMessage.bind(this));
|
|
373
|
+
const context = this;
|
|
374
|
+
this.initPromise = Promise.resolve()
|
|
375
|
+
.then(() => { var _a; return (_a = this.settings).session || (_a.session = 'tfy-' + this.getEntropicString(16)); })
|
|
376
|
+
.then(() => this.debug('INFO', 4, '[0/6] Init', 'Session', this.settings.session, 'against', this.settings.siteUrl))
|
|
377
|
+
.then(() => {
|
|
378
|
+
if (!this.settings.devMode)
|
|
379
|
+
return; // Not in dev mode
|
|
380
|
+
if (this.settings.debugPaths) {
|
|
381
|
+
this.settings.debugPaths = this.settings.debugPaths.map((path) => Array.isArray(path) ? path.join('.') // Transform arrays into dotted notation
|
|
382
|
+
: typeof path == 'string' ? path // Assume already in dotted notation
|
|
383
|
+
: (() => { throw new Error('Unknown path type - should be an array or string in dotted notation'); })());
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
this.settings.debugPaths = null;
|
|
387
|
+
}
|
|
388
|
+
this.debug('INFO', 0, 'Watching state paths', this.settings.debugPaths);
|
|
389
|
+
})
|
|
390
|
+
.then(() => this.detectMode())
|
|
391
|
+
.then(mode => {
|
|
392
|
+
this.debug('INFO', 4, '[1/6] Setting client mode to', mode);
|
|
393
|
+
this.settings.mode = mode;
|
|
394
|
+
if (this.settings.modeOverrides[mode]) {
|
|
395
|
+
this.debug('INFO', 4, '[1/6] Applying specific config overrides for mode', mode);
|
|
396
|
+
return this.settings.modeOverrides[mode](this.settings);
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
.then(() => this.debug('INFO', 4, '[2/6] Injecting comms + styles + methods'))
|
|
400
|
+
.then(() => Promise.all([
|
|
401
|
+
// Init core functions async
|
|
402
|
+
this.injectComms(),
|
|
403
|
+
this.injectStylesheet(),
|
|
404
|
+
this.injectMethods(),
|
|
405
|
+
]))
|
|
406
|
+
.then(() => {
|
|
407
|
+
if (this.settings.verbosity <= 1) {
|
|
408
|
+
this.debug('INFO', 4, '[3/6] Skip - Server verbosity is already at 1');
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
this.debug('INFO', 4, `[3/6] Set server verbosity to ${this.settings.verbosity}`);
|
|
413
|
+
return this.rpc('setServerVerbosity', this.settings.verbosity);
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
.then(() => this.debug('INFO', 4, `[4/6] Set server mode to "${this.settings.mode}"`))
|
|
417
|
+
.then(() => this.rpc('setServerMode', // Tell server what mode its in
|
|
418
|
+
this.settings.mode == 'child' ? 'embedded'
|
|
419
|
+
: this.settings.mode == 'parent' ? 'frame'
|
|
420
|
+
: this.settings.mode == 'popup' ? 'popup'
|
|
421
|
+
: (() => { throw (`Unknown server mode "${this.settings.mode}"`); })()))
|
|
422
|
+
.then(() => this.debug('INFO', 4, '[5/6] Run client plugins'))
|
|
423
|
+
.then(() => Promise.all(// Init all plugins (with this outer module as the context)
|
|
424
|
+
this.plugins.map(plugin => plugin.init.call(context, this.settings))))
|
|
425
|
+
.then(() => {
|
|
426
|
+
this.debug('INFO', 4, '[6/6] Init complete');
|
|
427
|
+
return context; // Resolve with the instance
|
|
428
|
+
})
|
|
429
|
+
.catch(e => {
|
|
430
|
+
this.debug('WARN', 0, 'Init process fault', e);
|
|
431
|
+
throw e; // Re-throw
|
|
432
|
+
});
|
|
433
|
+
return this.initPromise;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Populate `settings.mode`
|
|
437
|
+
* Try to communicate with a parent frame, if none assume we need to fallback to child mode
|
|
438
|
+
*
|
|
439
|
+
* @returns {Promise<String>} A promise which will resolve with the detected mode to use
|
|
440
|
+
*/
|
|
441
|
+
detectMode() {
|
|
442
|
+
if (this.settings.mode != 'detect') { // Dev has specified a forced mode to use
|
|
443
|
+
return Promise.resolve(this.settings.mode);
|
|
444
|
+
}
|
|
445
|
+
else if (window.self === window.parent) { // This frame is already at the top
|
|
446
|
+
return Promise.resolve(this.settings.modeFallback);
|
|
447
|
+
}
|
|
448
|
+
else { // No idea - try messaging
|
|
449
|
+
return Promise.resolve()
|
|
450
|
+
.then(() => this.settings.mode = 'parent') // Switch to parent mode...
|
|
451
|
+
.then(() => new Promise((resolve, reject) => {
|
|
452
|
+
let timeoutHandle = setTimeout(() => reject('TIMEOUT'), this.settings.modeTimeout);
|
|
453
|
+
this.rpc('handshake')
|
|
454
|
+
.then(() => clearTimeout(timeoutHandle))
|
|
455
|
+
.then(() => resolve(undefined))
|
|
456
|
+
.catch(reject); // Propagate RPC errors
|
|
457
|
+
}))
|
|
458
|
+
.then(() => 'parent')
|
|
459
|
+
.catch(() => this.settings.modeFallback);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Find an existing active TERA server OR initalize one
|
|
464
|
+
*
|
|
465
|
+
* @returns {Promise} A promise which will resolve when the loading has completed and we have found a parent TERA instance or initiallized a child
|
|
466
|
+
*/
|
|
467
|
+
injectComms() {
|
|
468
|
+
switch (this.settings.mode) {
|
|
469
|
+
case 'child': return Promise.resolve()
|
|
470
|
+
.then(() => new Promise(resolve => {
|
|
471
|
+
this.debug('INFO', 2, 'Injecting TERA site as iFrame child');
|
|
472
|
+
this.dom.el = document.createElement('div');
|
|
473
|
+
this.dom.el.id = 'tera-fy';
|
|
474
|
+
this.dom.el.classList.toggle('dev-mode', this.settings.devMode);
|
|
475
|
+
this.dom.el.classList.add('minimized');
|
|
476
|
+
document.body.append(this.dom.el);
|
|
477
|
+
this.dom.el.addEventListener('click', () => this.dom.el.classList.toggle('minimized'));
|
|
478
|
+
this.dom.iframe = document.createElement('iframe');
|
|
479
|
+
// Queue up event chain when document loads
|
|
480
|
+
this.dom.iframe.setAttribute('sandbox', this.settings.frameSandbox.join(' '));
|
|
481
|
+
this.dom.iframe.addEventListener('load', () => {
|
|
482
|
+
this.debug('INFO', 3, 'Embeded iframe ready');
|
|
483
|
+
resolve(undefined);
|
|
484
|
+
});
|
|
485
|
+
// Start document load sequence + append to DOM
|
|
486
|
+
this.dom.iframe.src = this.settings.siteUrl;
|
|
487
|
+
this.dom.el.append(this.dom.iframe);
|
|
488
|
+
}))
|
|
489
|
+
.then(() => this.handshakeLoop());
|
|
490
|
+
case 'parent':
|
|
491
|
+
this.debug('INFO', 2, 'Using TERA window parent');
|
|
492
|
+
return Promise.resolve();
|
|
493
|
+
case 'popup':
|
|
494
|
+
this.debug('INFO', 2, 'Injecting TERA site as a popup window');
|
|
495
|
+
this.dom.popup = window.open(this.settings.siteUrl, '_blank', 'popup=1, location=0, menubar=0, status=0, scrollbars=0, width=500, height=600');
|
|
496
|
+
return this.handshakeLoop()
|
|
497
|
+
.then(() => this.debug('INFO', 3, 'Popup window accepted handshake'));
|
|
498
|
+
default:
|
|
499
|
+
throw new Error(`Unsupported mode "${this.settings.mode}" when calling injectComms()`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Keep trying to handshake until the target responds
|
|
504
|
+
*
|
|
505
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
506
|
+
* @property {Number} [handshakeInterval] Interval in milliseconds when sanning for a handshake, defaults to global setting
|
|
507
|
+
* @property {Number} [handshakeTimeout] Interval in milliseconds for when to give up trying to handshake, defaults to global setting
|
|
508
|
+
*
|
|
509
|
+
* @returns {Promise} A promise which will either resolve when the handshake is successful OR fail with 'TIMEOUT'
|
|
510
|
+
*/
|
|
511
|
+
handshakeLoop(options) {
|
|
512
|
+
let settings = {
|
|
513
|
+
handshakeInterval: this.settings.handshakeInterval,
|
|
514
|
+
handshakeTimeout: this.settings.handshakeTimeout,
|
|
515
|
+
...options,
|
|
516
|
+
};
|
|
517
|
+
// Loop until the window context returns a handshake
|
|
518
|
+
return new Promise((resolve, reject) => {
|
|
519
|
+
let handshakeCount = 0;
|
|
520
|
+
let handshakeTimer;
|
|
521
|
+
let handshakeTimeout = setTimeout(() => {
|
|
522
|
+
clearTimeout(handshakeTimer);
|
|
523
|
+
reject('TIMEOUT');
|
|
524
|
+
}, settings.handshakeTimeout);
|
|
525
|
+
const tryHandshake = () => {
|
|
526
|
+
this.debug('INFO', 4, 'Trying handshake', ++handshakeCount);
|
|
527
|
+
clearTimeout(handshakeTimer);
|
|
528
|
+
handshakeTimer = setTimeout(tryHandshake, settings.handshakeInterval);
|
|
529
|
+
this.rpc('handshake')
|
|
530
|
+
.then(() => {
|
|
531
|
+
clearTimeout(handshakeTimeout);
|
|
532
|
+
clearTimeout(handshakeTimer);
|
|
533
|
+
})
|
|
534
|
+
.then(() => resolve(undefined))
|
|
535
|
+
.catch(reject); // Let RPC errors propagate
|
|
536
|
+
};
|
|
537
|
+
tryHandshake(); // Kick off initial handshake
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Inject a local stylesheet to handle TERA server functionality
|
|
542
|
+
*
|
|
543
|
+
* @returns {Promise} A promise which will resolve when the loading has completed and we have found a parent TERA instance or initiallized a child
|
|
544
|
+
*/
|
|
545
|
+
injectStylesheet() {
|
|
546
|
+
switch (this.settings.mode) {
|
|
547
|
+
case 'child':
|
|
548
|
+
this.dom.stylesheet = document.createElement('style');
|
|
549
|
+
this.dom.stylesheet.innerHTML = [
|
|
550
|
+
':root {',
|
|
551
|
+
'--TERA-accent: #4d659c;',
|
|
552
|
+
'}',
|
|
553
|
+
'#tera-fy {',
|
|
554
|
+
'display: none;',
|
|
555
|
+
'position: fixed;',
|
|
556
|
+
'right: 50px;',
|
|
557
|
+
'bottom: 50px;',
|
|
558
|
+
'width: 300px;',
|
|
559
|
+
'height: 150px;',
|
|
560
|
+
'background: transparent;',
|
|
561
|
+
// Minimize / de-minimize functionality {{{
|
|
562
|
+
'body:not(.tera-fy-focus) &.minimized.dev-mode {',
|
|
563
|
+
'background: var(--TERA-accent) !important;',
|
|
564
|
+
'opacity: 0.5;',
|
|
565
|
+
'right: 0px;',
|
|
566
|
+
'bottom: 0px;',
|
|
567
|
+
'width: 30px;',
|
|
568
|
+
'height: 30px;',
|
|
569
|
+
'transition: opacity 0.2s ease-out;',
|
|
570
|
+
'cursor: pointer;',
|
|
571
|
+
'border: none;',
|
|
572
|
+
'border-top-left-radius: 30px;',
|
|
573
|
+
'border-top: 2px solid var(--TERA-accent);',
|
|
574
|
+
'border-left: 2px solid var(--TERA-accent);',
|
|
575
|
+
'display: flex;',
|
|
576
|
+
'justify-content: center;',
|
|
577
|
+
'align-items: center;',
|
|
578
|
+
'&::before {',
|
|
579
|
+
'margin: 2px 0 0 0;',
|
|
580
|
+
'content: "🌀";',
|
|
581
|
+
'color: #FFF;',
|
|
582
|
+
'}',
|
|
583
|
+
'&:hover {',
|
|
584
|
+
'opacity: 1;',
|
|
585
|
+
'}',
|
|
586
|
+
'& > iframe {',
|
|
587
|
+
'display: none;',
|
|
588
|
+
'}',
|
|
589
|
+
'}',
|
|
590
|
+
'body:not(.tera-fy-focus) &:not(.minimized) {',
|
|
591
|
+
'&::before {',
|
|
592
|
+
'display: flex;',
|
|
593
|
+
'align-items: center;',
|
|
594
|
+
'justify-content: center;',
|
|
595
|
+
'cursor: pointer;',
|
|
596
|
+
'background: var(--TERA-accent) !important;',
|
|
597
|
+
'opacity: 0.5;',
|
|
598
|
+
'transition: opacity 0.2s ease-out;',
|
|
599
|
+
'position: absolute;',
|
|
600
|
+
'right: 0px;',
|
|
601
|
+
'bottom: 0px;',
|
|
602
|
+
'width: 20px;',
|
|
603
|
+
'height: 20px;',
|
|
604
|
+
'margin: 2px 0 0 0;',
|
|
605
|
+
'content: "⭨";',
|
|
606
|
+
'color: #FFF;',
|
|
607
|
+
'border: none;',
|
|
608
|
+
'border-top-left-radius: 30px;',
|
|
609
|
+
'border-top: 2px solid var(--TERA-accent);',
|
|
610
|
+
'border-left: 2px solid var(--TERA-accent);',
|
|
611
|
+
'}',
|
|
612
|
+
'&:hover::before {',
|
|
613
|
+
'opacity: 1;',
|
|
614
|
+
'}',
|
|
615
|
+
'}',
|
|
616
|
+
// }}}
|
|
617
|
+
'&.dev-mode {',
|
|
618
|
+
'display: flex;',
|
|
619
|
+
'border: 5px solid var(--TERA-accent);',
|
|
620
|
+
'background: #FFF;',
|
|
621
|
+
'}',
|
|
622
|
+
'& > iframe {',
|
|
623
|
+
'width: 100%;',
|
|
624
|
+
'height: 100%;',
|
|
625
|
+
'}',
|
|
626
|
+
'}',
|
|
627
|
+
// Fullscreen functionality {{{
|
|
628
|
+
'body.tera-fy-focus {',
|
|
629
|
+
'overflow: hidden;',
|
|
630
|
+
'& #tera-fy {',
|
|
631
|
+
'display: flex !important;',
|
|
632
|
+
'position: fixed !important;',
|
|
633
|
+
'top: 0px !important;',
|
|
634
|
+
'width: 100vw !important;',
|
|
635
|
+
'height: 100vh !important;',
|
|
636
|
+
'left: 0px !important;',
|
|
637
|
+
'z-index: 10000 !important;',
|
|
638
|
+
'}',
|
|
639
|
+
'}',
|
|
640
|
+
// }}}
|
|
641
|
+
].join('\n');
|
|
642
|
+
document.head.appendChild(this.dom.stylesheet);
|
|
643
|
+
break;
|
|
644
|
+
case 'parent':
|
|
645
|
+
case 'popup':
|
|
646
|
+
break;
|
|
647
|
+
default:
|
|
648
|
+
throw new Error(`Unsupported mode "${this.settings.mode}" when injectStylesheet()`);
|
|
649
|
+
}
|
|
650
|
+
return Promise.resolve();
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Inject all server methods defined in `methods` as local functions wrapped in the `rpc` function
|
|
654
|
+
*/
|
|
655
|
+
injectMethods() {
|
|
656
|
+
this.methods.forEach(method => this[method] = this.rpc.bind(this, method));
|
|
657
|
+
}
|
|
658
|
+
// }}}
|
|
659
|
+
// Utility - debug(), set(), setIfDev(), use(), mixin(), toggleDevMode(), toggleFocus(), getEntropicString() {{{
|
|
660
|
+
/* eslint-disable jsdoc/check-param-names */
|
|
661
|
+
/**
|
|
662
|
+
* Debugging output function
|
|
663
|
+
* This function will only act if `settings.devMode` is truthy
|
|
664
|
+
*
|
|
665
|
+
* @param {'INFO'|'LOG'|'WARN'|'ERROR'} [method='LOG'] Logging method to use
|
|
666
|
+
* @param {Number} [verboseLevel=1] The verbosity level to trigger at. If `settings.verbosity` is lower than this, the message is ignored
|
|
667
|
+
* @param {...*} [msg] Output to show
|
|
668
|
+
*/
|
|
669
|
+
debug(...msg) {
|
|
670
|
+
if (!this.settings.devMode || this.settings.verbosity < 1)
|
|
671
|
+
return; // Debugging is disabled
|
|
672
|
+
let method = 'log';
|
|
673
|
+
let verboseLevel = 1;
|
|
674
|
+
// Argument mangling for prefix method + verbosity level {{{
|
|
675
|
+
if (typeof msg[0] == 'string' && ['INFO', 'LOG', 'WARN', 'ERROR'].includes(msg[0])) {
|
|
676
|
+
method = msg.shift().toLowerCase();
|
|
677
|
+
}
|
|
678
|
+
if (typeof msg[0] == 'number') {
|
|
679
|
+
verboseLevel = msg[0];
|
|
680
|
+
msg.shift();
|
|
681
|
+
}
|
|
682
|
+
// }}}
|
|
683
|
+
if (this.settings.verbosity < verboseLevel)
|
|
684
|
+
return; // Called but this output is too verbose for our settings - skip
|
|
685
|
+
console[method]('%c[TERA-FY CLIENT]', 'font-weight: bold; color: #ff5722;', ...msg);
|
|
686
|
+
}
|
|
687
|
+
/* eslint-enable */
|
|
688
|
+
/**
|
|
689
|
+
* Set or merge settings
|
|
690
|
+
* This function also routes 'special' keys like `devMode` to their internal handlers
|
|
691
|
+
*
|
|
692
|
+
* @param {String|Object} key Either a single setting key to set or an object to merge
|
|
693
|
+
* @param {*} value The value to set if `key` is a string
|
|
694
|
+
*
|
|
695
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
696
|
+
* @param {Boolean} [options.ignoreNullish=true] If falsy, this forces the setting of undefined or null values rather than ignoring them when specifying values by string
|
|
697
|
+
*
|
|
698
|
+
* @returns {TeraFy} This chainable terafy instance
|
|
699
|
+
*/
|
|
700
|
+
set(key, value, options) {
|
|
701
|
+
let settings = {
|
|
702
|
+
ignoreNullish: true,
|
|
703
|
+
...options,
|
|
704
|
+
};
|
|
705
|
+
if (typeof key == 'string') {
|
|
706
|
+
if (!settings.ignoreNullish || (value !== null && value !== undefined))
|
|
707
|
+
this.settings[key] = value;
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
Object.assign(this.settings, key);
|
|
711
|
+
}
|
|
712
|
+
return this.toggleDevMode(this.settings.devMode);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Set or merge settings - but only in dev mode and only if the value is not undefined
|
|
716
|
+
*
|
|
717
|
+
* @see set()
|
|
718
|
+
* @param {String|Object} key Either a single setting key to set or an object to merge
|
|
719
|
+
* @param {*} value The value to set if `key` is a string
|
|
720
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
721
|
+
*
|
|
722
|
+
* @returns {TeraFy} This chainable terafy instance
|
|
723
|
+
*/
|
|
724
|
+
setIfDev(key, value, options) {
|
|
725
|
+
if (!this.settings.devMode || value === undefined)
|
|
726
|
+
return this;
|
|
727
|
+
return this.set(key, value, options);
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Include a TeraFy client plugin
|
|
731
|
+
*
|
|
732
|
+
* @param {Function|Object|String} source Either the JS module class, singleton object or URL to fetch it from. Eventually constructed as invoked as `(teraClient:TeraFy, options:Object)`
|
|
733
|
+
* @param {Object} [options] Additional options to mutate behaviour during construction (pass options to init() to intialize later options)
|
|
734
|
+
*
|
|
735
|
+
* @returns {TeraFy} This chainable terafy instance
|
|
736
|
+
*/
|
|
737
|
+
use(source, options) {
|
|
738
|
+
let mod = typeof source == 'function' ? new source(this, options)
|
|
739
|
+
: typeof source == 'object' ? source
|
|
740
|
+
: typeof source == 'string' ? (() => { throw new Error('use(String) is not yet supported'); })()
|
|
741
|
+
: (() => { throw new Error('Expected use() call to be provided with a class initalizer'); })();
|
|
742
|
+
this.mixin(this, mod);
|
|
743
|
+
this.plugins.push(mod);
|
|
744
|
+
return this;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Internal function used by use() to merge an external declared singleton against this object
|
|
748
|
+
*
|
|
749
|
+
* @param {Object} target Initalied class instance to extend
|
|
750
|
+
* @param {Object} source Initalized source object to extend from
|
|
751
|
+
*/
|
|
752
|
+
mixin(target, source) {
|
|
753
|
+
// Iterate through the source object upwards extracting each prototype
|
|
754
|
+
let prototypeStack = [];
|
|
755
|
+
let node = source;
|
|
756
|
+
do {
|
|
757
|
+
prototypeStack.unshift(node);
|
|
758
|
+
} while (node = Object.getPrototypeOf(node)); // Walk upwards until we hit null (no more inherited classes)
|
|
759
|
+
// Iterate through stacks inheriting each prop into the target
|
|
760
|
+
prototypeStack.forEach(stack => Object.getOwnPropertyNames(stack)
|
|
761
|
+
.filter(prop => !['constructor', 'init', 'prototype', 'name'].includes(prop) // Ignore forbidden properties
|
|
762
|
+
&& !prop.startsWith('__') // Ignore double underscore meta properties
|
|
763
|
+
)
|
|
764
|
+
.forEach(prop => {
|
|
765
|
+
if (typeof source[prop] == 'function') { // Inheriting function - glue onto object as non-editable, non-enumerable property
|
|
766
|
+
Object.defineProperty(target, prop, {
|
|
767
|
+
enumerable: false,
|
|
768
|
+
value: source[prop].bind(target), // Rebind functions
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
else { // Everything else, just glue onto the object
|
|
772
|
+
target[prop] = source[prop];
|
|
773
|
+
}
|
|
774
|
+
}));
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Set or toggle devMode
|
|
778
|
+
* This function also accepts meta values:
|
|
779
|
+
*
|
|
780
|
+
* 'toggle' - Set dev mode to whatever the opposing value of the current mode
|
|
781
|
+
* 'proxy' - Optimize for using a loopback proxy
|
|
782
|
+
*
|
|
783
|
+
* @param {'toggle'|'proxy'|Boolean} [devModeEnabled='toggle'] Optional boolean to force dev mode or specify other behaviour
|
|
784
|
+
*
|
|
785
|
+
* @returns {TeraFy} This chainable terafy instance
|
|
786
|
+
*/
|
|
787
|
+
toggleDevMode(devModeEnabled = 'toggle') {
|
|
788
|
+
if (devModeEnabled === 'toggle') {
|
|
789
|
+
this.settings.devMode = !this.settings.devMode;
|
|
790
|
+
}
|
|
791
|
+
else if (devModeEnabled === 'proxy') {
|
|
792
|
+
Object.assign(this.settings, {
|
|
793
|
+
devMode: true,
|
|
794
|
+
siteUrl: 'http://localhost:7334/embed',
|
|
795
|
+
mode: 'child',
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
this.settings.devMode = !!devModeEnabled;
|
|
800
|
+
}
|
|
801
|
+
if (this.settings.devMode)
|
|
802
|
+
this.settings.restrictOrigin = '*'; // Allow all upstream iframes
|
|
803
|
+
if (this.dom?.el) // Have we actually set up yet?
|
|
804
|
+
this.dom.el.classList.toggle('dev-mode', this.settings.devMode);
|
|
805
|
+
return this;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Fit the nested TERA server to a full-screen
|
|
809
|
+
* This is usually because the server component wants to perform some user activity like calling $prompt
|
|
810
|
+
*
|
|
811
|
+
* @param {String|Boolean} [isFocused='toggle'] Whether to fullscreen the embedded component
|
|
812
|
+
*/
|
|
813
|
+
toggleFocus(isFocused = 'toggle') {
|
|
814
|
+
this.debug('INFO', 2, 'Request focus', { isFocused });
|
|
815
|
+
globalThis.document.body.classList.toggle('tera-fy-focus', isFocused === 'toggle' ? undefined : isFocused);
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Generate random entropic character string in Base64
|
|
819
|
+
*
|
|
820
|
+
* @param {Number} [maxLength=32] Maximum lengh of the genrated string
|
|
821
|
+
* @returns {String}
|
|
822
|
+
*/
|
|
823
|
+
getEntropicString(maxLength = 32) {
|
|
824
|
+
const array = new Uint32Array(4);
|
|
825
|
+
window.crypto.getRandomValues(array);
|
|
826
|
+
return btoa(String.fromCharCode(...new Uint8Array(array.buffer)))
|
|
827
|
+
.replace(/[+/]/g, '') // Remove + and / characters
|
|
828
|
+
.slice(0, maxLength); // Trim
|
|
829
|
+
}
|
|
830
|
+
// }}}
|
|
831
|
+
// Stub documentation carried over from ./terafy.server.js {{{
|
|
832
|
+
/**
|
|
833
|
+
* Return basic server information as a form of validation
|
|
834
|
+
*
|
|
835
|
+
* @function handshake
|
|
836
|
+
* @returns {Promise<Object>} Basic promise result
|
|
837
|
+
* @property {Date} date Server date
|
|
838
|
+
*/
|
|
839
|
+
/**
|
|
840
|
+
* RPC callback to set the server verbostiy level
|
|
841
|
+
*
|
|
842
|
+
* @function setServerVerbosity
|
|
843
|
+
* @param {Number} verbosity The desired server verbosity level
|
|
844
|
+
*/
|
|
845
|
+
/**
|
|
846
|
+
* User / active session within TERA
|
|
847
|
+
*
|
|
848
|
+
* @name User
|
|
849
|
+
* @property {String} id Unique identifier of the user
|
|
850
|
+
* @property {String} email The email address of the current user
|
|
851
|
+
* @property {String} name The provided full name of the user
|
|
852
|
+
* @property {Boolean} isSubscribed Whether the active user has a TERA subscription
|
|
853
|
+
*/
|
|
854
|
+
/**
|
|
855
|
+
* Fetch the current session user
|
|
856
|
+
*
|
|
857
|
+
* @function getUser
|
|
858
|
+
* @param {Boolean} [options.forceRetry=false] Forcabily try to refresh the user state
|
|
859
|
+
* @param {Boolean} [options.waitPromises=true] Wait for $auth + $subscriptions to resolve before fetching the user (mainly internal use)
|
|
860
|
+
* @returns {Promise<User>} The current logged in user or null if none
|
|
861
|
+
*/
|
|
862
|
+
/**
|
|
863
|
+
* Provide an object of credentials for 3rd party services like Firebase/Supabase
|
|
864
|
+
*
|
|
865
|
+
* @function getCredentials
|
|
866
|
+
* @returns {Object} An object containing 3rd party service credentials
|
|
867
|
+
*/
|
|
868
|
+
/**
|
|
869
|
+
* Require a user login to TERA
|
|
870
|
+
* If there is no user OR they are not logged in a prompt is shown to go and do so
|
|
871
|
+
* This is an pre-requisite step for requireProject()
|
|
872
|
+
*
|
|
873
|
+
* @function requireUser
|
|
874
|
+
*
|
|
875
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
876
|
+
* @param {Boolean} [options.forceRetry=false] Forcabily try to refresh the user state
|
|
877
|
+
*
|
|
878
|
+
* @returns {Promise<User>} The current logged in user or null if none
|
|
879
|
+
*/
|
|
880
|
+
/**
|
|
881
|
+
* Require a user login to TERA
|
|
882
|
+
* If there is no user OR they are not logged in a prompt is shown to go and do so
|
|
883
|
+
* This is an pre-requisite step for requireProject()
|
|
884
|
+
*
|
|
885
|
+
* @returns {Promise} A promise which will resolve if the there is a user and they are logged in
|
|
886
|
+
*/
|
|
887
|
+
/**
|
|
888
|
+
* Project entry within TERA
|
|
889
|
+
*
|
|
890
|
+
* @name Project
|
|
891
|
+
* @url https://tera-tools.com/help/api/schema
|
|
892
|
+
*/
|
|
893
|
+
/**
|
|
894
|
+
* Get the currently active project, if any
|
|
895
|
+
*
|
|
896
|
+
* @function getProject
|
|
897
|
+
* @returns {Promise<Project|null>} The currently active project, if any
|
|
898
|
+
*/
|
|
899
|
+
/**
|
|
900
|
+
* Get a list of projects the current session user has access to
|
|
901
|
+
*
|
|
902
|
+
* @function getProjects
|
|
903
|
+
* @returns {Promise<Array<Project>>} Collection of projects the user has access to
|
|
904
|
+
*/
|
|
905
|
+
/**
|
|
906
|
+
* Set the currently active project within TERA
|
|
907
|
+
*
|
|
908
|
+
* @function setActiveProject
|
|
909
|
+
* @param {Object|String} project The project to set as active - either the full Project object or its ID
|
|
910
|
+
*/
|
|
911
|
+
/**
|
|
912
|
+
* Ask the user to select a project from those available - if one isn't already active
|
|
913
|
+
* Note that this function will percist in asking the uesr even if they try to cancel
|
|
914
|
+
*
|
|
915
|
+
* @function requireProject
|
|
916
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
917
|
+
* @param {Boolean} [options.autoSetActiveProject=true] After selecting a project set that project as active in TERA
|
|
918
|
+
* @param {String} [options.title="Select a project to work with"] The title of the dialog to display
|
|
919
|
+
* @param {String} [options.noSelectTitle='Select project'] Dialog title when warning the user they need to select something
|
|
920
|
+
* @param {String} [options.noSelectBody='A project needs to be selected to continue'] Dialog body when warning the user they need to select something
|
|
921
|
+
*
|
|
922
|
+
* @returns {Promise<Project>} The active project
|
|
923
|
+
*/
|
|
924
|
+
/**
|
|
925
|
+
* Prompt the user to select a project from those available
|
|
926
|
+
*
|
|
927
|
+
* @function selectProject
|
|
928
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
929
|
+
* @param {String} [options.title="Select a project to work with"] The title of the dialog to display
|
|
930
|
+
* @param {Boolean} [options.allowCancel=true] Advertise cancelling the operation, the dialog can still be cancelled by closing it
|
|
931
|
+
* @param {Boolean} [options.setActive=false] Also set the project as active when selected
|
|
932
|
+
*
|
|
933
|
+
* @returns {Promise<Project>} The active project
|
|
934
|
+
*/
|
|
935
|
+
/**
|
|
936
|
+
* Get a one-off snapshot of a namespace without mounting it
|
|
937
|
+
* This can be used for simpler apps which don't have their own reactive / observer equivelent
|
|
938
|
+
*
|
|
939
|
+
* @function getNamespace
|
|
940
|
+
* @param {String} name The alias of the namespace, this should be alphanumeric + hyphens + underscores
|
|
941
|
+
*
|
|
942
|
+
* @returns {Promise<Object>} A promise which resolves to the namespace POJO state
|
|
943
|
+
*/
|
|
944
|
+
/**
|
|
945
|
+
* Set (or merge by default) a one-off snapshot over an existing namespace
|
|
946
|
+
* This can be used for simpler apps which don't have their own reactive / observer equivelent and just want to quickly set something
|
|
947
|
+
*
|
|
948
|
+
* @function setNamespace
|
|
949
|
+
* @param {String} name The name of the namespace
|
|
950
|
+
* @param {Object} state The state to merge
|
|
951
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
952
|
+
* @param {'merge'|'set'} [options.method='merge'] How to handle the state. 'merge' (merge a partial state over the existing namespace state), 'set' (completely overwrite the existing namespace)
|
|
953
|
+
*
|
|
954
|
+
* @returns {Promise<Object>} A promise which resolves to the namespace POJO state
|
|
955
|
+
*/
|
|
956
|
+
/**
|
|
957
|
+
* Return a list of namespaces available to the current project
|
|
958
|
+
*
|
|
959
|
+
* @function listNamespaces
|
|
960
|
+
* @returns {Promise<Array<Object>>} Collection of available namespaces for the current project
|
|
961
|
+
* @property {String} name The name of the namespace
|
|
962
|
+
*/
|
|
963
|
+
/**
|
|
964
|
+
* Return the current, full snapshot state of the active project
|
|
965
|
+
*
|
|
966
|
+
* @function getProjectState
|
|
967
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
968
|
+
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
969
|
+
* @param {Array<String>} Paths to subscribe to e.g. ['/users/'],
|
|
970
|
+
*
|
|
971
|
+
* @returns {Promise<Object>} The current project state snapshot
|
|
972
|
+
*/
|
|
973
|
+
/**
|
|
974
|
+
* Set a nested value within the project state
|
|
975
|
+
* Paths can be any valid Lodash.set() value such as:
|
|
976
|
+
*
|
|
977
|
+
* - Dotted notation - e.g. `foo.bar.1.baz`
|
|
978
|
+
* - Array path segments e.g. `['foo', 'bar', 1, 'baz']`
|
|
979
|
+
*
|
|
980
|
+
* @function setProjectState
|
|
981
|
+
* @param {String|Array<String>} path The sub-path within the project state to set
|
|
982
|
+
* @param {*} value The value to set
|
|
983
|
+
*
|
|
984
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
985
|
+
* @param {Boolean} [options.save=true] Save the changes to the server immediately, disable to queue up multiple writes
|
|
986
|
+
*
|
|
987
|
+
* @returns {Promise} A promise which resolves when the operation has been dispatched to the server
|
|
988
|
+
*/
|
|
989
|
+
/**
|
|
990
|
+
* Set a nested value within the project state - just like `setProjectState()` - but only if no value for that path exists
|
|
991
|
+
*
|
|
992
|
+
* @function setProjectStateDefaults
|
|
993
|
+
* @see setProjectState()
|
|
994
|
+
* @param {String|Array<String>} path The sub-path within the project state to set
|
|
995
|
+
* @param {*} value The value to set
|
|
996
|
+
* @param {Object} [options] Additional options to mutate behaviour, see setProjectState() for the full list of supported options
|
|
997
|
+
*
|
|
998
|
+
* @returns {Promise<Boolean>} A promise which resolves to whether any changes were made - True if defaults were applied, false otherwise
|
|
999
|
+
*/
|
|
1000
|
+
/**
|
|
1001
|
+
* Force refetching the remote project state into local
|
|
1002
|
+
* This is only ever needed when saving large quantities of data that need to be immediately available
|
|
1003
|
+
*
|
|
1004
|
+
* @function setProjectStateRefresh
|
|
1005
|
+
* @returns {Promise} A promise which resolves when the operation has completed
|
|
1006
|
+
*/
|
|
1007
|
+
/**
|
|
1008
|
+
* Data structure for a file filter
|
|
1009
|
+
* @name FileFilters
|
|
1010
|
+
*
|
|
1011
|
+
* @property {Boolean} [library=false] Restrict to library files only
|
|
1012
|
+
* @property {String} [filename] CSV of @momsfriendlydevco/match expressions to filter the filename by (filenames are the basename sans extension)
|
|
1013
|
+
* @property {String} [basename] CSV of @momsfriendlydevco/match expressions to filter the basename by
|
|
1014
|
+
* @property {String} [ext] CSV of @momsfriendlydevco/match expressions to filter the file extension by
|
|
1015
|
+
*/
|
|
1016
|
+
/**
|
|
1017
|
+
* Prompt the user to select a library to operate on
|
|
1018
|
+
*
|
|
1019
|
+
* @function selectProjectFile
|
|
1020
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
1021
|
+
* @param {String} [options.title="Select a file"] The title of the dialog to display
|
|
1022
|
+
* @param {String|Array<String>} [options.hint] Hints to identify the file to select in array order of preference
|
|
1023
|
+
* @param {Boolean} [options.save=false] Set to truthy if saving a new file, UI will adjust to allowing overwrite OR new file name input
|
|
1024
|
+
* @param {FileFilters} [options.filters] Optional file filters
|
|
1025
|
+
* @param {Boolean} [options.allowUpload=true] Allow uploading new files
|
|
1026
|
+
* @param {Boolean} [options.allowRefresh=true] Allow the user to manually refresh the file list
|
|
1027
|
+
* @param {Boolean} [options.allowDownloadZip=true] Allow the user to download a Zip of all files
|
|
1028
|
+
* @param {Boolean} [options.allowCancel=true] Allow cancelling the operation. Will throw `'CANCEL'` as the promise rejection if acationed
|
|
1029
|
+
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
1030
|
+
* @param {FileFilters} [options.filter] Optional file filters
|
|
1031
|
+
*
|
|
1032
|
+
* @returns {Promise<ProjectFile>} The eventually selected file, if in save mode new files are created as stubs
|
|
1033
|
+
*/
|
|
1034
|
+
selectProjectFile(options) {
|
|
1035
|
+
return this.rpc('selectProjectFile', options)
|
|
1036
|
+
.then((file) => file
|
|
1037
|
+
? new ProjectFile({
|
|
1038
|
+
tera: this,
|
|
1039
|
+
...file,
|
|
1040
|
+
})
|
|
1041
|
+
: file);
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Fetch the files associated with a given project
|
|
1045
|
+
*
|
|
1046
|
+
* @function getProjectFiles
|
|
1047
|
+
* @param {Object} options Options which mutate behaviour
|
|
1048
|
+
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
1049
|
+
* @param {Boolean} [options.lazy=true] If true, use the fastest method to retrieve the file list such as the cache. If false, force a refresh each time
|
|
1050
|
+
* @param {Boolean} [options.meta=true] Pull meta information for each file entity
|
|
1051
|
+
*
|
|
1052
|
+
* @returns {Promise<Array<ProjectFile>>} A collection of project files for the given project
|
|
1053
|
+
*/
|
|
1054
|
+
getProjectFiles(options) {
|
|
1055
|
+
return this.rpc('getProjectFiles', options)
|
|
1056
|
+
.then((files) => files.map((file) => new ProjectFile({
|
|
1057
|
+
tera: this,
|
|
1058
|
+
...file,
|
|
1059
|
+
})));
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Fetch the raw contents of a file by its ID
|
|
1063
|
+
*
|
|
1064
|
+
* @function getProjectFileContents
|
|
1065
|
+
* @param {String} [id] File ID to retrieve the contents of
|
|
1066
|
+
* @param {Object} [options] Additioanl options to mutate behaviour
|
|
1067
|
+
* @param {'blob'|'json'} [options.format='blob'] The format to retrieve the file in
|
|
1068
|
+
*
|
|
1069
|
+
* @returns {*} The file contents in the requested format
|
|
1070
|
+
*/
|
|
1071
|
+
/**
|
|
1072
|
+
* Fetch a project file by its name
|
|
1073
|
+
*
|
|
1074
|
+
* @function getProjectFile
|
|
1075
|
+
* @param {String} id The name + relative directory path component
|
|
1076
|
+
*
|
|
1077
|
+
* @param {Object|String} [options] Additional options to mutate behaviour, if a string is given `options.subkey` is assumed
|
|
1078
|
+
* @param {String} [options.subkey] If specified only the extracted subkey is returned rather than the full object
|
|
1079
|
+
* @param {Boolean} [options.cache=true] Use the existing file cache if possible, set to false to force a refresh of files from the server first
|
|
1080
|
+
*
|
|
1081
|
+
* @returns {Promise<ProjectFile>} The eventual fetched ProjectFile (or requested subkey)
|
|
1082
|
+
*/
|
|
1083
|
+
getProjectFile(id, options) {
|
|
1084
|
+
return this.rpc('getProjectFile', id, options)
|
|
1085
|
+
.then((file) => file
|
|
1086
|
+
? new ProjectFile({
|
|
1087
|
+
tera: this,
|
|
1088
|
+
...file,
|
|
1089
|
+
})
|
|
1090
|
+
: file);
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Create a new file
|
|
1094
|
+
* This creates an empty file which can then be written to
|
|
1095
|
+
*
|
|
1096
|
+
* @function createProjectFile
|
|
1097
|
+
* @param {String} name The name + relative directory path component
|
|
1098
|
+
* @returns {Promise<ProjectFile>} The eventual ProjectFile created
|
|
1099
|
+
*/
|
|
1100
|
+
createProjectFile(name) {
|
|
1101
|
+
return this.rpc('createProjectFile', name)
|
|
1102
|
+
.then((file) => file
|
|
1103
|
+
? new ProjectFile({
|
|
1104
|
+
tera: this,
|
|
1105
|
+
...file,
|
|
1106
|
+
})
|
|
1107
|
+
: file);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
//# sourceMappingURL=terafy.client.js.map
|