@iebh/tera-fy 2.0.1 → 2.0.3

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,
@@ -301,12 +306,11 @@ export default class Syncro {
301
306
  let {fsCollection, fsId} = Syncro.pathSplit(path);
302
307
 
303
308
  return Promise.resolve()
304
- .then(async ()=> { // Set up binding and wait for it to come ready
305
- let docRef = FirestoreDocRef(Syncro.firestore, fsCollection, fsId);
306
- return FirestoreGetDoc(docRef);
307
- })
309
+ .then(async ()=> FirestoreGetDoc( // Set up binding and wait for it to come ready
310
+ FirestoreDocRef(Syncro.firestore, fsCollection, fsId)
311
+ ))
308
312
  .then(doc => doc
309
- ? Syncro.fromFirestore(snapshot.data())
313
+ ? Syncro.fromFirestore(doc.data())
310
314
  : null
311
315
  )
312
316
  }
@@ -419,9 +423,8 @@ export default class Syncro {
419
423
  .then(()=> syncEntities[entity] || Promise.reject(`Unknown Sync entity "${entity}"`))
420
424
  .then(()=> syncEntities[entity].initState({
421
425
  supabase: Syncro.supabase,
422
- entity,
423
- id,
424
- relation,
426
+ fsCollection, fsId,
427
+ entity, id, relation,
425
428
  }))
426
429
  .then(state => FirestoreSetDoc(
427
430
  this.docRef,
@@ -437,7 +440,9 @@ export default class Syncro {
437
440
  );
438
441
  }, this.throttle));
439
442
  })
440
- .then(()=> this.setHeartbeat(true))
443
+ .then(()=> this.setHeartbeat(true, {
444
+ immediate: true,
445
+ }))
441
446
  .then(()=> this)
442
447
  }
443
448
 
@@ -447,27 +452,82 @@ export default class Syncro {
447
452
  * This populates the `sync` presence meta-information
448
453
  *
449
454
  * @param {Boolean} [enable=true] Whether to enable heartbeating
455
+ *
456
+ * @param {Object} [options] Additional options to mutate behaviour
457
+ * @param {Boolean} [options.immediate=false] Fire a heartbeat as soon as this function is called, this is only really useful on mount
450
458
  */
451
- setHeartbeat(enable = true) {
459
+ setHeartbeat(enable = true, options) {
460
+ let settings = {
461
+ immediate: true,
462
+ ...options,
463
+ };
464
+
465
+ // Clear existing heartbeat timer, if there is one
452
466
  clearTimeout(this._heartbeatTimer);
453
467
 
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
- },
468
+ if (enable) {
469
+ this._heartbeatTimer = setTimeout(async ()=> {
470
+ // Perform the heartbeat
471
+ await this.heartbeat();
472
+
473
+ // If we're enabled - schedule the next heartbeat timer
474
+ enable && this.setHeartbeat(true);
475
+ }, this.heartbeatInterval);
476
+
477
+ if (settings.immediate) this.heartbeat();
478
+ }
479
+ }
480
+
481
+
482
+ /**
483
+ * Perform one heartbeat pulse to the server to indicate presense within this Syncro
484
+ * This function is automatically called by a timer if `setHeartbeat(true)` (the default behaviour)
485
+ *
486
+ * @returns {Promise} A promise which resolves when the operation has completed
487
+ */
488
+ async heartbeat() {
489
+ this.debug('heartbeat!');
490
+ let timestamp = (new Date()).toISOString();
491
+
492
+ let docRef = FirestoreDocRef(Syncro.firestore, 'presence', this.path);
493
+ let doc = await FirestoreGetDoc(docRef);
494
+
495
+ await FirestoreSetDoc(
496
+ docRef,
497
+ {
498
+ latest: timestamp,
499
+ ...(!doc.exists() && { // New doc - populate some base fields
500
+ created: timestamp,
501
+ lastFlush: null,
502
+ }),
503
+ sessions: {
504
+ [Syncro.session]: timestamp,
464
505
  },
465
- {merge: true},
466
- );
506
+ },
507
+ {merge: true},
508
+ );
509
+ }
467
510
 
