@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/api.md +68 -66
  3. package/dist/lib/projectFile.d.ts +182 -0
  4. package/dist/lib/projectFile.js +157 -0
  5. package/dist/lib/projectFile.js.map +1 -0
  6. package/dist/lib/syncro/entities.d.ts +28 -0
  7. package/dist/lib/syncro/entities.js +203 -0
  8. package/dist/lib/syncro/entities.js.map +1 -0
  9. package/dist/lib/syncro/keyed.d.ts +95 -0
  10. package/dist/lib/syncro/keyed.js +286 -0
  11. package/dist/lib/syncro/keyed.js.map +1 -0
  12. package/dist/lib/syncro/syncro.d.ts +328 -0
  13. package/dist/lib/syncro/syncro.js +633 -0
  14. package/dist/lib/syncro/syncro.js.map +1 -0
  15. package/dist/lib/terafy.bootstrapper.d.ts +42 -0
  16. package/dist/lib/terafy.bootstrapper.js +130 -0
  17. package/dist/lib/terafy.bootstrapper.js.map +1 -0
  18. package/dist/lib/terafy.client.d.ts +532 -0
  19. package/dist/lib/terafy.client.js +1110 -0
  20. package/dist/lib/terafy.client.js.map +1 -0
  21. package/dist/lib/terafy.proxy.d.ts +66 -0
  22. package/dist/lib/terafy.proxy.js +123 -0
  23. package/dist/lib/terafy.proxy.js.map +1 -0
  24. package/dist/lib/terafy.server.d.ts +607 -0
  25. package/dist/lib/terafy.server.js +1774 -0
  26. package/dist/lib/terafy.server.js.map +1 -0
  27. package/dist/plugin.vue2.es2019.js +30 -13
  28. package/dist/plugins/base.d.ts +20 -0
  29. package/dist/plugins/base.js +21 -0
  30. package/dist/plugins/base.js.map +1 -0
  31. package/dist/plugins/firebase.d.ts +62 -0
  32. package/dist/plugins/firebase.js +111 -0
  33. package/dist/plugins/firebase.js.map +1 -0
  34. package/dist/plugins/vite.d.ts +12 -0
  35. package/dist/plugins/vite.js +22 -0
  36. package/dist/plugins/vite.js.map +1 -0
  37. package/dist/plugins/vue2.d.ts +68 -0
  38. package/dist/plugins/vue2.js +96 -0
  39. package/dist/plugins/vue2.js.map +1 -0
  40. package/dist/plugins/vue3.d.ts +64 -0
  41. package/dist/plugins/vue3.js +96 -0
  42. package/dist/plugins/vue3.js.map +1 -0
  43. package/dist/terafy.bootstrapper.es2019.js +2 -2
  44. package/dist/terafy.bootstrapper.js +2 -2
  45. package/dist/terafy.es2019.js +2 -2
  46. package/dist/terafy.js +1 -1
  47. package/dist/utils/mixin.d.ts +11 -0
  48. package/dist/utils/mixin.js +15 -0
  49. package/dist/utils/mixin.js.map +1 -0
  50. package/dist/utils/pDefer.d.ts +12 -0
  51. package/dist/utils/pDefer.js +14 -0
  52. package/dist/utils/pDefer.js.map +1 -0
  53. package/dist/utils/pathTools.d.ts +70 -0
  54. package/dist/utils/pathTools.js +120 -0
  55. package/dist/utils/pathTools.js.map +1 -0
  56. package/eslint.config.js +44 -8
  57. package/lib/{projectFile.js → projectFile.ts} +83 -40
  58. package/lib/syncro/entities.ts +288 -0
  59. package/lib/syncro/{keyed.js → keyed.ts} +114 -57
  60. package/lib/syncro/{syncro.js → syncro.ts} +204 -169
  61. package/lib/{terafy.bootstrapper.js → terafy.bootstrapper.ts} +49 -31
  62. package/lib/{terafy.client.js → terafy.client.ts} +94 -86
  63. package/lib/{terafy.proxy.js → terafy.proxy.ts} +43 -16
  64. package/lib/{terafy.server.js → terafy.server.ts} +364 -223
  65. package/package.json +65 -26
  66. package/plugins/{base.js → base.ts} +3 -1
  67. package/plugins/{firebase.js → firebase.ts} +34 -16
  68. package/plugins/{vite.js → vite.ts} +3 -3
  69. package/plugins/{vue2.js → vue2.ts} +17 -10
  70. package/plugins/{vue3.js → vue3.ts} +11 -9
  71. package/tsconfig.json +30 -0
  72. package/utils/{mixin.js → mixin.ts} +1 -1
  73. package/utils/{pDefer.js → pDefer.ts} +10 -3
  74. package/utils/{pathTools.js → pathTools.ts} +11 -9
  75. 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