@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
@@ -1,8 +1,19 @@
1
+ // Global 'app' declaration
2
+ declare var app: any;
3
+
4
+ // Global window augmentation
5
+ declare global {
6
+ interface Window {
7
+ panic(text: any): void;
8
+ }
9
+ }
10
+
1
11
  import {cloneDeep} from 'lodash-es';
2
12
  import mixin from '#utils/mixin';
3
13
  import {nanoid} from 'nanoid';
4
14
  import pathTools from '#utils/pathTools';
5
15
  import promiseDefer from '#utils/pDefer';
16
+ // @ts-ignore
6
17
  import Reflib from '@iebh/reflib';
7
18
  import {reactive} from 'vue';
8
19
 
@@ -29,7 +40,7 @@ export default class TeraFyServer {
29
40
  * @property {String} sitePathLogin Either an absolute URL or the relative path (taken from `siteUrl`) when trying to log in the user
30
41
  * @property {Boolean} embedWorkaround Try to use `getUserViaEmbedWorkaround()` to force a login via popup if the user is running in local mode (see function docs for more details). This is toggled to false after the first run
31
42
  */
32
- settings = {
43
+ settings: any = {
33
44
  devMode: false,
34
45
  verbosity: 9,
35
46
  restrictOrigin: '*',
@@ -57,20 +68,21 @@ export default class TeraFyServer {
57
68
  *
58
69
  * @returns {Object} A context, which is this instance extended with additional properties
59
70
  */
60
- createContext(e) {
71
+ createContext(e: MessageEvent): any {
61
72
  // Construct wrapper for sendRaw for this client
62
73
  return mixin(this, {
63
74
  messageEvent: e,
64
- sendRaw(message) {
75
+ sendRaw(message: any) {
65
76
  let payload;
66
77
  try {
67
78
  payload = {
68
79
  TERA: 1,
69
80
  ...cloneDeep(message), // Need to clone to resolve promise nasties
70
81
  };
71
- e.source.postMessage(payload, this.settings.restrictOrigin);
72
- } catch (err) {
73
- this.debug('ERROR', 1, 'Attempted to dispatch payload server(via reply)->client', {payload, err});
82
+ // Use type assertion assuming e.source is a WindowProxy or similar
83
+ (e.source as WindowProxy).postMessage(payload, this.settings.restrictOrigin);
84
+ } catch (err: any) { // Changed variable name e -> err
85
+ this.debug('ERROR', 1, 'Attempted to dispatch payload server(via reply)->client', {payload, e: err});
74
86
  throw err;
75
87
  }
76
88
  },
@@ -84,14 +96,14 @@ export default class TeraFyServer {
84
96
  *
85
97
  * @returns {Object} A context, which is this instance extended with additional properties
86
98
  */
87
- getClientContext() {
99
+ getClientContext(): any {
88
100
  switch (this.settings.serverMode) {
89
101
  case TeraFyServer.SERVERMODE_NONE:
90
102
  throw new Error('Client has not yet initiated communication');
91
103
  case TeraFyServer.SERVERMODE_EMBEDDED:
92
104
  // Server is inside an iFrame so we need to send messages to the window parent
93
105
  return mixin(this, {
94
- sendRaw(message) {
106
+ sendRaw(message: any) {
95
107
  let payload;
96
108
  try {
97
109
  payload = {
@@ -99,7 +111,7 @@ export default class TeraFyServer {
99
111
  ...cloneDeep(message), // Need to clone to resolve promise nasties
100
112
  };
101
113
  window.parent.postMessage(payload, this.settings.restrictOrigin);
102
- } catch (e) {
114
+ } catch (e: any) {
103
115
  this.debug('ERROR', 1, 'Attempted to dispatch payload server(iframe)->cient(top level window)', {payload, e});
104
116
  throw e;
105
117
  }
@@ -108,28 +120,29 @@ export default class TeraFyServer {
108
120
  case TeraFyServer.SERVERMODE_TERA:
109
121
  case TeraFyServer.SERVERMODE_FRAME: {
110
122
  // Server is the top-level window so we need to send messages to an embedded iFrame
111
- let iFrame = document.querySelector('iframe#external');
123
+ let iFrame = document.querySelector('iframe#external') as HTMLIFrameElement | null;
112
124
  if (!iFrame) {
113
125
  this.debug('INFO', 2, 'Cannot locate TERA-FY top-level->iFrame#external - maybe there is none');
114
126
  return mixin(this, {
115
- sendRaw(message) {
127
+ sendRaw(message: any) {
116
128
  this.debug('INFO', 2, 'Sending broadcast to zero listening clients', {message});
117
129
  },
118
130
  });
119
131
  }
120
132
 
121
133
  return mixin(this, {
122
- sendRaw(message) {
134
+ sendRaw(message: any) {
123
135
  let payload;
124
136
  try {
125
137
  payload = {
126
138
  TERA: 1,
127
139
  ...cloneDeep(message), // Need to clone to resolve promise nasties
128
140
  };
129
- iFrame.contentWindow.postMessage(payload, this.settings.restrictOrigin);
130
- } catch (err) {
131
- this.debug('ERROR', 1, 'Attempted to dispatch payload server(top level window)->cient(iframe)', {payload, err});
132
- throw err;
141
+ // Check if contentWindow exists before posting
142
+ iFrame.contentWindow?.postMessage(payload, this.settings.restrictOrigin);
143
+ } catch (e: any) {
144
+ this.debug('ERROR', 1, 'Attempted to dispatch payload server(top level window)->cient(iframe)', {payload, e});
145
+ throw e;
133
146
  }
134
147
  },
135
148
  });
@@ -147,7 +160,7 @@ export default class TeraFyServer {
147
160
  *
148
161
  * @type {MessageEvent}
149
162
  */
150
- messageEvent = null;
163
+ messageEvent: MessageEvent | null = null;
151
164
 
152
165
 
153
166
  /**
@@ -159,10 +172,12 @@ export default class TeraFyServer {
159
172
  *
160
173
  * @returns {Promise<*>} The resolved output of the server function
161
174
  */
162
- senderRpc(method, ...args) {
175
+ senderRpc(method: string, ...args: any[]): Promise<any> {
163
176
  if (!this.messageEvent) throw new Error('senderRpc() can only be used if given a context from `createContext()`');
164
177
 
165
- return this.send({
178
+ // Create a context specific to this event to use its sendRaw
179
+ const context = this.createContext(this.messageEvent);
180
+ return context.send({ // Use the context's send method if available, otherwise fallback? Assuming send is on the base class.
166
181
  action: 'rpc',
167
182
  method,
168
183
  args,
@@ -178,7 +193,7 @@ export default class TeraFyServer {
178
193
  * @returns {Promise<Object>} Basic promise result
179
194
  * @property {Date} date Server date
180
195
  */
181
- handshake() {
196
+ handshake(): Promise<any> {
182
197
  return Promise.resolve({
183
198
  date: new Date(),
184
199
  });
@@ -187,11 +202,13 @@ export default class TeraFyServer {
187
202
 
188
203
  /**
189
204
  * Send a message + wait for a response object
205
+ * This method should likely be part of the context returned by createContext
206
+ * Assuming it's intended to work on the base class referencing a stored messageEvent
190
207
  *
191
208
  * @param {Object} message Message object to send
192
209
  * @returns {Promise<*>} A promise which resolves when the operation has completed with the remote reply
193
210
  */
194
- send(message) {
211
+ send(message: any): Promise<any> {
195
212
  if (!this.messageEvent?.source) throw new Error('send() requires a messageEvent with a source');
196
213
 
197
214
  let id = nanoid();
@@ -201,7 +218,6 @@ export default class TeraFyServer {
201
218
  Object.assign(this.acceptPostboxes[id], {
202
219
  resolve, reject,
203
220
  });
204
-
205
221
  // Use sendRaw with the specific source from the stored messageEvent
206
222
  this.sendRaw({
207
223
  id,
@@ -220,7 +236,7 @@ export default class TeraFyServer {
220
236
  * @param {Object} message Message object to send
221
237
  * @param {Window} sendVia Window context to dispatch the message via if its not the same as the regular window
222
238
  */
223
- sendRaw(message, sendVia) {
239
+ sendRaw(message: any, sendVia?: any): void {
224
240
  let payload;
225
241
  try {
226
242
  payload = {
@@ -228,11 +244,14 @@ export default class TeraFyServer {
228
244
  ...cloneDeep(message), // Need to clone to resolve promise nasties
229
245
  };
230
246
  this.debug('INFO', 3, 'Dispatch response', message, '<=>', payload);
231
-
232
- let target = sendVia || globalThis.parent;
233
- if (!target) this.debug('WARN', 1, 'Cannot sendRaw, no target window (sendVia or parent) found.');
234
- target.postMessage(payload, this.settings.restrictOrigin);
235
- } catch (e) {
247
+ // Default to parent if sendVia is not provided, but check if it exists
248
+ const target = sendVia || (typeof globalThis !== 'undefined' ? globalThis.parent : undefined);
249
+ if (target) {
250
+ target.postMessage(payload, this.settings.restrictOrigin);
251
+ } else {
252
+ this.debug('WARN', 1, 'Cannot sendRaw, no target window (sendVia or parent) found.');
253
+ }
254
+ } catch (e: any) {
236
255
  this.debug('ERROR', 2, 'Attempted to dispatch response server->client', payload);
237
256
  this.debug('ERROR', 2, 'Message compose server->client:', e);
238
257
  }
@@ -244,7 +263,7 @@ export default class TeraFyServer {
244
263
  *
245
264
  * @param {String} mode The server mode to set to
246
265
  */
247
- setServerMode(mode) {
266
+ setServerMode(mode: string): void {
248
267
  switch (mode) {
249
268
  case 'embedded':
250
269
  this.settings.serverMode = TeraFyServer.SERVERMODE_EMBEDDED;
@@ -266,37 +285,55 @@ export default class TeraFyServer {
266
285
  *
267
286
  * @param {MessageEvent} rawMessage Raw message event to process
268
287
  */
269
- acceptMessage(rawMessage) {
270
- if (rawMessage.origin == window.location.origin) return; // Message came from us
288
+ acceptMessage(rawMessage: MessageEvent): void {
289
+ // Ignore messages from the same origin (potential loops)
290
+ if (typeof window !== 'undefined' && rawMessage.origin === window.location.origin) return;
271
291
 
272
292
  let message = rawMessage.data;
273
- if (!message.TERA) return; // Ignore non-TERA signed messages
293
+ // Ensure message is an object and has TERA property
294
+ if (typeof message !== 'object' || message === null || !message.TERA) return;
274
295
  this.debug('INFO', 3, 'Recieved message', message);
275
296
 
276
297
  Promise.resolve()
277
298
  .then(()=> {
278
- if (message?.action == 'response' && this.acceptPostboxes[message.id]) { // Postbox waiting for reply
299
+ if (message?.action == 'response' && message.id && this.acceptPostboxes[message.id]) { // Postbox waiting for reply
279
300
  if (message.isError === true) {
280
301
  this.acceptPostboxes[message.id].reject(message.response);
281
302
  } else {
282
303
  this.acceptPostboxes[message.id].resolve(message.response);
283
304
  }
284
305
  delete this.acceptPostboxes[message.id]; // Clean up postbox
285
- } else if (message.action == 'rpc') { // Relay RPC calls
286
- if (!this[message.method]) throw new Error(`Unknown RPC method "${message.method}"`);
287
- return this[message.method].apply(this.createContext(rawMessage), message.args);
306
+ } else if (message.action == 'rpc' && typeof message.method === 'string') { // Relay RPC calls
307
+ const method = message.method as string;
308
+ // Use type assertion for dynamic method call
309
+ if (typeof (this as any)[method] === 'function') {
310
+ // Create context for this specific message event
311
+ const context = this.createContext(rawMessage);
312
+ // Store the event temporarily for potential use in send() called by the RPC method
313
+ context.messageEvent = rawMessage;
314
+ return (this as any)[method].apply(context, message.args || []);
315
+ } else {
316
+ throw new Error(`Unknown RPC method "${method}"`);
317
+ }
288
318
  } else {
289
319
  this.debug('ERROR', 2, 'Unexpected incoming TERA-FY SERVER message', {message});
290
- throw new Error('Unknown message format');
320
+ // Don't throw, just ignore unknown formats silently? Or throw?
321
+ // throw new Error('Unknown message format');
322
+ }
323
+ })
324
+ .then(response => {
325
+ // Only send response if it was an RPC call that returned something
326
+ if (message.action === 'rpc' && rawMessage.source) {
327
+ this.sendRaw({
328
+ id: message.id,
329
+ action: 'response',
330
+ response,
331
+ }, rawMessage.source);
291
332
  }
292
333
  })
293
- .then(response => this.sendRaw({
294
- id: message.id,
295
- action: 'response',
296
- response,
297
- }, rawMessage.source))
298
334
  .catch(e => {
299
335
  console.warn(`TERA-FY server threw on RPC:${message.method}:`, e);
336
+ // Send error response back if possible
300
337
  if (message.action === 'rpc' && message.id && rawMessage.source) {
301
338
  this.sendRaw({
302
339
  id: message.id,
@@ -314,7 +351,7 @@ export default class TeraFyServer {
314
351
  /**
315
352
  * Listening postboxes, these correspond to outgoing message IDs that expect a response
316
353
  */
317
- acceptPostboxes = {};
354
+ acceptPostboxes: Record<string, any> = {};
318
355
 
319
356
 
320
357
  /**
@@ -326,7 +363,7 @@ export default class TeraFyServer {
326
363
  *
327
364
  * @returns {Promise<*>} A promise which resolves with the resulting inner callback payload
328
365
  */
329
- requestFocus(cb) {
366
+ requestFocus(cb: () => Promise<any>): Promise<any> {
330
367
  // Ensure messageEvent is set before calling senderRpc
331
368
  if (!this.messageEvent && this.settings.serverMode != TeraFyServer.SERVERMODE_TERA) {
332
369
  console.warn("requestFocus called without a messageEvent context. Cannot toggle focus.");
@@ -335,9 +372,11 @@ export default class TeraFyServer {
335
372
  }
336
373
 
337
374
  return Promise.resolve()
338
- .then(()=> this.settings.serverMode != TeraFyServer.SERVERMODE_TERA && this.senderRpc('toggleFocus', true))
375
+ // Only toggle focus if not in TERA mode and messageEvent is available
376
+ .then(()=> this.settings.serverMode != TeraFyServer.SERVERMODE_TERA && this.messageEvent && this.senderRpc('toggleFocus', true))
339
377
  .then(()=> cb.call(this))
340
- .finally(()=> this.settings.serverMode != TeraFyServer.SERVERMODE_TERA && this.senderRpc('toggleFocus', false))
378
+ // Only toggle focus back if not in TERA mode and messageEvent is available
379
+ .finally(()=> this.settings.serverMode != TeraFyServer.SERVERMODE_TERA && this.messageEvent && this.senderRpc('toggleFocus', false))
341
380
  }
342
381
 
343
382
 
@@ -349,8 +388,10 @@ export default class TeraFyServer {
349
388
  * @param {...*} [args] Optional event payload to send
350
389
  * @returns {Promise} A promise which resolves when the transmission has completed
351
390
  */
352
- emitClients(event, ...args) {
353
- return this.getClientContext().sendRaw({
391
+ emitClients(event: string, ...args: any[]): Promise<void> {
392
+ // Use getClientContext to get the appropriate sendRaw method
393
+ const context = this.getClientContext();
394
+ return context.sendRaw({
354
395
  action: 'event',
355
396
  id: nanoid(),
356
397
  event,
@@ -364,7 +405,7 @@ export default class TeraFyServer {
364
405
  *
365
406
  * @param {Number} verbosity The desired server verbosity level
366
407
  */
367
- setServerVerbosity(verbosity) {
408
+ setServerVerbosity(verbosity: number): void {
368
409
  this.settings.verbosity = +verbosity;
369
410
  this.debug('INFO', 1, 'Server verbosity set to', this.settings.verbosity);
370
411
  }
@@ -390,7 +431,7 @@ export default class TeraFyServer {
390
431
  *
391
432
  * @returns {Promise<User>} The current logged in user or null if none
392
433
  */
393
- getUser(options) {
434
+ getUser(options?: any): Promise<any | null> {
394
435
  let settings = {
395
436
  forceRetry: false,
396
437
  waitPromises: true,
@@ -422,7 +463,7 @@ export default class TeraFyServer {
422
463
  }
423
464
  : null
424
465
  )
425
- .catch(e => {
466
+ .catch((e: any) => {
426
467
  console.warn('getUser() catch', e);
427
468
  return null; // Return null on error
428
469
  })
@@ -436,8 +477,8 @@ export default class TeraFyServer {
436
477
  *
437
478
  * @returns {Promise<User>} A promise which will resolve if the there is a user and they are logged in
438
479
  */
439
- requireUser() {
440
- let user; // Last getUser() response
480
+ requireUser(): Promise<any> {
481
+ let user: any; // Last getUser() response
441
482
  return Promise.resolve() // NOTE: This promise is upside down, it only continues down the chain if the user is NOT valid, otherwise it throws to exit
442
483
  .then(()=> this.getUser())
443
484
  .then(res => user = res)
@@ -495,7 +536,7 @@ export default class TeraFyServer {
495
536
  *
496
537
  * @returns {Object} An object containing 3rd party service credentials
497
538
  */
498
- getCredentials() {
539
+ getCredentials(): any {
499
540
  return app.service('$auth').credentials;
500
541
  }
501
542
 
@@ -516,10 +557,10 @@ export default class TeraFyServer {
516
557
  *
517
558
  * @returns {Promise} A promise which resolves when the operation has completed
518
559
  */
519
- async getUserViaEmbedWorkaround() {
560
+ async getUserViaEmbedWorkaround(): Promise<void> {
520
561
  this.debug('INFO', 4, 'Attempting to use getUserViaEmbedWorkaround()');
521
562
 
522
- let lsState = window.localStorage.getItem('tera.embedUser');
563
+ let lsState: any = window.localStorage.getItem('tera.embedUser');
523
564
  if (lsState) {
524
565
  this.debug('INFO', 4, 'Using localStorage state');
525
566
  try {
@@ -539,8 +580,8 @@ export default class TeraFyServer {
539
580
  // Force refresh projects against the new user
540
581
  await app.service('$projects').refresh();
541
582
  return;
542
- } catch (err) {
543
- throw new Error(`Failed to decode local dev state - ${err.toString()}`);
583
+ } catch (e: any) {
584
+ throw new Error(`Failed to decode local dev state - ${e.toString()}`);
544
585
  }
545
586
  }
546
587
 
@@ -551,7 +592,7 @@ export default class TeraFyServer {
551
592
  + '<div class="mt-2"><a class="btn btn-light">Open Popup...</a></div>';
552
593
 
553
594
  // Attach click listner to internal button to re-popup the auth window (in case popups are blocked)
554
- focusContent.querySelector('a.btn').addEventListener('click', ()=>
595
+ focusContent.querySelector('a.btn')?.addEventListener('click', ()=>
555
596
  this.uiWindow(new URL(this.settings.sitePathLogin, this.settings.siteUrl).toString())
556
597
  );
557
598
 
@@ -559,7 +600,7 @@ export default class TeraFyServer {
559
600
  let waitOnWindowAuth = promiseDefer();
560
601
 
561
602
  // Create a listener for the message from the downstream window to resolve the promise
562
- let listenMessages = ({data}) => {
603
+ let listenMessages = ({data}: {data: any}) => {
563
604
  this.debug('INFO', 3, 'Recieved message from popup window', {data});
564
605
  if (data.TERA && data.action == 'popupUserState' && data.user) { // Signal sent from landing page - we're logged in, yey!
565
606
  let $auth = app.service('$auth');
@@ -585,7 +626,7 @@ export default class TeraFyServer {
585
626
  window.addEventListener('message', listenMessages);
586
627
 
587
628
  // Go fullscreen, try to open the auth window + prompt the user to retry (if popups are blocked) and wait for resolution
588
- await this.requestFocus(()=> {
629
+ await this.requestFocus(async ()=> {
589
630
  // Try opening the popup automatically - this will likely fail if the user has popup blocking enabled
590
631
  this.uiWindow(new URL(this.settings.sitePathLogin, this.settings.siteUrl).toString());
591
632
 
@@ -626,7 +667,7 @@ export default class TeraFyServer {
626
667
  *
627
668
  * @returns {Promise<Project|null>} The currently active project, if any
628
669
  */
629
- getProject() {
670
+ getProject(): Promise<any | null> {
630
671
  let $projects = app.service('$projects');
631
672
 
632
673
  return $projects.promise()
@@ -647,11 +688,11 @@ export default class TeraFyServer {
647
688
  *
648
689
  * @returns {Promise<Array<Project>>} Collection of projects the user has access to
649
690
  */
650
- getProjects() {
691
+ getProjects(): Promise<any[]> {
651
692
  let $projects = app.service('$projects');
652
693
 
653
694
  return $projects.promise()
654
- .then(()=> $projects.list.map(project => ({
695
+ .then(()=> $projects.list.map((project: any) => ({
655
696
  id: project.id,
656
697
  name: project.name,
657
698
  created: project.created,
@@ -666,7 +707,7 @@ export default class TeraFyServer {
666
707
  * @param {Object|String} project The project to set as active - either the full Project object or its ID
667
708
  * @returns {Promise} A promise which resolves when the operation has completed
668
709
  */
669
- setActiveProject(project) {
710
+ setActiveProject(project: any): Promise<void> {
670
711
  return app.service('$projects').setActive(project);
671
712
  }
672
713
 
@@ -684,7 +725,7 @@ export default class TeraFyServer {
684
725
  *
685
726
  * @returns {Promise<Project>} The active project
686
727
  */
687
- requireProject(options) {
728
+ requireProject(options?: any): Promise<any> {
688
729
  let settings = {
689
730
  autoRequireUser: true,
690
731
  autoSetActiveProject: true,
@@ -701,13 +742,13 @@ export default class TeraFyServer {
701
742
  if (active) return active; // Use active project
702
743
 
703
744
  return new Promise((resolve, reject) => {
704
- let askProject = ()=> Promise.resolve()
745
+ let askProject = (): Promise<any> => Promise.resolve()
705
746
  .then(()=> this.selectProject({
706
747
  allowCancel: false,
707
748
  }))
708
749
  .then(project => resolve(project))
709
750
  .catch(e => {
710
- if (e.toLowerCase() == 'cancel') {
751
+ if (e == 'cancel' || e === 'CANCEL') { // Handle string 'cancel' or rejected 'CANCEL'
711
752
  return this.requestFocus(()=>
712
753
  app.service('$prompt').dialog({
713
754
  title: settings.noSelectTitle,
@@ -723,7 +764,7 @@ export default class TeraFyServer {
723
764
  })
724
765
  askProject(); // Kick off intial project loop
725
766
  })
726
- .then(async (project) => {
767
+ .then(async (project: any) => {
727
768
  if (settings.autoSetActiveProject) await this.setActiveProject(project);
728
769
  return project;
729
770
  })
@@ -741,7 +782,7 @@ export default class TeraFyServer {
741
782
  *
742
783
  * @returns {Promise<Project>} The active project
743
784
  */
744
- selectProject(options) {
785
+ selectProject(options?: any): Promise<any> {
745
786
  let settings = {
746
787
  title: 'Select a project to work with',
747
788
  allowCancel: true,
@@ -754,10 +795,10 @@ export default class TeraFyServer {
754
795
  app.service('$prompt').dialog({
755
796
  title: settings.title,
756
797
  component: 'projectsSelect',
757
- buttons: settings.allowCancel ? ['cancel'] : false,
798
+ buttons: settings.allowCancel ? ['cancel'] : [],
758
799
  })
759
800
  ))
760
- .then(project => settings.setActive
801
+ .then((project: any) => settings.setActive
761
802
  ? this.setActiveProject(project)
762
803
  .then(()=> project)
763
804
  : project
@@ -776,7 +817,7 @@ export default class TeraFyServer {
776
817
  *
777
818
  * @returns {Promise<Object>} A promise which resolves to the namespace POJO state
778
819
  */
779
- getNamespace(name) {
820
+ getNamespace(name: string): Promise<any> {
780
821
  if (!/^[\w-]+$/.test(name)) throw new Error('Namespaces must be alphanumeric + hyphens + underscores');
781
822
 
782
823
  return app.service('$sync').getSnapshot(`project_namespaces::${app.service('$projects').active.id}::${name}`);
@@ -794,12 +835,12 @@ export default class TeraFyServer {
794
835
  *
795
836
  * @returns {Promise<Object>} A promise which resolves to the namespace POJO state
796
837
  */
797
- setNamespace(name, state, options) {
838
+ setNamespace(name: string, state: any, options?: any): Promise<any> {
798
839
  if (!/^[\w-]+$/.test(name)) throw new Error('Namespaces must be alphanumeric + hyphens + underscores');
799
840
  if (typeof state != 'object') throw new Error('State must be an object');
800
841
 
801
- return app.service('$sync').setSnapshot(`project_namespaces::${app.service('$projects').active.id}}::${name}`, state, {
802
- method: options.method ?? 'merge',
842
+ return app.service('$sync').setSnapshot(`project_namespaces::${app.service('$projects').active.id}::${name}`, state, {
843
+ method: options?.method ?? 'merge',
803
844
  });
804
845
  }
805
846
 
@@ -810,7 +851,7 @@ export default class TeraFyServer {
810
851
  * @returns {Promise<Array<Object>>} Collection of available namespaces for the current project
811
852
  * @property {String} name The name of the namespace
812
853
  */
813
- listNamespaces() {
854
+ listNamespaces(): Promise<any[]> {
814
855
  return app.service('$projects').listNamespaces();
815
856
  }
816
857
  // }}}
@@ -826,7 +867,7 @@ export default class TeraFyServer {
826
867
  *
827
868
  * @returns {Promise<Object>} The current project state snapshot
828
869
  */
829
- getProjectState(options) {
870
+ getProjectState(options?: any): Promise<any> {
830
871
  let settings = {
831
872
  autoRequire: true,
832
873
  paths: null,
@@ -859,7 +900,7 @@ export default class TeraFyServer {
859
900
  *
860
901
  * @returns {Promise<*>} A promise which resolves to `value` when the operation has been dispatched to the server and saved
861
902
  */
862
- setProjectState(path, value, options) {
903
+ setProjectState(path: string | string[], value: any, options?: any): Promise<any> {
863
904
  let settings = {
864
905
  strategy: 'set',
865
906
  ...options,
@@ -899,32 +940,33 @@ export default class TeraFyServer {
899
940
  *
900
941
  * @returns {Promise<*>} A promise which resolves to the eventual input value after defaults have been applied
901
942
  */
902
- setProjectStateDefaults(path, value, options) {
903
- let settings = {
904
- ...options,
905
- };
943
+ setProjectStateDefaults(path: string | string[] | any, value?: any, options?: any): Promise<any> {
944
+ let settings = { ...options }; // Initialize settings from the third argument if present
906
945
  if (!app.service('$projects').active) throw new Error('No active project');
907
946
 
908
947
  let target = app.service('$projects').active;
948
+ let actualValue: any;
909
949
 
910
950
  if (typeof path == 'string' || Array.isArray(path)) { // Called as (path, value, options?) Set sub-object
951
+ actualValue = value;
911
952
  return this.setProjectState(
912
953
  path,
913
- value,
954
+ actualValue,
914
955
  {
915
956
  strategy: 'defaults',
916
- ...settings,
957
+ ...settings, // Pass options from the third argument
917
958
  },
918
959
  )
919
960
  .then(()=> pathTools.get(target, path));
920
- } else { // Called as (value) - Populate entire project layout
921
- pathTools.defaults(target, path);
961
+ } else { // Called as (value, options?) - Populate entire project layout
962
+ actualValue = path; // The first argument is the value
963
+ settings = { ...value }; // The second argument holds the options
964
+ pathTools.defaults(target, actualValue);
922
965
  this.debug('INFO', 1, 'setProjectStateDefaults', {
923
- defaults: path,
966
+ defaults: actualValue,
924
967
  newState: cloneDeep(target),
925
968
  });
926
-
927
- return Promise.resolve(value);
969
+ return Promise.resolve(target); // Resolve with the modified target state
928
970
  }
929
971
  }
930
972
 
@@ -934,7 +976,7 @@ export default class TeraFyServer {
934
976
  *
935
977
  * @returns {Promise} A promise which resolves when the operation has completed
936
978
  */
937
- setProjectStateRefresh() {
979
+ setProjectStateRefresh(): Promise<null> {
938
980
  this.debug('INFO', 1, 'Force project state refresh!');
939
981
  if (!app.service('$projects').active) throw new Error('No active project');
940
982
  return app.service('$projects').active.$read({force: true})
@@ -992,7 +1034,7 @@ export default class TeraFyServer {
992
1034
  *
993
1035
  * @returns {Promise<ProjectFile>} The eventually selected file, if in save mode new files are created as stubs
994
1036
  */
995
- selectProjectFile(options) {
1037
+ selectProjectFile(options?: any): Promise<any> {
996
1038
  let settings = {
997
1039
  title: 'Select a file',
998
1040
  hint: null,
@@ -1025,15 +1067,15 @@ export default class TeraFyServer {
1025
1067
  filters: settings.filters,
1026
1068
  },
1027
1069
  componentEvents: {
1028
- fileSave(file) {
1070
+ fileSave(file: any) {
1029
1071
  app.service('$prompt').close(true, file);
1030
1072
  },
1031
- fileSelect(file) {
1073
+ fileSelect(file: any) {
1032
1074
  app.service('$prompt').close(true, file);
1033
1075
  },
1034
1076
  },
1035
1077
  modalDialogClass: 'modal-dialog-lg',
1036
- buttons: settings.allowCancel ? ['cancel'] : false,
1078
+ buttons: settings.allowCancel ? ['cancel'] : [],
1037
1079
  })
1038
1080
  ))
1039
1081
  }
@@ -1049,7 +1091,7 @@ export default class TeraFyServer {
1049
1091
  *
1050
1092
  * @returns {Promise<Array<ProjectFile>>} A collection of project files for the given project
1051
1093
  */
1052
- getProjectFiles(options) {
1094
+ getProjectFiles(options?: any): Promise<any[]> {
1053
1095
  let settings = {
1054
1096
  autoRequire: true,
1055
1097
  lazy: true,
@@ -1082,7 +1124,7 @@ export default class TeraFyServer {
1082
1124
  *
1083
1125
  * @returns {Promise<ProjectFile>} The eventual fetched ProjectFile (or requested subkey)
1084
1126
  */
1085
- getProjectFile(name, options) {
1127
+ getProjectFile(name: string, options?: any): Promise<any> {
1086
1128
  let settings = {
1087
1129
  subkey: null,
1088
1130
  cache: true,
@@ -1099,12 +1141,12 @@ export default class TeraFyServer {
1099
1141
  })
1100
1142
  : app.service('$projects').activeFiles // Otherwise use file cache
1101
1143
  )
1102
- .then(files =>
1103
- files.find(file =>
1144
+ .then((files: any[]) =>
1145
+ files.find((file: any) =>
1104
1146
  file.name == name
1105
1147
  )
1106
1148
  )
1107
- .then(file => file && settings.subkey ? file[settings.subkey] : file) // Provide subkey if asked
1149
+ .then((file: any) => file && settings.subkey ? (file as any)[settings.subkey] : file)
1108
1150
  }
1109
1151
 
1110
1152
 
@@ -1118,7 +1160,7 @@ export default class TeraFyServer {
1118
1160
  *
1119
1161
  * @returns {*} The file contents in the requested format
1120
1162
  */
1121
- getProjectFileContents(id, options) {
1163
+ getProjectFileContents(id: string, options?: any): Promise<any> {
1122
1164
  let settings = {
1123
1165
  format: 'blob',
1124
1166
  ...options,
@@ -1139,7 +1181,7 @@ export default class TeraFyServer {
1139
1181
  * @param {String} name The name + relative directory path component
1140
1182
  * @returns {Promise<ProjectFile>} The eventual ProjectFile created
1141
1183
  */
1142
- createProjectFile(name) {
1184
+ createProjectFile(name: string): Promise<any> {
1143
1185
  return Promise.resolve()
1144
1186
  .then(()=> app.service('$supabase').fileUpload(app.service('$projects').convertRelativePath(name), {
1145
1187
  file: new Blob([''], {type: 'text/plain'}),
@@ -1152,7 +1194,7 @@ export default class TeraFyServer {
1152
1194
  .then(()=> this.getProjectFile(name, {
1153
1195
  cache: false, // Force cache to update, as this is a new file
1154
1196
  }))
1155
- .then(file => file || Promise.reject(`Could not create new file "${name}"`))
1197
+ .then((file: any) => file || Promise.reject(`Could not create new file "${name}"`))
1156
1198
  }
1157
1199
 
1158
1200
 
@@ -1163,7 +1205,7 @@ export default class TeraFyServer {
1163
1205
  *
1164
1206
  * @returns {Promise} A promise which resolves when the operation has completed
1165
1207
  */
1166
- deleteProjectFile(id) {
1208
+ deleteProjectFile(id: string): Promise<null> {
1167
1209
  return app.service('$supabase').fileRemove(app.service('$projects').decodeFilePath(id))
1168
1210
  .then(()=> app.service('$projects').refreshFiles({ // Force a local file list update
1169
1211
  lazy: false,
@@ -1187,33 +1229,51 @@ export default class TeraFyServer {
1187
1229
  *
1188
1230
  * @returns {Promise} A promise which will resolve when the write operation has completed
1189
1231
  */
1190
- setProjectFileContents(id, contents, options) {
1191
- // Argument mangling {{{
1192
- // TODO: Horrible kludge to detect ID is "not a JSON blob"
1193
- if (typeof id == 'string' && !id.startsWith('{') && contents && options) { // Called as `(id, contents, options)`
1194
- // Pass
1195
- } else if (typeof id != 'string' && !options) { // Called as `(contents, options)`
1196
- [id, contents, options] = [null, id, contents];
1232
+ setProjectFileContents(id: string | any | null, contents: any, options?: any): Promise<null> {
1233
+ // Argument Mangling Logic (Simplified)
1234
+ let fileId: string | null = null;
1235
+ let fileContents: any;
1236
+ let mergedOptions: any;
1237
+
1238
+ if (typeof id === 'string') {
1239
+ fileId = id;
1240
+ fileContents = contents;
1241
+ mergedOptions = { ...options };
1242
+ } else if (id !== null && typeof id === 'object' && !(id instanceof Blob) && !(id instanceof File) && !(id instanceof FormData) && !Array.isArray(id)) {
1243
+ // Assuming called as (optionsObject)
1244
+ mergedOptions = { ...id };
1245
+ fileId = mergedOptions.id ?? null;
1246
+ fileContents = mergedOptions.contents;
1197
1247
  } else {
1198
- throw new Error('Unknown function signature. Requires (id:String?, contents:*, options:Object?)');
1248
+ // Assuming called as (contents, options)
1249
+ fileId = options?.id ?? null; // Check options for id if provided
1250
+ fileContents = id; // First arg is contents
1251
+ mergedOptions = { ...contents }; // Second arg is options
1199
1252
  }
1200
- // }}}
1253
+
1254
+ if (fileContents === undefined) throw new Error('setProjectFileContents requires contents to save.');
1201
1255
 
1202
1256
  let settings = {
1203
- id,
1257
+ id: fileId,
1204
1258
  autoRequire: true,
1205
1259
  hint: null,
1206
1260
  filename: null,
1207
1261
  title: 'Save file',
1208
1262
  meta: null,
1209
- ...options,
1263
+ ...mergedOptions, // Apply options derived from mangling
1210
1264
  };
1211
1265
 
1266
+
1212
1267
  return Promise.resolve()
1213
- .then(()=> settings.autoRequire && this.requireProject())
1214
1268
  .then(()=> {
1215
- if (settings.id) return; // We already have a file ID specified - skip
1216
-
1269
+ settings.autoRequire && this.requireProject()
1270
+ })
1271
+ .then((): Promise<string> => { // Ensure the promise returns a string (fileId)
1272
+ if (settings.id) {
1273
+ // Validate the provided ID exists? Optional, but good practice.
1274
+ // For now, just return it assuming it's valid.
1275
+ return Promise.resolve(settings.id);
1276
+ }
1217
1277
  // Prompt for a save filename
1218
1278
  return this.selectProjectFile({
1219
1279
  title: settings.title,
@@ -1222,15 +1282,20 @@ export default class TeraFyServer {
1222
1282
  saveFilename: settings.filename,
1223
1283
  autoRequire: false, // Handled above anyway
1224
1284
  })
1225
- .then(file => settings.id = file.id)
1285
+ .then((file: any) => {
1286
+ if (!file || !file.id) throw new Error('File selection cancelled or failed.');
1287
+ return file.id; // Return the selected file ID
1288
+ });
1226
1289
  })
1227
- .then(()=> { // Final checks
1228
- if (!settings.id) throw new Error("Could not determine file ID to save to.");
1290
+ .then((resolvedFileId: string) => {
1291
+ settings.id = resolvedFileId; // Update settings.id with the resolved/validated ID
1292
+ if (!settings.id) throw new Error("Could not determine file ID to save to."); // Final check
1293
+ return app.service('$supabase').fileSet(app.service('$projects').decodeFilePath(settings.id), fileContents, {
1294
+ overwrite: true,
1295
+ toast: false,
1296
+ // TODO: Handle settings.meta if $supabase.fileSet supports it
1297
+ });
1229
1298
  })
1230
- .then(()=> app.service('$supabase').fileSet(app.service('$projects').decodeFilePath(settings.id), contents, {
1231
- overwrite: true,
1232
- toast: false,
1233
- }))
1234
1299
  .then(()=> null)
1235
1300
  }
1236
1301
  // }}}
@@ -1252,7 +1317,7 @@ export default class TeraFyServer {
1252
1317
  *
1253
1318
  * @returns {Promise<Array<Ref>>} A collection of references from the selected file
1254
1319
  */
1255
- selectProjectLibrary(options) {
1320
+ selectProjectLibrary(options?: any): Promise<any[]> {
1256
1321
  let settings = {
1257
1322
  title: 'Select a citation library',
1258
1323
  hint: null,
@@ -1268,9 +1333,14 @@ export default class TeraFyServer {
1268
1333
  ...options,
1269
1334
  };
1270
1335
 
1336
+
1271
1337
  return app.service('$projects').promise()
1272
- .then(()=> this.selectProjectFile(settings))
1273
- .then(selectedFile => this.getProjectLibrary(selectedFile.id, settings))
1338
+ .then(()=> this.selectProjectFile(settings)) // Pass merged settings
1339
+ .then((selectedFile: any) => {
1340
+ if (!selectedFile || !selectedFile.id) throw new Error('Library selection failed or was cancelled.');
1341
+ // Pass relevant options down to getProjectLibrary
1342
+ return this.getProjectLibrary(selectedFile.id, settings);
1343
+ })
1274
1344
  }
1275
1345
 
1276
1346
 
@@ -1287,16 +1357,16 @@ export default class TeraFyServer {
1287
1357
  *
1288
1358
  * @returns {Promise<Array<Ref>>|Promise<*>} A collection of references (default bevahiour) or a whatever format was requested
1289
1359
  */
1290
- getProjectLibrary(id, options) {
1360
+ getProjectLibrary(id: string, options?: any): Promise<any> {
1291
1361
  let settings = {
1292
1362
  format: 'pojo',
1293
1363
  autoRequire: true,
1294
- filter: file => true, // eslint-disable-line no-unused-vars
1295
- find: files => files.at(0),
1364
+ filter: (file: any) => true, // Default filter
1365
+ find: (files: any[]) => files.at(0), // Default find
1296
1366
  ...options,
1297
1367
  };
1298
1368
 
1299
- let filePath = app.service('$projects').decodeFilePath(id);
1369
+ let filePath: string = app.service('$projects').decodeFilePath(id);
1300
1370
 
1301
1371
  return Promise.resolve()
1302
1372
  .then(()=> settings.autoRequire && this.requireProject())
@@ -1305,7 +1375,6 @@ export default class TeraFyServer {
1305
1375
  }))
1306
1376
  .then(blob => {
1307
1377
  if (!blob) throw new Error(`File not found or empty: ${filePath}`);
1308
-
1309
1378
  switch (settings.format) {
1310
1379
  // NOTE: Any updates to the format list should also extend setProjectLibrary()
1311
1380
  case 'pojo':
@@ -1348,8 +1417,33 @@ export default class TeraFyServer {
1348
1417
  *
1349
1418
  * @returns {Promise} A promise which resolves when the save operation has completed
1350
1419
  */
1351
- setProjectLibrary(id, refs, options) {
1420
+ setProjectLibrary(id: string | any | null, refs?: any, options?: any): Promise<null> {
1421
+ // Argument Mangling Logic (Simplified)
1422
+ let fileId: string | null = null;
1423
+ let libraryRefs: any;
1424
+ let mergedOptions: any;
1425
+
1426
+ if (typeof id === 'string') {
1427
+ fileId = id;
1428
+ libraryRefs = refs;
1429
+ mergedOptions = { ...options };
1430
+ } else if (id !== null && typeof id === 'object' && !(id instanceof Blob) && !(id instanceof File) && !Array.isArray(id)) {
1431
+ // Assuming called as (optionsObject)
1432
+ mergedOptions = { ...id };
1433
+ fileId = mergedOptions.id ?? null;
1434
+ libraryRefs = mergedOptions.refs;
1435
+ } else {
1436
+ // Assuming called as (refs, options)
1437
+ fileId = options?.id ?? null; // Check options for id if provided
1438
+ libraryRefs = id; // First arg is refs
1439
+ mergedOptions = { ...refs }; // Second arg is options
1440
+ }
1441
+
1442
+ if (libraryRefs === undefined) throw new Error('setProjectLibrary requires refs to save.');
1443
+
1352
1444
  let settings = {
1445
+ id: fileId,
1446
+ refs: libraryRefs,
1353
1447
  format: 'auto',
1354
1448
  autoRequire: true,
1355
1449
  hint: null,
@@ -1357,20 +1451,17 @@ export default class TeraFyServer {
1357
1451
  title: 'Save citation library',
1358
1452
  overwrite: true,
1359
1453
  meta: null,
1360
- ...(
1361
- typeof id == 'string' && Array.isArray(refs) ? {id, refs, ...options} // Called as (id, refs, options?)
1362
- : Array.isArray(id) || refs instanceof Blob || refs instanceof File ? {refs: id, ...refs} // Called as (refs, options?)
1363
- : id // Called as (options?)
1364
- )
1454
+ ...mergedOptions // Apply options derived from mangling
1365
1455
  };
1366
- if (!settings.refs) throw new Error('No refs to save');
1367
1456
 
1368
- let filePath; // Eventual Supabase path to use
1457
+ let filePath: any; // Eventual Supabase path to use
1369
1458
  return Promise.resolve()
1370
1459
  .then(()=> settings.autoRequire && this.requireProject())
1371
- .then(()=> {
1372
- if (settings.id) return; // We already have a file ID specified - skip
1373
-
1460
+ .then((): Promise<string> => { // Ensure promise returns string (fileId)
1461
+ if (settings.id) {
1462
+ // Optional: Validate settings.id exists?
1463
+ return Promise.resolve(settings.id);
1464
+ }
1374
1465
  // Prompt for a save filename
1375
1466
  return this.selectProjectFile({
1376
1467
  title: settings.title,
@@ -1382,9 +1473,14 @@ export default class TeraFyServer {
1382
1473
  },
1383
1474
  autoRequire: false, // Handled above anyway
1384
1475
  })
1385
- .then(file => settings.id = file.id)
1476
+ .then((file: any) => {
1477
+ if (!file || !file.id) throw new Error('File selection cancelled or failed.');
1478
+ return file.id; // Return selected file ID
1479
+ });
1386
1480
  })
1387
- .then(()=> { // Compute filePath
1481
+ .then((resolvedFileId: string)=> { // Compute filePath
1482
+ settings.id = resolvedFileId; // Update settings.id
1483
+ if (!settings.id) throw new Error("Could not determine file ID to save library to.");
1388
1484
  filePath = app.service('$projects').decodeFilePath(settings.id);
1389
1485
  })
1390
1486
  .then(()=> {
@@ -1417,10 +1513,11 @@ export default class TeraFyServer {
1417
1513
  throw new Error(`Unsupported library format "${settings.format}"`);
1418
1514
  }
1419
1515
  })
1420
- .then(fileBlob => app.service('$supabase').fileUpload(filePath, {
1516
+ .then((fileBlob: File) => app.service('$supabase').fileUpload(filePath, { // Expect File type
1421
1517
  file: fileBlob,
1422
1518
  overwrite: settings.overwrite,
1423
1519
  mode: 'encoded',
1520
+ // TODO: Handle settings.meta if $supabase.fileUpload supports it
1424
1521
  }))
1425
1522
  .then(()=> null)
1426
1523
  }
@@ -1436,7 +1533,7 @@ export default class TeraFyServer {
1436
1533
  * @param {Object} log The log entry to create
1437
1534
  * @returns {Promise} A promise which resolves when the operation has completed
1438
1535
  */
1439
- projectLog(log) {
1536
+ projectLog(log: any): Promise<void> {
1440
1537
  return app.service('$projects').log(log);
1441
1538
  }
1442
1539
  // }}}
@@ -1450,7 +1547,7 @@ export default class TeraFyServer {
1450
1547
  * @param {String} [options.path] The URL path segment to restore on next refresh
1451
1548
  * @param {String} [options.title] The page title associated with the path
1452
1549
  */
1453
- setPage(options) {
1550
+ setPage(options: any): void {
1454
1551
  app.service('$projects').setPage(options);
1455
1552
  }
1456
1553
  // }}}
@@ -1462,7 +1559,7 @@ export default class TeraFyServer {
1462
1559
  *
1463
1560
  * @param {Object} [options] Additional options to merge into `settings`
1464
1561
  */
1465
- constructor(options) {
1562
+ constructor(options?: any) {
1466
1563
  Object.assign(this.settings, options);
1467
1564
  }
1468
1565
 
@@ -1470,9 +1567,12 @@ export default class TeraFyServer {
1470
1567
  /**
1471
1568
  * Initialize the browser listener
1472
1569
  */
1473
- init() {
1474
- globalThis.addEventListener('message', this.acceptMessage.bind(this));
1475
- this.debug('INFO', 1, 'Ready');
1570
+ init(): void {
1571
+ // Ensure this only runs in a browser context
1572
+ if (typeof window !== 'undefined' && typeof globalThis !== 'undefined') {
1573
+ globalThis.addEventListener('message', this.acceptMessage.bind(this));
1574
+ this.debug('INFO', 1, 'Ready');
1575
+ }
1476
1576
  }
1477
1577
  // }}}
1478
1578
 
@@ -1490,7 +1590,7 @@ export default class TeraFyServer {
1490
1590
  *
1491
1591
  * @returns {Promise} A promise which resolves when the alert has been dismissed
1492
1592
  */
1493
- uiAlert(text, options) {
1593
+ uiAlert(text: string | any, options?: any): Promise<void> {
1494
1594
  let settings = {
1495
1595
  body: 'Alert!',
1496
1596
  isHtml: false,
@@ -1507,7 +1607,10 @@ export default class TeraFyServer {
1507
1607
  app.service('$prompt').dialog({
1508
1608
  title: settings.title,
1509
1609
  body: settings.body,
1510
- buttons: settings.buttons == 'ok' ? ['ok'] : false,
1610
+ buttons:
1611
+ settings.buttons == 'ok' ? ['ok']
1612
+ : settings.buttons === false ? []
1613
+ : settings.buttons, // Allow passing custom button arrays
1511
1614
  isHtml: settings.isHtml,
1512
1615
  dialogClose: 'resolve', // Resolve promise when closed
1513
1616
  })
@@ -1527,7 +1630,7 @@ export default class TeraFyServer {
1527
1630
  *
1528
1631
  * @returns {Promise} A promise which resolves with `Promise.resolve('OK')` or rejects with `Promise.reject('CANCEL')`
1529
1632
  */
1530
- uiConfirm(text, options) {
1633
+ uiConfirm(text: string | any, options?: any): Promise<'OK'> {
1531
1634
  let settings = {
1532
1635
  body: 'Confirm?',
1533
1636
  isHtml: false,
@@ -1553,11 +1656,11 @@ export default class TeraFyServer {
1553
1656
  {
1554
1657
  title: 'Cancel',
1555
1658
  class: 'btn btn-danger',
1556
- click: 'reject',
1659
+ click: 'reject', // Reject promise
1557
1660
  },
1558
1661
  ],
1559
1662
  })
1560
- .then(()=> 'OK') // Resolve with 'OK' if OK button clicked
1663
+ .then(()=> 'OK' as 'OK') // Resolve with 'OK' if OK button clicked
1561
1664
  .catch(()=> Promise.reject('CANCEL')) // Reject with 'CANCEL' if Cancel button clicked or closed
1562
1665
  );
1563
1666
  }
@@ -1569,8 +1672,15 @@ export default class TeraFyServer {
1569
1672
  * @function uiPanic
1570
1673
  * @param {String} [text] Text to display
1571
1674
  */
1572
- uiPanic(text) {
1573
- window.panic(text);
1675
+ uiPanic(text: any): void {
1676
+ // Ensure window context exists
1677
+ if (typeof window !== 'undefined' && typeof window.panic === 'function') {
1678
+ window.panic(text);
1679
+ } else {
1680
+ console.error("PANIC (window.panic not available):", text);
1681
+ // Fallback behavior if window.panic doesn't exist
1682
+ alert(`PANIC: ${text}`);
1683
+ }
1574
1684
  }
1575
1685
 
1576
1686
 
@@ -1588,10 +1698,19 @@ export default class TeraFyServer {
1588
1698
  *
1589
1699
  * @returns {Promise} A promise which resolves when the dialog has been updated
1590
1700
  */
1591
- uiProgress(options) {
1592
- if (options === false) options = {close: true}; // Shorthand to close existing ui progress window
1593
-
1594
- if (!options?.close && this._uiProgress.options === null) { // Create new uiProgress options object
1701
+ uiProgress(options?: any): Promise<void> {
1702
+ let currentOptions = options === false ? {close: true} : options || {};
1703
+
1704
+ if (currentOptions.close) { // Asked to close the dialog
1705
+ const closePromise = this._uiProgress.promise
1706
+ ? app.service('$prompt').close(true) // Assume close takes 1 arg
1707
+ : Promise.resolve();
1708
+ return closePromise.then(()=> { // Release state
1709
+ this._uiProgress.options = null;
1710
+ this._uiProgress.promise = null;
1711
+ });
1712
+ } else if (!this._uiProgress.promise) { // Not created the dialog yet
1713
+ // Initialize options if they don't exist
1595
1714
  this._uiProgress.options = reactive({
1596
1715
  body: '',
1597
1716
  bodyHtml: false,
@@ -1599,39 +1718,32 @@ export default class TeraFyServer {
1599
1718
  close: false,
1600
1719
  progress: 0,
1601
1720
  progressMax: 0,
1602
- backdrop: true,
1603
- ...options,
1721
+ backdrop: true, // Default backdrop
1722
+ ...currentOptions, // Apply initial options
1604
1723
  });
1605
- } else { // Merge options with existing uiProgress window
1606
- Object.assign(this._uiProgress.options, options);
1607
- }
1608
-
1609
- if (this._uiProgress.options.close) { // Asked to close the dialog
1610
- return Promise.resolve()
1611
- .then(()=> this._uiProgress.promise && app.service('$prompt').close(true)) // Close the dialog if its open
1612
- .then(()=> { // Release state
1613
- this._uiProgress.options = {};
1614
- this._uiProgress.promise = null;
1615
- })
1616
- } else if (!this._uiProgress.promise) { // Not created the dialog yet
1617
1724
  this._uiProgress.promise = this.requestFocus(()=>
1618
1725
  app.service('$prompt').dialog({
1619
- title: this._uiProgress.options.title,
1620
- backdrop: this._uiProgress.options.backdrop ?? true, // pass backdrop to allow 'static' dialog
1726
+ title: this._uiProgress.options?.title,
1727
+ backdrop: this._uiProgress.options?.backdrop ?? true,
1621
1728
  component: 'uiProgress',
1622
- componentProps: this._uiProgress.options,
1729
+ componentProps: this._uiProgress.options, // Pass reactive object
1623
1730
  closeable: false,
1624
1731
  keyboard: false,
1625
1732
  })
1626
1733
  );
1627
- return Promise.resolve();
1734
+ return Promise.resolve(); // Dialog creation is async via requestFocus
1735
+ } else if (this._uiProgress.options) { // Dialog exists, merge options
1736
+ Object.assign(this._uiProgress.options, currentOptions);
1737
+ return Promise.resolve(); // Updates handled by reactivity
1628
1738
  } else {
1629
- throw new Error('Unknown uiProgress state');
1739
+ // Should not happen if initialized correctly
1740
+ console.warn("uiProgress called in unexpected state");
1741
+ return Promise.resolve();
1630
1742
  }
1631
1743
  }
1632
1744
 
1633
- _uiProgress = {
1634
- options: {},
1745
+ _uiProgress: { options: any | null, promise: Promise<any> | null } = {
1746
+ options: null,
1635
1747
  promise: null,
1636
1748
  };
1637
1749
 
@@ -1651,7 +1763,7 @@ export default class TeraFyServer {
1651
1763
  *
1652
1764
  * @returns {Promise<*>} Either the eventual user value or a throw with `Promise.reject('CANCEL')`
1653
1765
  */
1654
- uiPrompt(text, options) {
1766
+ uiPrompt(text: string | any, options?: any): Promise<any> {
1655
1767
  let settings = {
1656
1768
  body: '',
1657
1769
  isHtml: false,
@@ -1682,23 +1794,26 @@ export default class TeraFyServer {
1682
1794
  class: 'btn btn-success',
1683
1795
  icon: 'fas fa-check',
1684
1796
  title: 'Ok',
1685
- click() {
1686
- return app.service('$prompt').close(true, this.newValue);
1797
+ click(): any {
1798
+ // Assuming 'this' is the component instance with 'newValue' property
1799
+ // And $prompt service is available globally via 'app'
1800
+ app.service('$prompt').close(true, (this as any).newValue); // Use app.$prompt.close
1687
1801
  },
1688
1802
  },
1689
- 'cancel',
1803
+ 'cancel', // Standard cancel button that rejects
1690
1804
  ],
1691
1805
  })
1692
1806
  )
1693
- .then(answer => {
1694
- if (answer) {
1807
+ .then((answer: any) => {
1808
+ // Check if the answer is non-empty or if required is false
1809
+ if (answer || !settings.required) {
1695
1810
  return answer;
1696
- } else if (settings.required) { // Required answer but returned response is falsy
1811
+ } else {
1812
+ // If required and answer is empty/nullish, treat as cancel
1697
1813
  return Promise.reject('CANCEL');
1698
- } else { // Everything else - relay the raw value
1699
- return answer;
1700
1814
  }
1701
1815
  })
1816
+ // Catch rejection from 'cancel' button or closing the dialog
1702
1817
  .catch(()=> Promise.reject('CANCEL'))
1703
1818
  }
1704
1819
 
@@ -1710,7 +1825,7 @@ export default class TeraFyServer {
1710
1825
  *
1711
1826
  * @returns {Void} This function is fatal
1712
1827
  */
1713
- uiThrow(error) {
1828
+ uiThrow(error: any): Promise<void> {
1714
1829
  return this.requestFocus(()=>
1715
1830
  app.service('$errors').catch(error)
1716
1831
  );
@@ -1730,7 +1845,10 @@ export default class TeraFyServer {
1730
1845
  *
1731
1846
  * @returns {WindowProxy} The opened window object (if `noopener` is not set in permissions)
1732
1847
  */
1733
- uiWindow(url, options) {
1848
+ uiWindow(url: string | URL, options?: any): WindowProxy | null {
1849
+ // Ensure this runs only in browser context
1850
+ if (typeof window === 'undefined' || typeof screen === 'undefined') return null;
1851
+
1734
1852
  let settings = {
1735
1853
  width: 500,
1736
1854
  height: 600,
@@ -1745,7 +1863,9 @@ export default class TeraFyServer {
1745
1863
  ...options,
1746
1864
  };
1747
1865
 
1748
- return window.open(url, '_blank', Object.entries({
1866
+ const urlString = typeof url === 'string' ? url : url.toString();
1867
+
1868
+ const features = Object.entries({
1749
1869
  ...settings.permissions,
1750
1870
  width: settings.width,
1751
1871
  height: settings.height,
@@ -1754,12 +1874,10 @@ export default class TeraFyServer {
1754
1874
  top: screen.height/2 - settings.height/2,
1755
1875
  }),
1756
1876
  })
1757
- .map(([key, val]) => key + '=' + (
1758
- typeof val == 'boolean' ? val ? '1' : '0' // Map booleans to 1/0
1759
- : val
1760
- ))
1761
- .join(', ')
1762
- );
1877
+ .map(([key, val]) => `${key}=${typeof val === 'boolean' ? (val ? 'yes' : 'no') : val}`) // Use yes/no for booleans
1878
+ .join(',');
1879
+
1880
+ return window.open(urlString, '_blank', features);
1763
1881
  }
1764
1882
 
1765
1883
 
@@ -1772,30 +1890,42 @@ export default class TeraFyServer {
1772
1890
  * @param {Object} [options] Additional options to mutate behaviour
1773
1891
  * @param {Boolean|String} [options.logo=false] Add a logo to the output, if boolean true the Tera-tools logo is used otherwise specify a path or URL
1774
1892
  */
1775
- uiSplat(content, options) {
1893
+ uiSplat(content: Element | string | false, options?: any): void {
1894
+ // Ensure this runs only in browser context
1895
+ if (typeof window === 'undefined' || typeof document === 'undefined') return;
1896
+
1776
1897
  let settings = {
1777
1898
  logo: false,
1778
1899
  ...options,
1779
1900
  };
1780
1901
 
1781
- if (!content) { // Remove content
1782
- globalThis.document.body.querySelector('.tera-fy-uiSplat').remove();
1902
+ // Remove existing splat first
1903
+ const existingSplat = globalThis.document.body.querySelector('.tera-fy-uiSplat');
1904
+ if (existingSplat) {
1905
+ existingSplat.remove();
1906
+ }
1907
+
1908
+ if (!content) { // If content is false, just remove and return
1783
1909
  return;
1784
1910
  }
1785
1911
 
1786
- let compiledContent = typeof content == 'string'
1787
- ? (()=> {
1788
- let el = document.createElement('div')
1789
- el.innerHTML = content;
1790
- return el;
1791
- })()
1792
- : content;
1912
+ let compiledContent: Element;
1913
+ if (typeof content == 'string') {
1914
+ let el = document.createElement('div')
1915
+ el.innerHTML = content;
1916
+ // If the string contained multiple top-level elements, wrap them
1917
+ compiledContent = el.children.length === 1 ? el.firstElementChild! : el;
1918
+ } else {
1919
+ compiledContent = content;
1920
+ }
1921
+
1793
1922
 
1794
1923
  compiledContent.classList.add('tera-fy-uiSplat');
1795
1924
 
1796
1925
  if (settings.logo) {
1797
1926
  let logoEl = document.createElement('div');
1798
1927
  logoEl.innerHTML = `<img src="${typeof settings.logo == 'string' ? settings.logo : '/assets/logo/logo.svg'}" class="img-logo"/>`;
1928
+ // Prepend logo within the content element
1799
1929
  compiledContent.prepend(logoEl);
1800
1930
  }
1801
1931
 
@@ -1814,29 +1944,40 @@ export default class TeraFyServer {
1814
1944
  * @param {Number} [verboseLevel=1] The verbosity level to trigger at. If `settings.verbosity` is lower than this, the message is ignored
1815
1945
  * @param {...*} [msg] Output to show
1816
1946
  */
1817
- debug(...msg) {
1947
+ debug(...inputArgs: any[]): void {
1948
+ // Ensure console exists
1949
+ if (typeof console === 'undefined') return;
1818
1950
  if (!this.settings.devMode || this.settings.verbosity < 1) return; // Debugging is disabled
1819
- let method = 'log';
1951
+
1952
+ let method: keyof Console = 'log'; // Default method
1820
1953
  let verboseLevel = 1;
1954
+ let msgArgs = [...inputArgs]; // Copy args to modify
1955
+
1821
1956
  // Argument mangling for prefix method + verbosity level {{{
1822
- if (typeof msg[0] == 'string' && ['INFO', 'LOG', 'WARN', 'ERROR'].includes(msg[0])) {
1823
- method = msg.shift().toLowerCase();
1957
+ if (typeof msgArgs[0] == 'string' && ['INFO', 'LOG', 'WARN', 'ERROR'].includes(msgArgs[0].toUpperCase())) {
1958
+ const potentialMethod = msgArgs.shift().toLowerCase() as keyof Console;
1959
+ // Check if it's a valid console method
1960
+ if (potentialMethod in console) {
1961
+ method = potentialMethod;
1962
+ } else {
1963
+ msgArgs.unshift(potentialMethod); // Put it back if not a valid method
1964
+ }
1824
1965
  }
1825
1966
 
1826
- if (typeof msg[0] == 'number') {
1827
- verboseLevel = msg[0];
1828
- msg.shift();
1967
+ if (typeof msgArgs[0] == 'number') {
1968
+ verboseLevel = msgArgs.shift();
1829
1969
  }
1830
1970
  // }}}
1831
1971
 
1832
1972
  if (this.settings.verbosity < verboseLevel) return; // Called but this output is too verbose for our settings - skip
1833
1973
 
1834
- console[method](
1974
+ // Use type assertion for dynamic console method call
1975
+ (console as any)[method](
1835
1976
  '%c[TERA-FY SERVER]',
1836
1977
  'font-weight: bold; color: #4d659c;',
1837
- ...msg,
1978
+ ...msgArgs,
1838
1979
  );
1839
1980
  }
1840
1981
  /* eslint-enable */
1841
1982
  // }}}
1842
- }
1983
+ }