468
- // If we're enabled - set the next heartbeat timer
469
- this.setHeartbeat(true);
470
- }, this.heartbeatInterval);
511
+
512
+ /**
513
+ * Force the server to flush state
514
+ * This is only really useful for debugging as this happens automatically anyway
515
+ *
516
+ * @param {Object} [options] Additional options to mutate behaviour
517
+ * @param {Boolean} [options.destroy=false] Instruct the server to also dispose of the Syncro state
518
+ *
519
+ * @returns {Promise} A promise which resolves when the operation has completed
520
+ */
521
+ flush(options) {
522
+ let settings = {
523
+ destroy: false,
524
+ ...options,
525
+ };
526
+
527
+ return fetch(`https://tera-tools.com/api/sync/${this.path}/flush` + (settings.destroy ? '?destroy=1' : ''), {
528
+ method: 'post',
529
+ })
530
+ .then(response => response.ok ? null : Promise.reject(response.statusText || 'An error occured'))
471
531
  }
472
532
 
473
533
 
@@ -480,6 +540,39 @@ export default class Syncro {
480
540
  }
481
541
 
482
542
 
543
+ /**
544
+ * Build a chaotic random tree structure based on dice rolls
545
+ * This funciton is mainly used for sync testing
546
+ *
547
+ * @param {Number} [depth=0] The current depth we are starting at, changes the nature of branches based on probability
548
+ *
549
+ * @returns {*} The current branch conotents
550
+ */
551
+ export function randomBranch(depth = 0) {
552
+ let dice = // Roll a dice to pick the content
553
+ depth == 0 ? 10 // first roll is always '10'
554
+ : random(0, 11 - depth, false); // Subsequent rolls bias downwards based on depth (to avoid recursion)
555
+
556
+ return (
557
+ dice == 0 ? false
558
+ : dice == 1 ? true
559
+ : dice == 2 ? random(1, 10_000)
560
+ : dice == 3 ? (new Date(random(1_000_000_000_000, 1_777_777_777_777))).toISOString()
561
+ : dice == 5 ? Array.from({length: random(1, 10)}, ()=> random(1, 10))
562
+ : dice == 6 ? null
563
+ : dice < 8 ? Array.from({length: random(1, 10)}, ()=> randomBranch(depth+1))
564
+ : Object.fromEntries(
565
+ Array.from({length: random(1, 5)})
566
+ .map((v, k) => [
567
+ sample(['foo', 'bar', 'baz', 'quz', 'flarp', 'corge', 'grault', 'garply', 'waldo', 'fred', 'plugh', 'thud'])
568
+ + `_${k}`,
569
+ randomBranch(depth+1),
570
+ ])
571
+ )
572
+ )
573
+ }
574
+
575
+
483
576
  /**
484
577
  * Entities we support syncro paths on, each needs to correspond with a Firebase/Firestore collection name
485
578
  *
@@ -519,12 +612,25 @@ export const syncEntities = {
519
612
  .eq('project', id)
520
613
  .eq('name', relation)
521
614
  )
522
- .then(rows => rows.length == 1 ? rows[0] : Promise.reject(`Syncro project ("${id}") namespace ("${relation}") not found`))
615
+ .then(rows => rows.length == 1
616
+ ? rows[0]
617
+ : Syncro.wrapSupabase(supabase.from('project_namespaces') // Doesn't exist - create it
618
+ .insert({
619
+ project: id,
620
+ name: relation,
621
+ data: {},
622
+ })
623
+ .select('data')
624
+ )
625
+ )
523
626
  .then(item => item.data);
524
627
  },
525
628
  flushState({supabase, state, id, relation}) {
526
629
  return Syncro.wrapSupabase(supabase.from('project_namespaces')
527
- .update(state)
630
+ .update({
631
+ edited_at: new Date(),
632
+ data: state,
633
+ })
528
634
  .eq('project', id)
529
635
  .eq('name', relation)
530
636
  )
@@ -758,7 +758,7 @@ export default class TeraFyServer {
758
758
  getNamespace(name) {
759
759
  if (!/^[\w-]+$/.test(name)) throw new Error('Namespaces must be alphanumeric + hyphens + underscores');
760
760
 
761
- return this.$syncro.getSnapshot(`project_namespaces::${this.$projects.active.id}::${name}`);
761
+ return app.service('$sync').getSnapshot(`project_namespaces::${app.service('$projects').active.id}::${name}`);
762
762
  }
763
763
 
764
764
 
@@ -777,7 +777,7 @@ export default class TeraFyServer {
777
777
  if (!/^[\w--]+$/.test(name)) throw new Error('Namespaces must be alphanumeric + hyphens + underscores');
778
778
  if (typeof state != 'object') throw new Error('State must be an object');
779
779
 
780
- return this.$syncro.setSnapshot(`project_namespaces::${this.$projects.active.id}}::${name}`, state, {
780
+ return app.service('$sync').setSnapshot(`project_namespaces::${app.service('$projects').active.id}}::${name}`, state, {
781
781
  method: options.method,
782
782
  });
783
783
  }
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.3",
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": {