@iebh/tera-fy 2.0.22 → 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 +36 -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} +201 -168
  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,10 +1,10 @@
1
1
  import {
2
2
  isEmpty,
3
- isEqual,
4
3
  cloneDeep,
5
4
  random,
6
5
  sample,
7
6
  throttle,
7
+ isEqual
8
8
  } from 'lodash-es';
9
9
  import {
10
10
  doc as FirestoreDocRef,
@@ -12,10 +12,32 @@ import {
12
12
  onSnapshot as FirestoreOnSnapshot,
13
13
  setDoc as FirestoreSetDoc,
14
14
  updateDoc as FirestoreUpdateDoc,
15
+ DocumentReference,
16
+ Firestore,
17
+ Unsubscribe,
15
18
  } from 'firebase/firestore';
19
+ // @ts-ignore
16
20
  import marshal from '@momsfriendlydevco/marshal';
17
21
  import {nanoid} from 'nanoid';
18
22
  import PromiseRetry from 'p-retry';
23
+ import {FirebaseApp} from 'firebase/app';
24
+ import { BoundSupabaseyFunction } from '@iebh/supabasey';
25
+
26
+
27
+ interface ReactiveWrapper<T = any> {
28
+ doc: T;
29
+ setState: (newState: T) => void;
30
+ getState: () => T;
31
+ watch: (cb: (newState: T) => void) => void;
32
+ }
33
+
34
+ interface PathSplitResult {
35
+ fsCollection: string;
36
+ fsId: string;
37
+ entity: string;
38
+ id: string;
39
+ relation?: string;
40
+ }
19
41
 
20
42
 
21
43
  /**
@@ -29,9 +51,9 @@ export default class Syncro {
29
51
  /**
30
52
  * Firebase instance in use
31
53
  *
32
- * @type {Firebase}
54
+ * @type {FirebaseApp}
33
55
  */
34
- static firebase;
56
+ static firebase: FirebaseApp;
35
57
 
36
58
 
37
59
  /**
@@ -39,15 +61,15 @@ export default class Syncro {
39
61
  *
40
62
  * @type {Firestore}
41
63
  */
42
- static firestore;
64
+ static firestore: Firestore;
43
65
 
44
66
 
45
67
  /**
46
- * Supabase instance in use
68
+ * Supabasey instance in use
47
69
  *
48
- * @type {SupabaseClient}
70
+ * @type {Supabasey}
49
71
  */
50
- static supabase;
72
+ static supabasey: BoundSupabaseyFunction;
51
73
 
52
74
 
53
75
  /**
@@ -56,16 +78,16 @@ export default class Syncro {
56
78
  *
57
79
  * @type {String}
58
80
  */
59
- static session;
81
+ static session: string | undefined;
60
82
 
61
83
 
62
84
  /**
63
85
  * OPTIONAL SyncroEntiries from './entiries.js' if its required
64
86
  * This only gets populated if `config.forceLocalInit` is truthy and we've mounted at least one Syncro
65
87
  *
66
- * @type {Object}
88
+ * @type {Record<string, any>}
67
89
  */
68
- static SyncroEntities;
90
+ static SyncroEntities: Record<string, any>;
69
91
 
70
92
 
71
93
  /**
@@ -73,24 +95,24 @@ export default class Syncro {
73
95
  *
74
96
  * @type {String}
75
97
  */
76
- path;
98
+ path: string;
77
99
 
78
100
 
79
101
  /**
80
102
  * The Firestore docHandle when calling various Firestore functions
81
103
  *
82
- * @type {FirestoreRef}
104
+ * @type {DocumentReference | undefined}
83
105
  */
84
- docRef;
106
+ docRef: DocumentReference | undefined;
85
107
 
86
108
 
87
109
  /**
88
110
  * The reactive object managed by this Syncro instance
89
111
  * The nature of this varies by framework and what `getReactive()` provides
90
112
  *
91
- * @type {*}
113
+ * @type {any}
92
114
  */
93
- value;
115
+ value: any;
94
116
 
95
117
 
96
118
  /**
@@ -110,9 +132,9 @@ export default class Syncro {
110
132
  * @property {Object} context Additional named parameters to pass to callbacks like initState
111
133
  */
112
134
  config = {
113
- heartbeatInterval: 50_000, //~= 50s
114
- syncroRegistryUrl: 'https://tera-tools.com/api/sync',
115
- context: {},
135
+ heartbeatInterval: 50_000 as number, //~= 50s
136
+ syncroRegistryUrl: 'https://tera-tools.com/api/sync' as string,
137
+ context: {} as Record<string, any>,
116
138
  };
117
139
 
118
140
 
@@ -134,7 +156,7 @@ export default class Syncro {
134
156
  *
135
157
  * @param {*...} [msg] The message to output
136
158
  */
137
- debug(...msg) {} // eslint-disable-line no-unused-vars
159
+ debug(...msg: any[]) {} // eslint-disable-line no-unused-vars
138
160
 
139
161
 
140
162
  /**
@@ -144,7 +166,7 @@ export default class Syncro {
144
166
  *
145
167
  * @param {*...} [msg] The message to output
146
168
  */
147
- debugError(...msg) {
169
+ debugError(...msg: any[]) {
148
170
  console.log(`[Syncro ${this.path}]`, ...msg);
149
171
  }
150
172
 
@@ -155,7 +177,7 @@ export default class Syncro {
155
177
  * @param {String} path Mount path for the Syncro. Should be in the form `${ENTITY}::${ID}(::${RELATION})?`
156
178
  * @param {Object} [options] Additional instance setters (mutates instance directly), note that the `config` subkey is merged with the existing config rather than assigned
157
179
  */
158
- constructor(path, options) {
180
+ constructor(path: string, options?: any) {
159
181
  this.path = path;
160
182
  Object.assign(this, {
161
183
  ...options,
@@ -176,7 +198,7 @@ export default class Syncro {
176
198
  *
177
199
  * @returns {Promise} A promise which resolves when the operation has completed
178
200
  */
179
- destroy() {
201
+ destroy(): Promise<any[]> {
180
202
  this.debug('Destroy!');
181
203
  return Promise.all(this._destroyActions
182
204
  .map(fn => fn())
@@ -189,9 +211,9 @@ export default class Syncro {
189
211
  * Actions to preform when we are destroying this instance
190
212
  * This is an array of function callbacks to execute in parallel when `destroy()` is called
191
213
  *
192
- * @type {Array<Function>}
214
+ * @type {Array<() => void>}
193
215
  */
194
- _destroyActions = [];
216
+ _destroyActions: Array<() => void> = [];
195
217
 
196
218
 
197
219
  /**
@@ -200,18 +222,18 @@ export default class Syncro {
200
222
  *
201
223
  * @param {Object} value Initial value of the reactive
202
224
  *
203
- * @returns {Object} A reactive object prototype
225
+ * @returns {ReactiveWrapper} A reactive object prototype
204
226
  * @property {Object} doc The reactive object
205
227
  * @property {Function} setState Function used to overwrite the default state, called as `(newState:Object)`
206
228
  * @property {Function} getState Function used to fetch the current snapshot state, called as `()`
207
229
  * @property {Function} watch Function used to set up state watchers, should call its callback when a change is detected, called as `(cb:Function)`
208
230
  */
209
- getReactive(value) {
231
+ getReactive(value: any): ReactiveWrapper {
210
232
  console.warn('Syncro.getReactive has not been subclassed, assuming a POJO response');
211
- let doc = {...value};
233
+ let doc: Record<string, any> = {...value};
212
234
  return {
213
235
  doc,
214
- setState(state) {
236
+ setState(state: any) {
215
237
  // Shallow copy all sub keys into existing object (keeping the object pointer)
216
238
  Object.entries(state || {})
217
239
  .forEach(([k, v]) => doc[k] = v)
@@ -219,7 +241,7 @@ export default class Syncro {
219
241
  getState() {
220
242
  return cloneDeep(doc);
221
243
  },
222
- watch(cb) { // eslint-disable-line no-unused-vars
244
+ watch(cb: (newState: any) => void) { // eslint-disable-line no-unused-vars
223
245
  // Stub
224
246
  },
225
247
  };
@@ -238,14 +260,9 @@ export default class Syncro {
238
260
  * @param {Object} [options] Additional options to mutate behaviour
239
261
  * @param {Boolean} [options.allowAsterisk=false] Whether to allow the meta asterisk character when recognising paths, this is used by the SyncroKeyed class
240
262
  *
241
- * @returns {Object} An object composed of the session path components
242
- * @property {String} fbEntity The top level Firebase collection to store within
243
- * @property {String} fsId The top level Firebase ID of the collection to store as, this is either just a copy of the ID or a combination of id + relation
244
- * @property {String} entity A valid entity name (in plural form e.g. 'projects')
245
- * @property {String} id A valid UUID ID
246
- * @property {String} [relation] A string representing a sub-relationship. Usually a short string alias
263
+ * @returns {PathSplitResult} An object composed of the session path components
247
264
  */
248
- static pathSplit(path, options) {
265
+ static pathSplit(path: string, options?: any): PathSplitResult {
249
266
  let settings = {
250
267
  allowAsterisk: false,
251
268
  ...options,
@@ -265,10 +282,10 @@ export default class Syncro {
265
282
  + '$'
266
283
  );
267
284
 
268
- let extracted = { ...pathMatcher.exec(path)?.groups };
285
+ let extracted = { ...pathMatcher.exec(path)?.groups } as { entity?: string, id?: string, relation?: string };
269
286
 
270
- if (!extracted) throw new Error(`Invalid session path syntax "${path}"`);
271
- if (Syncro.SyncroEntities && !(extracted.entity in SyncroEntities)) throw new Error(`Unsupported entity "${path}" -> Entity="${extracted.entity}"`);
287
+ if (!extracted || !extracted.entity || !extracted.id) throw new Error(`Invalid session path syntax "${path}"`);
288
+ if (Syncro.SyncroEntities && !(extracted.entity in Syncro.SyncroEntities)) throw new Error(`Unsupported entity "${path}" -> Entity="${extracted.entity}"`);
272
289
 
273
290
  return {
274
291
  entity: extracted.entity,
@@ -292,7 +309,7 @@ export default class Syncro {
292
309
  * @param {Object} snapshot The current state to convert
293
310
  * @returns {Object} A Firebase compatible object
294
311
  */
295
- static toFirestore(snapshot = {}) {
312
+ static toFirestore(snapshot: any = {}): any {
296
313
  return marshal.serialize(snapshot, {
297
314
  circular: false,
298
315
  clone: true, // Clone away from the original Vue Reactive so we dont mangle it while traversing
@@ -312,7 +329,7 @@ export default class Syncro {
312
329
  * @param {Object} snapshot The raw Firebase state to convert
313
330
  * @returns {Object} A JavaScript POJO representing the converted state
314
331
  */
315
- static fromFirestore(snapshot = {}) {
332
+ static fromFirestore(snapshot: any = {}): any {
316
333
  return marshal.deserialize(snapshot, {
317
334
  circular: false,
318
335
  clone: true, // Clone away from original so we don't trigger a loop within Firebase
@@ -334,8 +351,8 @@ export default class Syncro {
334
351
  * @param {Object} data The raw value to convert
335
352
  * @returns {Object} A Firestore compatible, typed data structure
336
353
  */
337
- static toFirestoreFields(data) {
338
- const result = {};
354
+ static toFirestoreFields(data: any): Record<string, any> {
355
+ const result: Record<string, any> = {};
339
356
 
340
357
  for (const [key, value] of Object.entries(data)) {
341
358
  const type = typeof value;
@@ -349,9 +366,11 @@ export default class Syncro {
349
366
  } else if (value === null) {
350
367
  result[key] = { nullValue: null };
351
368
  } else if (Array.isArray(value)) {
352
- result[key] = { arrayValue: { values:
353
- value.map(item => Syncro.toFirestoreFields({item}).item)
354
- }};
369
+ // Need to handle the inner item structure correctly
370
+ result[key] = { arrayValue: { values: value.map(item => {
371
+ const field = Syncro.toFirestoreFields({ item });
372
+ return field.item; // Extract the typed value from the temporary {item: ...} structure
373
+ }) } };
355
374
  } else if (type === 'object') {
356
375
  result[key] = { mapValue: { fields: Syncro.toFirestoreFields(value) } };
357
376
  }
@@ -370,26 +389,25 @@ export default class Syncro {
370
389
  * @param {Object} fields The raw Snapshot to convert
371
390
  * @returns {Object} A JavaScript POJO representing the converted state
372
391
  */
373
- static fromFirestoreFields(fields = {}) {
374
- let result = {};
392
+ static fromFirestoreFields(fields: any = {}): any {
393
+ let result: Record<string, any> = {};
375
394
  for (let key in fields) {
376
395
  let value = fields[key];
377
396
  let isDocumentType = [
378
397
  'stringValue', 'booleanValue', 'doubleValue',
379
- 'integerValue', 'timestampValue', 'mapValue', 'arrayValue', 'nullValue',
380
- ].find(t => t === key);
398
+ 'integerValue', 'timestampValue', 'mapValue', 'arrayValue', 'nullValue', // Added nullValue
399
+ ].find(t => t === Object.keys(value)[0]); // Check the first key of the value object
381
400
 
382
401
  if (isDocumentType) {
383
- let item = ['stringValue', 'booleanValue', 'doubleValue', 'integerValue', 'timestampValue']
384
- .find(t => t === key)
385
-
386
- if (item) {
387
- return value;
388
- } else if ('mapValue' == key) {
389
- return Syncro.fromFirestoreFields(value.fields || {});
390
- } else if ('arrayValue' == key) {
391
- let list = value.values;
392
- return !!list ? list.map(l => Syncro.fromFirestoreFields(l)) : [];
402
+ if (isDocumentType === 'mapValue') {
403
+ result[key] = Syncro.fromFirestoreFields(value.mapValue.fields || {});
404
+ } else if (isDocumentType === 'arrayValue') {
405
+ let list = value.arrayValue.values;
406
+ result[key] = !!list ? list.map((l: any) => Syncro.fromFirestoreFields(l)) : [];
407
+ } else if (isDocumentType === 'nullValue') {
408
+ result[key] = null;
409
+ } else {
410
+ result[key] = value[isDocumentType];
393
411
  }
394
412
  } else {
395
413
  // This case might not be standard Firestore field structure, but handle recursively
@@ -407,14 +425,14 @@ export default class Syncro {
407
425
  *
408
426
  * @returns {Promise<Object|Null>} An eventual snapshot of the given path, if the entity doesn't exist null is returned
409
427
  */
410
- static getSnapshot(path) {
428
+ static getSnapshot(path: string): Promise<any | null> {
411
429
  let {fsCollection, fsId} = Syncro.pathSplit(path);
412
430
 
413
431
  return Promise.resolve()
414
432
  .then(async ()=> FirestoreGetDoc( // Set up binding and wait for it to come ready
415
433
  FirestoreDocRef(Syncro.firestore, fsCollection, fsId)
416
434
  ))
417
- .then(doc => doc
435
+ .then(doc => doc.exists() // Use exists() method
418
436
  ? Syncro.fromFirestore(doc.data())
419
437
  : null
420
438
  )
@@ -432,20 +450,23 @@ export default class Syncro {
432
450
  *
433
451
  * @returns {Promise<*>} The state object after it has been applied
434
452
  */
435
- static setSnapshot(path, state, options) {
453
+ static setSnapshot(path: string, state: any, options?: { method?: 'merge' | 'set' }): Promise<any> {
436
454
  let settings = {
437
455
  method: 'merge',
438
456
  ...options,
439
457
  };
440
458
  let {fsCollection, fsId} = Syncro.pathSplit(path);
459
+ const docRef = FirestoreDocRef(Syncro.firestore, fsCollection, fsId);
460
+ const firestoreData = Syncro.toFirestore(state);
441
461
 
442
462
  return Promise.resolve()
443
- .then(()=> // Set up binding and wait for it to come ready
444
- (settings.method == 'merge' ? 'FirestoreUpdateDoc' : 'FirestoreSetDoc')(
445
- FirestoreDocRef(Syncro.firestore, fsCollection, fsId),
446
- Syncro.toFirestore(state),
447
- )
448
- )
463
+ .then(()=> {
464
+ if (settings.method === 'merge') {
465
+ return FirestoreUpdateDoc(docRef, firestoreData);
466
+ } else { // method === 'set'
467
+ return FirestoreSetDoc(docRef, firestoreData); // Default set overwrites
468
+ }
469
+ })
449
470
  .then(()=> state)
450
471
  }
451
472
 
@@ -458,7 +479,7 @@ export default class Syncro {
458
479
  * @param {Number} [options.retries=3] Number of times to retry if a mounted Syncro fails its sanity checks
459
480
  * @returns {Promise<Syncro>} A promise which resolves as this syncro instance when completed
460
481
  */
461
- mount(options) {
482
+ mount(options?: any): Promise<Syncro> {
462
483
  let settings = {
463
484
  initialState: null,
464
485
  retries: 5,
@@ -467,68 +488,68 @@ export default class Syncro {
467
488
  };
468
489
 
469
490
  let {fsCollection, fsId, entity} = Syncro.pathSplit(this.path);
470
- let reactive; // Eventual response from reactive() with the intitial value
471
- let doc; // Eventual Firebase document
491
+ let reactive: ReactiveWrapper; // Eventual response from reactive() with the intitial value
492
+ let doc: any; // Eventual Firebase document
472
493
 
473
494
  return PromiseRetry(
474
- ()=> Promise.resolve()
475
- .then(()=> this.setHeartbeat(false)) // Disable any existing heartbeat - this only really applies if we're changing path for some reason
476
- .then(async ()=> { // Set up binding and wait for it to come ready
477
- this.docRef = FirestoreDocRef(Syncro.firestore, fsCollection, fsId);
478
-
479
- // Initalize state
480
- let initialState = await this.getFirestoreState();
481
-
482
- // Construct a reactive component
483
- reactive = this.getReactive(initialState);
484
- if (!reactive.doc || !reactive.setState || !reactive.getState || !reactive.watch) throw new Error('Syncro.getReactive() requires a returned `doc`, `setState()`, `getState()` + `watch()`');
485
- this.value = doc = reactive.doc;
486
-
487
- this.debug('Initial state', {doc});
488
-
489
- // Subscribe to remote updates
490
- this._destroyActions.push( // Add the unsubscribe handle to the list of destroyAction promises we call on `destroy()`
491
- FirestoreOnSnapshot(this.docRef, snapshot => {
492
- let snapshotData = Syncro.fromFirestore(snapshot.data());
493
- this.debug('Incoming snapshot', {snapshotData});
494
- reactive.setState(snapshotData);
495
- })
496
- );
497
- })
498
- .then(()=> { // Optionally create the doc if it has no content
499
- if (!isEmpty(doc)) { // Doc already has content - skip
500
- return;
501
- } else if (settings.initialState) { // Provided an intiailState - use that instead of the entities own method
502
- this.debug('Populate initial Syncro state (from provided initialState)');
503
- return this.setFirestoreState(settings.initialState, {method: 'set'});
504
- } else {
505
- this.debug(`Populate initial Syncro state (from "${entity}" Syncro worker)`);
506
-
507
- return fetch(`${this.config.syncroRegistryUrl}/${this.path}`)
508
- .then(response => response.ok || Promise.reject(`Failed to check Syncro "${fsCollection}::${fsId}" status - ${response.statusText}`))
495
+ async (): Promise<Syncro> => { // Added async here for await
496
+ await this.setHeartbeat(false); // Disable any existing heartbeat - this only really applies if we're changing path for some reason
497
+
498
+ // Set up binding and wait for it to come ready
499
+ this.docRef = FirestoreDocRef(Syncro.firestore, fsCollection, fsId);
500
+
501
+ // Initalize state
502
+ let initialState = await this.getFirestoreState();
503
+
504
+ // Construct a reactive component
505
+ reactive = this.getReactive(initialState);
506
+ if (!reactive.doc || !reactive.setState || !reactive.getState || !reactive.watch) throw new Error('Syncro.getReactive() requires a returned `doc`, `setState()`, `getState()` + `watch()`');
507
+ this.value = doc = reactive.doc;
508
+
509
+ this.debug('Initial state', {doc});
510
+
511
+ // Subscribe to remote updates
512
+ const unsubscribe: Unsubscribe = FirestoreOnSnapshot(this.docRef, snapshot => {
513
+ let snapshotData = Syncro.fromFirestore(snapshot.data());
514
+ this.debug('Incoming snapshot', {snapshotData});
515
+ reactive.setState(snapshotData);
516
+ });
517
+ this._destroyActions.push(unsubscribe); // Add the unsubscribe handle to the list of destroyAction promises we call on `destroy()`
518
+
519
+ // Optionally create the doc if it has no content
520
+ if (!isEmpty(doc)) { // Doc already has content - skip
521
+ // Do nothing
522
+ } else if (settings.initialState) { // Provided an intiailState - use that instead of the entities own method
523
+ this.debug('Populate initial Syncro state (from provided initialState)');
524
+ await this.setFirestoreState(settings.initialState, {method: 'set'});
525
+ } else {
526
+ this.debug(`Populate initial Syncro state (from "${entity}" Syncro worker)`);
527
+ const response = await fetch(`${this.config.syncroRegistryUrl}/${this.path}`);
528
+ if (!response.ok) {
529
+ throw new Error(`Failed to check Syncro "${fsCollection}::${fsId}" status - ${response.statusText}`);
509
530
  }
510
- })
511
- .then(()=> { // Setup local state watcher
512
- reactive.watch(throttle(newState => {
513
- this.debug('Local change', {newState});
514
- this.markDirty();
515
- this.setFirestoreState(newState, {method: 'merge'});
516
- }, this.throttle));
517
- })
518
- .then(()=> this.setHeartbeat(true, {
531
+ // Assuming the fetch populates the syncro state server-side, no local state set needed here
532
+ }
533
+
534
+ // Setup local state watcher
535
+ reactive.watch(throttle((newState: any) => {
536
+ this.debug('Local change', {newState});
537
+ this.markDirty();
538
+ this.setFirestoreState(newState, {method: 'merge'});
539
+ }, this.throttle));
540
+
541
+ await this.setHeartbeat(true, {
519
542
  immediate: true,
520
- }))
521
- .then(()=> this)
522
- .catch(async (e) => {
523
- await this.destroy();
524
- throw e;
525
- }),
543
+ });
544
+
545
+ return this;
546
+ },
526
547
  { // PromiseRetry / p-retry options
527
548
  retries: settings.retries,
528
549
  minTimeout: settings.retryMinTime,
529
550
  randomize: true,
530
551
  factor: 3,
531
- onFailedAttempt: async (e) => {
552
+ onFailedAttempt: async (e: any) => {
532
553
  this.debugError(`[Attempt ${e.attemptNumber}/${e.attemptNumber + e.retriesLeft - 1}] to mount syncro`, e);
533
554
  await this.destroy(); // Ensure cleanup on failed attempt before retry
534
555
  },
@@ -536,7 +557,6 @@ export default class Syncro {
536
557
  );
537
558
  }
538
559
 
539
-
540
560
  /**
541
561
  * Merge a single or multiple values into a Syncro data object
542
562
  * NOTE: Default behaviour is to flush (if any changes apply), use direct object mutation or disable with `flush:false` to disable
@@ -552,9 +572,13 @@ export default class Syncro {
552
572
  *
553
573
  * @returns {Promise<Syncro>} A promise which resolves with this Syncro instance on completion
554
574
  */
555
- async set(key, value, options) {
575
+ async set(
576
+ key: string | object,
577
+ value: any,
578
+ options: { delta?: boolean, flush?: boolean, forceFlush?: boolean, flushDestroy?: boolean }
579
+ ) {
556
580
  // Argument mangling - [key, value, settings] -> changes{}, settings {{{
557
- let changes;
581
+ let changes: any;
558
582
  if (typeof key == 'string') { // Called as (key:String, value:*, options?:Object)
559
583
  changes[key] = value;
560
584
  } else if (typeof key == 'object') { // Called as (changes:Object, options?:Object)
@@ -597,7 +621,6 @@ export default class Syncro {
597
621
  return this;
598
622
  }
599
623
 
600
-
601
624
  /**
602
625
  * Schedule Syncro heartbeats
603
626
  * This populates the `sync` presence meta-information
@@ -607,7 +630,7 @@ export default class Syncro {
607
630
  * @param {Object} [options] Additional options to mutate behaviour
608
631
  * @param {Boolean} [options.immediate=false] Fire a heartbeat as soon as this function is called, this is only really useful on mount
609
632
  */
610
- setHeartbeat(enable = true, options) {
633
+ setHeartbeat(enable: boolean = true, options?: any): Promise<void> | void { // Return type adjusted
611
634
  let settings = {
612
635
  immediate: true,
613
636
  ...options,
@@ -617,15 +640,17 @@ export default class Syncro {
617
640
  clearTimeout(this._heartbeatTimer);
618
641
 
619
642
  if (enable) {
620
- this._heartbeatTimer = setTimeout(async ()=> {
643
+ const heartbeatAction = async () => {
621
644
  // Perform the heartbeat
622
645
  await this.heartbeat();
623
646
 
624
647
  // If we're enabled - schedule the next heartbeat timer
625
- if (enable) this.setHeartbeat(true);
626
- }, this.config.heartbeatInterval);
648
+ if (enable) this.setHeartbeat(true); // Reschedule
649
+ };
627
650
 
628
- if (settings.immediate) this.heartbeat();
651
+ this._heartbeatTimer = setTimeout(heartbeatAction, this.config.heartbeatInterval);
652
+
653
+ if (settings.immediate) return this.heartbeat(); // Return the promise from immediate heartbeat
629
654
  }
630
655
  }
631
656
 
@@ -636,24 +661,29 @@ export default class Syncro {
636
661
  *
637
662
  * @returns {Promise} A promise which resolves when the operation has completed
638
663
  */
639
- async heartbeat() {
664
+ async heartbeat(): Promise<void> {
640
665
  this.debug('heartbeat!');
641
666
 
642
- await fetch(`${this.config.syncroRegistryUrl}/${this.path}/heartbeat`, {
643
- method: 'post',
644
- headers: {
645
- 'Content-Type': 'application/json'
646
- },
647
- body: JSON.stringify({
648
- session: Syncro.session,
649
- ...(this.isDirty && {dirty: true}),
650
- }),
651
- })
652
- .then(response => response.ok
653
- ? null
654
- : console.warn(this.path, `Heartbeat failed - ${response.statusText}`, {response})
655
- )
656
- .then(()=> this.isDirty = false) // Reset the dirty flag if it is set
667
+ try {
668
+ const response = await fetch(`${this.config.syncroRegistryUrl}/${this.path}/heartbeat`, {
669
+ method: 'post',
670
+ headers: {
671
+ 'Content-Type': 'application/json'
672
+ },
673
+ body: JSON.stringify({
674
+ session: Syncro.session,
675
+ ...(this.isDirty && {dirty: true}),
676
+ }),
677
+ });
678
+
679
+ if (!response.ok) {
680
+ console.warn(this.path, `Heartbeat failed - ${response.statusText}`, {response});
681
+ }
682
+ this.isDirty = false; // Reset the dirty flag if it is set
683
+ } catch (error) {
684
+ console.warn(this.path, 'Heartbeat fetch error', error);
685
+ // Decide if isDirty should be reset on network error
686
+ }
657
687
  }
658
688
 
659
689
 
@@ -666,17 +696,20 @@ export default class Syncro {
666
696
  *
667
697
  * @returns {Promise} A promise which resolves when the operation has completed
668
698
  */
669
- setFirestoreState(state, options) {
699
+ setFirestoreState(state: any, options?: { method?: 'merge' | 'set' }): Promise<void> {
670
700
  let settings = {
671
701
  method: 'merge',
672
702
  ...options,
673
703
  };
674
704
  if (!this.docRef) throw new Error('mount() must be called before setting Firestore state');
675
705
 
676
- return (settings.method == 'set' ? FirestoreSetDoc : FirestoreUpdateDoc)(
677
- this.docRef,
678
- Syncro.toFirestore(state),
679
- )
706
+ const firestoreData = Syncro.toFirestore(state);
707
+
708
+ if (settings.method === 'merge') {
709
+ return FirestoreUpdateDoc(this.docRef, firestoreData);
710
+ } else { // method === 'set'
711
+ return FirestoreSetDoc(this.docRef, firestoreData);
712
+ }
680
713
  }
681
714
 
682
715
 
@@ -686,7 +719,7 @@ export default class Syncro {
686
719
  *
687
720
  * @returns {Promise<Object>} A promise which resolves to the Firestore state
688
721
  */
689
- getFirestoreState() {
722
+ getFirestoreState(): Promise<any> {
690
723
  if (!this.docRef) throw new Error('mount() must be called before getting Firestore state');
691
724
 
692
725
  return FirestoreGetDoc(this.docRef)
@@ -700,7 +733,7 @@ export default class Syncro {
700
733
  * @see isDirty
701
734
  * @returns {Syncro} This chainable Syncro instance
702
735
  */
703
- markDirty() {
736
+ markDirty(): this {
704
737
  this.isDirty = true;
705
738
  return this;
706
739
  }
@@ -715,7 +748,7 @@ export default class Syncro {
715
748
  *
716
749
  * @returns {Promise} A promise which resolves when the operation has completed
717
750
  */
718
- flush(options) {
751
+ flush(options?: any): Promise<void | null> {
719
752
  let settings = {
720
753
  destroy: false,
721
754
  ...options,
@@ -725,16 +758,16 @@ export default class Syncro {
725
758
  .then(response => response.ok
726
759
  ? null
727
760
  : Promise.reject(response.statusText || 'An error occured')
728
- )
761
+ );
729
762
  }
730
763
 
731
764
 
732
765
  /**
733
766
  * Timer handle for heartbeats
734
767
  *
735
- * @type {Handle}
768
+ * @type {any}
736
769
  */
737
- _heartbeatTimer;
770
+ _heartbeatTimer: any; // Using any for simplicity with NodeJS.Timeout / number
738
771
  }
739
772
 
740
773
 
@@ -746,7 +779,7 @@ export default class Syncro {
746
779
  *
747
780
  * @returns {*} The current branch conotents
748
781
  */
749
- export function randomBranch(depth = 0) {
782
+ export function randomBranch(depth: number = 0): any {
750
783
  let dice = // Roll a dice to pick the content
751
784
  depth == 0 ? 10 // first roll is always '10'
752
785
  : random(0, 11 - depth, false); // Subsequent rolls bias downwards based on depth (to avoid recursion)
@@ -756,12 +789,12 @@ export function randomBranch(depth = 0) {
756
789
  : dice == 1 ? true
757
790
  : dice == 2 ? random(1, 10_000)
758
791
  : dice == 3 ? (new Date(random(1_000_000_000_000, 1_777_777_777_777))).toISOString()
759
- : dice == 5 ? Array.from({length: random(1, 10)}, ()=> random(1, 10))
792
+ : dice == 5 ? Array.from({length: random(1, 10)}, (): number => random(1, 10)) // Added return type hint
760
793
  : dice == 6 ? null
761
- : dice < 8 ? Array.from({length: random(1, 10)}, ()=> randomBranch(depth+1))
794
+ : dice < 8 ? Array.from({length: random(1, 10)}, (): any => randomBranch(depth+1)) // Added return type hint
762
795
  : Object.fromEntries(
763
796
  Array.from({length: random(1, 5)})
764
- .map((v, k) => [
797
+ .map((v, k): [string, any] => [ // Added return type hint
765
798
  sample(['foo', 'bar', 'baz', 'quz', 'flarp', 'corge', 'grault', 'garply', 'waldo', 'fred', 'plugh', 'thud'])
766
799
  + `_${k}`,
767
800
  randomBranch(depth+1),
@@ -778,14 +811,14 @@ export function randomBranch(depth = 0) {
778
811
  const marshalFlattenArrays = {
779
812
  id: `~array`,
780
813
  recursive: true,
781
- test: v => Array.isArray(v),
782
- serialize: v => ({_: '~array', ...v}),
783
- deserialize: v => {
784
- let arr = Array.from({length: Object.keys(v).length - 1});
814
+ test: (v: any): boolean => Array.isArray(v),
815
+ serialize: (v: any): any => ({_: '~array', ...v}),
816
+ deserialize: (v: any): any[] => {
817
+ let arr: any[] = Array.from({length: Object.keys(v).length - 1});
785
818
 
786
819
  Object.entries(v)
787
820
  .filter(([k]) => k !== '_')
788
- .forEach(([k, w]) => arr[+k] = w);
821
+ .forEach(([k, val]) => arr[+k] = val); // Changed v to val to avoid shadowing
789
822
 
790
823
  return arr;
791
824
  },