@iebh/tera-fy 2.0.1 → 2.0.2

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.
package/lib/syncro.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  isEmpty,
3
3
  cloneDeep,
4
+ random,
5
+ sample,
4
6
  throttle,
5
7
  } from 'lodash-es';
6
8
  import {
@@ -11,6 +13,7 @@ import {
11
13
  updateDoc as FirestoreUpdateDoc,
12
14
  } from 'firebase/firestore';
13
15
  import marshal from '@momsfriendlydevco/marshal';
16
+ import {nanoid} from 'nanoid';
14
17
 
15
18
 
16
19
  /**
@@ -114,7 +117,7 @@ export default class Syncro {
114
117
  Object.assign(this, options);
115
118
 
116
119
  if (!Syncro.session) // Assign a random session ID if we don't already have one
117
- Syncro.session = `syncro_${crypto.randomUUID()}`;
120
+ Syncro.session = `syncro_${nanoid()}`;
118
121
  }
119
122
 
120
123
 
@@ -221,6 +224,7 @@ export default class Syncro {
221
224
  */
222
225
  static toFirestore(snapshot = {}) {
223
226
  return marshal.serialize(snapshot, {
227
+ circular: false,
224
228
  clone: true, // Clone away from the original Vue Reactive so we dont mangle it while traversing
225
229
  modules: [
226
230
  marshalFlattenArrays,
@@ -242,6 +246,7 @@ export default class Syncro {
242
246
  */
243
247
  static fromFirestore(snapshot = {}) {
244
248
  return marshal.deserialize(snapshot, {
249
+ circular: false,
245
250
  clone: true, // Clone away from original so we don't trigger a loop within Firebase
246
251
  modules: [
247
252
  marshalFlattenArrays,
@@ -419,9 +424,8 @@ export default class Syncro {
419
424
  .then(()=> syncEntities[entity] || Promise.reject(`Unknown Sync entity "${entity}"`))
420
425
  .then(()=> syncEntities[entity].initState({
421
426
  supabase: Syncro.supabase,
422
- entity,
423
- id,
424
- relation,
427
+ fsCollection, fsId,
428
+ entity, id, relation,
425
429
  }))
426
430
  .then(state => FirestoreSetDoc(
427
431
  this.docRef,
@@ -437,7 +441,9 @@ export default class Syncro {
437
441
  );
438
442
  }, this.throttle));
439
443
  })
440
- .then(()=> this.setHeartbeat(true))
444
+ .then(()=> this.setHeartbeat(true, {
445
+ immediate: true,
446
+ }))
441
447
  .then(()=> this)
442
448
  }
443
449
 
@@ -447,27 +453,82 @@ export default class Syncro {
447
453
  * This populates the `sync` presence meta-information
448
454
  *
449
455
  * @param {Boolean} [enable=true] Whether to enable heartbeating
456
+ *
457
+ * @param {Object} [options] Additional options to mutate behaviour
458
+ * @param {Boolean} [options.immediate=false] Fire a heartbeat as soon as this function is called, this is only really useful on mount
450
459
  */
451
- setHeartbeat(enable = true) {
460
+ setHeartbeat(enable = true, options) {
461
+ let settings = {
462
+ immediate: true,
463
+ ...options,
464
+ };
465
+
466
+ // Clear existing heartbeat timer, if there is one
452
467
  clearTimeout(this._heartbeatTimer);
453
468
 
454
- this._heartbeatTimer = enable && setTimeout(async ()=> {
455
- this.debug('heartbeat!');
456
- let timestamp = (new Date()).toISOString();
457
- await FirestoreSetDoc(
458
- FirestoreDocRef(Syncro.firestore, 'presence', this.path),
459
- {
460
- latest: timestamp,
461
- sessions: {
462
- [this.session]: timestamp,
463
- },
469
+ if (enable) {
470
+ this._heartbeatTimer = setTimeout(async ()=> {
471
+ // Perform the heartbeat
472
+ await this.heartbeat();
473
+
474
+ // If we're enabled - schedule the next heartbeat timer
475
+ enable && this.setHeartbeat(true);
476
+ }, this.heartbeatInterval);
477
+
478
+ if (settings.immediate) this.heartbeat();
479
+ }
480
+ }
481
+
482
+
483
+ /**
484
+ * Perform one heartbeat pulse to the server to indicate presense within this Syncro
485
+ * This function is automatically called by a timer if `setHeartbeat(true)` (the default behaviour)
486
+ *
487
+ * @returns {Promise} A promise which resolves when the operation has completed
488
+ */
489
+ async heartbeat() {
490
+ this.debug('heartbeat!');
491
+ let timestamp = (new Date()).toISOString();
492
+
493
+ let docRef = FirestoreDocRef(Syncro.firestore, 'presence', this.path);
494
+ let doc = await FirestoreGetDoc(docRef);
495
+
496
+ await FirestoreSetDoc(
497
+ docRef,
498
+ {
499
+ latest: timestamp,
500
+ ...(!doc.exists() && { // New doc - populate some base fields
501
+ created: timestamp,
502
+ lastFlush: null,
503
+ }),
504
+ sessions: {
505
+ [Syncro.session]: timestamp,
464
506
  },
465
- {merge: true},
466
- );
507
+ },
508
+ {merge: true},
509
+ );
510
+ }
511
+
467
512
 
468
- // If we're enabled - set the next heartbeat timer
469
- this.setHeartbeat(true);
470
- }, this.heartbeatInterval);
513
+ /**
514
+ * Force the server to flush state
515
+ * This is only really useful for debugging as this happens automatically anyway
516
+ *
517
+ * @param {Object} [options] Additional options to mutate behaviour
518
+ * @param {Boolean} [options.destroy=false] Instruct the server to also dispose of the Syncro state
519
+ *
520
+ * @returns {Promise} A promise which resolves when the operation has completed
521
+ */
522
+ flush(options) {
523
+ let settings = {
524
+ destroy: false,
525
+ ...options,
526
+ };
527
+
528
+ return fetch(`https://tera-tools.com/api/sync/${this.path}/flush` + (settings.destroy ? '?destroy=1' : ''), {
529
+ method: 'post',
530
+ })
531
+ .then(response => response.ok ? null : Promise.reject(response.statusText || 'An error occured'))
471
532
  }
472
533
 
473
534
 
@@ -480,6 +541,39 @@ export default class Syncro {
480
541
  }
481
542
 
482
543
 
544
+ /**
545
+ * Build a chaotic random tree structure based on dice rolls
546
+ * This funciton is mainly used for sync testing
547
+ *
548
+ * @param {Number} [depth=0] The current depth we are starting at, changes the nature of branches based on probability
549
+ *
550
+ * @returns {*} The current branch conotents
551
+ */
552
+ export function randomBranch(depth = 0) {
553
+ let dice = // Roll a dice to pick the content
554
+ depth == 0 ? 10 // first roll is always '10'
555
+ : random(0, 11 - depth, false); // Subsequent rolls bias downwards based on depth (to avoid recursion)
556
+
557
+ return (
558
+ dice == 0 ? false
559
+ : dice == 1 ? true
560
+ : dice == 2 ? random(1, 10_000)
561
+ : dice == 3 ? (new Date(random(1_000_000_000_000, 1_777_777_777_777))).toISOString()
562
+ : dice == 5 ? Array.from({length: random(1, 10)}, ()=> random(1, 10))
563
+ : dice == 6 ? null
564
+ : dice < 8 ? Array.from({length: random(1, 10)}, ()=> randomBranch(depth+1))
565
+ : Object.fromEntries(
566
+ Array.from({length: random(1, 5)})
567
+ .map((v, k) => [
568
+ sample(['foo', 'bar', 'baz', 'quz', 'flarp', 'corge', 'grault', 'garply', 'waldo', 'fred', 'plugh', 'thud'])
569
+ + `_${k}`,
570
+ randomBranch(depth+1),
571
+ ])
572
+ )
573
+ )
574
+ }
575
+
576
+
483
577
  /**
484
578
  * Entities we support syncro paths on, each needs to correspond with a Firebase/Firestore collection name
485
579
  *
@@ -519,12 +613,25 @@ export const syncEntities = {
519
613
  .eq('project', id)
520
614
  .eq('name', relation)
521
615
  )
522
- .then(rows => rows.length == 1 ? rows[0] : Promise.reject(`Syncro project ("${id}") namespace ("${relation}") not found`))
616
+ .then(rows => rows.length == 1
617
+ ? rows[0]
618
+ : Syncro.wrapSupabase(supabase.from('project_namespaces') // Doesn't exist - create it
619
+ .insert({
620
+ project: id,
621
+ name: relation,
622
+ data: {},
623
+ })
624
+ .select('data')
625
+ )
626
+ )
523
627
  .then(item => item.data);
524
628
  },
525
629
  flushState({supabase, state, id, relation}) {
526
630
  return Syncro.wrapSupabase(supabase.from('project_namespaces')
527
- .update(state)
631
+ .update({
632
+ edited_at: new Date(),
633
+ data: state,
634
+ })
528
635
  .eq('project', id)
529
636
  .eq('name', relation)
530
637
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iebh/tera-fy",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "TERA website worker",
5
5
  "scripts": {
6
6
  "dev": "esbuild --platform=browser --format=esm --bundle lib/terafy.client.js --outfile=dist/terafy.js --minify --serve --servedir=.",
@@ -80,7 +80,7 @@
80
80
  "just-diff": "^6.0.2",
81
81
  "lodash-es": "^4.17.21",
82
82
  "mitt": "^3.0.1",
83
- "nanoid": "^5.0.7",
83
+ "nanoid": "^5.1.0",
84
84
  "release-it": "^17.6.0"
85
85
  },
86
86
  "devDependencies": {