@iebh/tera-fy 2.3.11 → 2.3.12

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