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