@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/CHANGELOG.md +28 -0
- package/dist/plugin.vue2.es2019.js +15 -15
- package/dist/terafy.es2019.js +1 -1
- package/dist/terafy.js +1 -1
- package/lib/syncro.js +134 -28
- package/lib/terafy.server.js +2 -2
- package/package.json +2 -2
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_${
|
|
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 ()=>
|
|
305
|
-
|
|
306
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
455
|
-
this.
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
466
|
-
|
|
506
|
+
},
|
|
507
|
+
{merge: true},
|
|
508
|
+
);
|
|
509
|
+
}
|
|
467
510
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
|
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(
|
|
630
|
+
.update({
|
|
631
|
+
edited_at: new Date(),
|
|
632
|
+
data: state,
|
|
633
|
+
})
|
|
528
634
|
.eq('project', id)
|
|
529
635
|
.eq('name', relation)
|
|
530
636
|
)
|
package/lib/terafy.server.js
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
|
83
|
+
"nanoid": "^5.1.0",
|
|
84
84
|
"release-it": "^17.6.0"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|