@iebh/tera-fy 2.0.5 → 2.0.7
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 +57 -0
- package/api.md +585 -686
- package/dist/plugin.vue2.es2019.js +19 -48
- package/dist/terafy.es2019.js +3 -3
- package/dist/terafy.js +3 -3
- package/lib/projectFile.js +11 -1
- package/lib/syncro/entities.js +184 -0
- package/lib/syncro/keyed.js +295 -0
- package/lib/{syncro.js → syncro/syncro.js} +154 -206
- package/lib/terafy.client.js +4 -116
- package/lib/terafy.server.js +6 -77
- package/package.json +16 -15
- package/plugins/firebase.js +1 -1
- package/plugins/vue2.js +1 -141
- package/plugins/vue3.js +1 -1
|
@@ -17,7 +17,9 @@ import {nanoid} from 'nanoid';
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
+
* @class Syncro
|
|
20
21
|
* TERA Isomorphic Syncro class
|
|
22
|
+
* Slurp an entity from Supabase and hold it in Firebase/Firestore as a "floating" entity which periodically gets flushed back to Supabase and eventually completely cleaned up
|
|
21
23
|
* This class tries to be as independent as possible to help with adapting it to various front-end TERA-fy plugin frameworks
|
|
22
24
|
*/
|
|
23
25
|
export default class Syncro {
|
|
@@ -89,11 +91,29 @@ export default class Syncro {
|
|
|
89
91
|
|
|
90
92
|
|
|
91
93
|
/**
|
|
92
|
-
*
|
|
94
|
+
* Various Misc config for the Syncro instance
|
|
93
95
|
*
|
|
94
|
-
* @type {
|
|
96
|
+
* @type {Object}
|
|
97
|
+
* @property {Number} heartbeatinterval Time in milliseconds between heartbeat beacons
|
|
98
|
+
* @property {String} syncWorkerUrl The prefix Sync worker URL
|
|
99
|
+
* @property {Object} context Additional named parameters to pass to callbacks like initState
|
|
95
100
|
*/
|
|
96
|
-
|
|
101
|
+
config = {
|
|
102
|
+
heartbeatInterval: 50_000, //~= 50s
|
|
103
|
+
syncWorkerUrl: 'https://tera-tools.com/api/sync',
|
|
104
|
+
context: {},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Whether the next heartbeat should be marked as 'dirty'
|
|
110
|
+
* This indicates that at least one change has occured since the last hearbeat and the server should perform a flush (but not a clean)
|
|
111
|
+
* This flag is only transmitted once in the next heartbeat before being reset
|
|
112
|
+
*
|
|
113
|
+
* @see markDirty()
|
|
114
|
+
* @type {Boolean}
|
|
115
|
+
*/
|
|
116
|
+
isDirty = false;
|
|
97
117
|
|
|
98
118
|
|
|
99
119
|
/**
|
|
@@ -103,18 +123,24 @@ export default class Syncro {
|
|
|
103
123
|
*
|
|
104
124
|
* @param {*...} [msg] The message to output
|
|
105
125
|
*/
|
|
106
|
-
debug(...msg) {}
|
|
126
|
+
debug(...msg) {} // eslint-disable-line no-unused-vars
|
|
107
127
|
|
|
108
128
|
|
|
109
129
|
/**
|
|
110
130
|
* Instance constructor
|
|
111
131
|
*
|
|
112
132
|
* @param {String} path Mount path for the Syncro. Should be in the form `${ENTITY}::${ID}(::${RELATION})?`
|
|
113
|
-
* @param {Object} [options] Additional instance setters (mutates instance directly)
|
|
133
|
+
* @param {Object} [options] Additional instance setters (mutates instance directly), note that the `config` subkey is merged with the existing config rather than assigned
|
|
114
134
|
*/
|
|
115
135
|
constructor(path, options) {
|
|
116
136
|
this.path = path;
|
|
117
|
-
Object.assign(this,
|
|
137
|
+
Object.assign(this, {
|
|
138
|
+
...options,
|
|
139
|
+
config: {
|
|
140
|
+
...this.config,
|
|
141
|
+
...options?.config,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
118
144
|
|
|
119
145
|
if (!Syncro.session) // Assign a random session ID if we don't already have one
|
|
120
146
|
Syncro.session = `syncro_${nanoid()}`;
|
|
@@ -170,7 +196,7 @@ export default class Syncro {
|
|
|
170
196
|
getState() {
|
|
171
197
|
return cloneDeep(doc);
|
|
172
198
|
},
|
|
173
|
-
watch(cb) {
|
|
199
|
+
watch(cb) { // eslint-disable-line no-unused-vars
|
|
174
200
|
// Stub
|
|
175
201
|
},
|
|
176
202
|
};
|
|
@@ -186,6 +212,8 @@ export default class Syncro {
|
|
|
186
212
|
* INPUT: `widgets::UUID::thing` -> `{entity:'widgets', id:UUID, relation:'thing'}`
|
|
187
213
|
*
|
|
188
214
|
* @param {String} path The input session path of the form `${ENTITY}::${ID}`
|
|
215
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
216
|
+
* @param {Boolean} [options.allowAsterisk=false] Whether to allow the meta asterisk character when recognising paths, this is used by the SyncroKeyed class
|
|
189
217
|
*
|
|
190
218
|
* @returns {Object} An object composed of the session path components
|
|
191
219
|
* @property {String} fbEntity The top level Firebase collection to store within
|
|
@@ -194,8 +222,27 @@ export default class Syncro {
|
|
|
194
222
|
* @property {String} id A valid UUID ID
|
|
195
223
|
* @property {String} [relation] A string representing a sub-relationship. Usually a short string alias
|
|
196
224
|
*/
|
|
197
|
-
static pathSplit(path) {
|
|
198
|
-
let
|
|
225
|
+
static pathSplit(path, options) {
|
|
226
|
+
let settings = {
|
|
227
|
+
allowAsterisk: false,
|
|
228
|
+
...options,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
let pathMatcher = new RegExp(
|
|
232
|
+
// Compose the patch matching expression - note double escapes for backslashes to avoid encoding as raw string values
|
|
233
|
+
'^'
|
|
234
|
+
+ '(?<entity>\\w+?)' // Any alpha-numeric sequence as the entity name (non-greedy capture)
|
|
235
|
+
+ '::' // Followed by '::'
|
|
236
|
+
+ '(?<id>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})' // Followed by a valid lower-case UUID
|
|
237
|
+
+ '(?:::(?<relation>[' // Followed by an optional ansi relation
|
|
238
|
+
+ '\\w' // Which is any alpha-numeric sequence...
|
|
239
|
+
+ '\\-' // ... Including UUID characters
|
|
240
|
+
+ (settings.allowAsterisk ? '\\*' : '') // ... and (optionally) an asterisk
|
|
241
|
+
+ ']+?))?'
|
|
242
|
+
+ '$'
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
let extracted = { ...pathMatcher.exec(path)?.groups };
|
|
199
246
|
|
|
200
247
|
if (!extracted) throw new Error(`Invalid session path syntax "${path}"`);
|
|
201
248
|
if (!(extracted.entity in syncEntities)) throw new Error(`Unsupported entity "${path}" -> Entity="${extracted.entity}"`);
|
|
@@ -217,8 +264,6 @@ export default class Syncro {
|
|
|
217
264
|
* 1. Arrays are converted to Objects (Firestore cannot store nested arrays)
|
|
218
265
|
* 2. All non-POJO objects (e.g. Dates) to a symetric object
|
|
219
266
|
*
|
|
220
|
-
* @FIXME: Pretty sure we can drop the fromEntities() + key serializer in future
|
|
221
|
-
*
|
|
222
267
|
* @param {Object} snapshot The current state to convert
|
|
223
268
|
* @returns {Object} A Firebase compatible object
|
|
224
269
|
*/
|
|
@@ -239,8 +284,6 @@ export default class Syncro {
|
|
|
239
284
|
* Convert local Firestore compatible object -> local POJO
|
|
240
285
|
* This reverses the mutations listed in `toFirestore()`
|
|
241
286
|
*
|
|
242
|
-
* @FIXME: Pretty sure we can drop the fromEntities() + key serializer in future
|
|
243
|
-
*
|
|
244
287
|
* @param {Object} snapshot The raw Firebase state to convert
|
|
245
288
|
* @returns {Object} A JavaScript POJO representing the converted state
|
|
246
289
|
*/
|
|
@@ -377,11 +420,18 @@ export default class Syncro {
|
|
|
377
420
|
|
|
378
421
|
|
|
379
422
|
/**
|
|
380
|
-
* Mount the remote Firestore document against this
|
|
423
|
+
* Mount the remote Firestore document against this Syncro instance
|
|
381
424
|
*
|
|
382
|
-
* @
|
|
425
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
426
|
+
* @param {Object} [options.initalState] State to use if no state is already loaded, overrides the entities own `initState` function fetcher
|
|
427
|
+
* @returns {Promise<Syncro>} A promise which resolves as this syncro instance when completed
|
|
383
428
|
*/
|
|
384
|
-
mount(
|
|
429
|
+
mount(options) {
|
|
430
|
+
let settings = {
|
|
431
|
+
initialState: null,
|
|
432
|
+
...options,
|
|
433
|
+
};
|
|
434
|
+
|
|
385
435
|
let {fsCollection, fsId, entity, id, relation} = Syncro.pathSplit(this.path);
|
|
386
436
|
let reactive; // Eventual response from reactive() with the intitial value
|
|
387
437
|
let doc; // Eventual Firebase document
|
|
@@ -392,10 +442,7 @@ export default class Syncro {
|
|
|
392
442
|
this.docRef = FirestoreDocRef(Syncro.firestore, fsCollection, fsId);
|
|
393
443
|
|
|
394
444
|
// Initalize state
|
|
395
|
-
let initialState =
|
|
396
|
-
(await FirestoreGetDoc(this.docRef))
|
|
397
|
-
.data()
|
|
398
|
-
);
|
|
445
|
+
let initialState = await this.getFirestoreState();
|
|
399
446
|
|
|
400
447
|
// Construct a reactive component
|
|
401
448
|
reactive = this.getReactive(initialState);
|
|
@@ -414,31 +461,31 @@ export default class Syncro {
|
|
|
414
461
|
);
|
|
415
462
|
})
|
|
416
463
|
.then(()=> { // Optionally create the doc if it has no content
|
|
417
|
-
if (!isEmpty(doc))
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
entity
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
464
|
+
if (!isEmpty(doc)) { // Doc already has content - skip
|
|
465
|
+
return;
|
|
466
|
+
} else if (settings.initialState) { // Provided an intiailState - use that instead of the entities own method
|
|
467
|
+
this.debug('Populate initial Syncro state (from provided initialState)');
|
|
468
|
+
return this.setFirestoreState(settings.initialState, {method: 'set'});
|
|
469
|
+
} else {
|
|
470
|
+
this.debug(`Populate initial Syncro state (from "${entity}" entity)`);
|
|
471
|
+
|
|
472
|
+
// Extract base data + add document and return new hook
|
|
473
|
+
return Promise.resolve()
|
|
474
|
+
.then(()=> syncEntities[entity] || Promise.reject(`Unknown Sync entity "${entity}"`))
|
|
475
|
+
.then(()=> syncEntities[entity].initState({
|
|
476
|
+
supabase: Syncro.supabase,
|
|
477
|
+
fsCollection, fsId,
|
|
478
|
+
entity, id, relation,
|
|
479
|
+
...this.config.context,
|
|
480
|
+
}))
|
|
481
|
+
.then(state => this.setFirestoreState(state, {method: 'set'})) // Send new base state to Firestore
|
|
482
|
+
}
|
|
434
483
|
})
|
|
435
484
|
.then(()=> { // Setup local state watcher
|
|
436
485
|
reactive.watch(throttle(newState => {
|
|
437
486
|
this.debug('Local change', {newState});
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
Syncro.toFirestore(newState),
|
|
441
|
-
);
|
|
487
|
+
this.markDirty();
|
|
488
|
+
this.setFirestoreState(newState, {method: 'merge'});
|
|
442
489
|
}, this.throttle));
|
|
443
490
|
})
|
|
444
491
|
.then(()=> this.setHeartbeat(true, {
|
|
@@ -473,7 +520,7 @@ export default class Syncro {
|
|
|
473
520
|
|
|
474
521
|
// If we're enabled - schedule the next heartbeat timer
|
|
475
522
|
enable && this.setHeartbeat(true);
|
|
476
|
-
}, this.heartbeatInterval);
|
|
523
|
+
}, this.config.heartbeatInterval);
|
|
477
524
|
|
|
478
525
|
if (settings.immediate) this.heartbeat();
|
|
479
526
|
}
|
|
@@ -488,25 +535,71 @@ export default class Syncro {
|
|
|
488
535
|
*/
|
|
489
536
|
async heartbeat() {
|
|
490
537
|
this.debug('heartbeat!');
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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,
|
|
506
|
-
},
|
|
538
|
+
|
|
539
|
+
await fetch(`${this.config.syncWorkerUrl}/${this.path}/heartbeat`, {
|
|
540
|
+
method: 'post',
|
|
541
|
+
headers: {
|
|
542
|
+
'Content-Type': 'application/json'
|
|
507
543
|
},
|
|
508
|
-
|
|
509
|
-
|
|
544
|
+
body: JSON.stringify({
|
|
545
|
+
session: Syncro.session,
|
|
546
|
+
...(this.isDirty && {dirty: true}),
|
|
547
|
+
}),
|
|
548
|
+
})
|
|
549
|
+
.then(response => response.ok
|
|
550
|
+
? null
|
|
551
|
+
: console.warn(this.path, `Heartbeat failed - ${response.statusText}`, {response})
|
|
552
|
+
)
|
|
553
|
+
.then(()=> this.isDirty = false) // Reset the dirty flag if it is set
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Utility function to directly set this documents firestore state
|
|
559
|
+
*
|
|
560
|
+
* @param {Object} state The state to set / merge
|
|
561
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
562
|
+
* @param {'merge'|'set'} [options.method='merge'] How to apply the new state. 'merge' (merge in partial data to an existing Syncro), 'set' (overwrite the entire Syncro state)
|
|
563
|
+
*
|
|
564
|
+
* @returns {Promise} A promise which resolves when the operation has completed
|
|
565
|
+
*/
|
|
566
|
+
setFirestoreState(state, options) {
|
|
567
|
+
let settings = {
|
|
568
|
+
method: 'merge',
|
|
569
|
+
...options,
|
|
570
|
+
};
|
|
571
|
+
if (!this.docRef) throw new Error('mount() must be called before setting Firestore state');
|
|
572
|
+
|
|
573
|
+
return (settings.method == 'set' ? FirestoreSetDoc : FirestoreUpdateDoc)(
|
|
574
|
+
this.docRef,
|
|
575
|
+
Syncro.toFirestore(state),
|
|
576
|
+
)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Utility method to fetch the Firestore state for this Syncro
|
|
582
|
+
* NOTE: This directly extracts the state of the Firestore, not its wrapping doc object returned by `FirestoreGetDoc`
|
|
583
|
+
*
|
|
584
|
+
* @returns {Promise<Object>} A promise which resolves to the Firestore state
|
|
585
|
+
*/
|
|
586
|
+
getFirestoreState() {
|
|
587
|
+
if (!this.docRef) throw new Error('mount() must be called before getting Firestore state');
|
|
588
|
+
|
|
589
|
+
return FirestoreGetDoc(this.docRef)
|
|
590
|
+
.then(doc => Syncro.fromFirestore(doc.data()))
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Set the Syncro dirty flag which gets passed to the server on the next heartbeat
|
|
596
|
+
*
|
|
597
|
+
* @see isDirty
|
|
598
|
+
* @returns {Syncro} This chainable Syncro instance
|
|
599
|
+
*/
|
|
600
|
+
markDirty() {
|
|
601
|
+
this.isDirty = true;
|
|
602
|
+
return this;
|
|
510
603
|
}
|
|
511
604
|
|
|
512
605
|
|
|
@@ -525,9 +618,7 @@ export default class Syncro {
|
|
|
525
618
|
...options,
|
|
526
619
|
};
|
|
527
620
|
|
|
528
|
-
return fetch(
|
|
529
|
-
method: 'post',
|
|
530
|
-
})
|
|
621
|
+
return fetch(`${this.config.syncWorkerUrl}/${this.path}/flush` + (settings.destroy ? '?destroy=1' : ''))
|
|
531
622
|
.then(response => response.ok ? null : Promise.reject(response.statusText || 'An error occured'))
|
|
532
623
|
}
|
|
533
624
|
|
|
@@ -574,149 +665,6 @@ export function randomBranch(depth = 0) {
|
|
|
574
665
|
}
|
|
575
666
|
|
|
576
667
|
|
|
577
|
-
/**
|
|
578
|
-
* Entities we support syncro paths on, each needs to correspond with a Firebase/Firestore collection name
|
|
579
|
-
*
|
|
580
|
-
* @type {Object} An object lookup of entities
|
|
581
|
-
*
|
|
582
|
-
* @property {String} singular The singular noun for the item
|
|
583
|
-
* @property {Function} initState Function called to initialize state when Firestore has no existing document. Called as `({supabase:SupabaseClient, entity:String, id:String, relation?:string})` and expected to return the initial data object state
|
|
584
|
-
* @property {Function} flushState Function called to flush state from Firebase to Supabase. Called the same as `initState` + `{state:Object}`
|
|
585
|
-
*/
|
|
586
|
-
export const syncEntities = {
|
|
587
|
-
projects: {
|
|
588
|
-
singular: 'project',
|
|
589
|
-
async initState({supabase, id, tera}) {
|
|
590
|
-
const result = await Syncro.wrapSupabase(supabase.from('projects')
|
|
591
|
-
.select('data')
|
|
592
|
-
.limit(1)
|
|
593
|
-
.eq('id', id)
|
|
594
|
-
);
|
|
595
|
-
|
|
596
|
-
if (result.length !== 1) {
|
|
597
|
-
throw new Error(`Syncro project "${id}" not found`);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
const data = result[0].data;
|
|
601
|
-
console.log('[MIGRATION] State of temp at start:', data.temp);
|
|
602
|
-
|
|
603
|
-
// Check if temp variable exists
|
|
604
|
-
if (!data.temp) return data;
|
|
605
|
-
|
|
606
|
-
let shownAlert = false;
|
|
607
|
-
for (const toolKey in data.temp) {
|
|
608
|
-
// Check if temp has already been set to file path
|
|
609
|
-
if (typeof data.temp[toolKey] !== 'object') {
|
|
610
|
-
console.log(`[MIGRATION] Skipping conversion of ${toolKey}, not an object:`, data.temp[toolKey])
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
if (!shownAlert) {
|
|
615
|
-
shownAlert = true;
|
|
616
|
-
console.log('[MIGRATION] showing alert');
|
|
617
|
-
alert('Data found that will be migrated to new TERA file storage system. This may take a few minutes, please do not close your browser or refresh.');
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Create filename based on existing key
|
|
621
|
-
const toolName = toolKey.split('-')[0];
|
|
622
|
-
const fileName = `data-${toolName}-${nanoid()}.json`;
|
|
623
|
-
console.log('[MIGRATION] Creating filename:', fileName);
|
|
624
|
-
|
|
625
|
-
// Create file, set contents and overwrite key
|
|
626
|
-
const projectFile = await tera.createProjectFile(fileName);
|
|
627
|
-
console.log('[MIGRATION] Setting file contents:', projectFile, 'to:', data.temp[toolKey]);
|
|
628
|
-
await projectFile.setContents(data.temp[toolKey])
|
|
629
|
-
console.log('[MIGRATION] Overwriting temp key with filepath:', fileName);
|
|
630
|
-
data.temp[toolKey] = fileName;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
console.log("[MIGRATION] State of temp at end:", data.temp)
|
|
634
|
-
|
|
635
|
-
return data;
|
|
636
|
-
},
|
|
637
|
-
flushState({supabase, state, fsId}) {
|
|
638
|
-
return Syncro.wrapSupabase(supabase.rpc('syncro_merge_data', {
|
|
639
|
-
table_name: 'projects',
|
|
640
|
-
entity_id: fsId,
|
|
641
|
-
new_data: state,
|
|
642
|
-
}))
|
|
643
|
-
},
|
|
644
|
-
},
|
|
645
|
-
project_namespaces: {
|
|
646
|
-
singular: 'project namespace',
|
|
647
|
-
initState({supabase, id, relation}) {
|
|
648
|
-
if (!relation) throw new Error('Project namespace relation missing, path should resemble "project_namespaces::${PROJECT}::${RELATION}"');
|
|
649
|
-
return Syncro.wrapSupabase(supabase.from('project_namespaces')
|
|
650
|
-
.select('data')
|
|
651
|
-
.limit(1)
|
|
652
|
-
.eq('project', id)
|
|
653
|
-
.eq('name', relation)
|
|
654
|
-
)
|
|
655
|
-
.then(rows => rows.length == 1
|
|
656
|
-
? rows[0]
|
|
657
|
-
: Syncro.wrapSupabase(supabase.from('project_namespaces') // Doesn't exist - create it
|
|
658
|
-
.insert({
|
|
659
|
-
project: id,
|
|
660
|
-
name: relation,
|
|
661
|
-
data: {},
|
|
662
|
-
})
|
|
663
|
-
.select('data')
|
|
664
|
-
)
|
|
665
|
-
)
|
|
666
|
-
.then(item => item.data);
|
|
667
|
-
},
|
|
668
|
-
flushState({supabase, state, id, relation}) {
|
|
669
|
-
return Syncro.wrapSupabase(supabase.from('project_namespaces')
|
|
670
|
-
.update({
|
|
671
|
-
edited_at: new Date(),
|
|
672
|
-
data: state,
|
|
673
|
-
})
|
|
674
|
-
.eq('project', id)
|
|
675
|
-
.eq('name', relation)
|
|
676
|
-
)
|
|
677
|
-
},
|
|
678
|
-
},
|
|
679
|
-
test: {
|
|
680
|
-
singular: 'test',
|
|
681
|
-
initState({supabase, id}) {
|
|
682
|
-
return Syncro.wrapSupabase(supabase.from('test')
|
|
683
|
-
.select('data')
|
|
684
|
-
.limit(1)
|
|
685
|
-
.eq('id', id)
|
|
686
|
-
)
|
|
687
|
-
.then(rows => rows.length == 1 ? rows[0] : Promise.reject(`Syncro test item "${id}" not found`))
|
|
688
|
-
.then(item => item.data);
|
|
689
|
-
},
|
|
690
|
-
flushState({supabase, state, fsId}) {
|
|
691
|
-
return Syncro.wrapSupabase(supabase.rpc('syncro_merge_data', {
|
|
692
|
-
table_name: 'test',
|
|
693
|
-
entity_id: fsId,
|
|
694
|
-
new_data: state,
|
|
695
|
-
}))
|
|
696
|
-
},
|
|
697
|
-
},
|
|
698
|
-
users: {
|
|
699
|
-
singular: 'user',
|
|
700
|
-
initState({supabase, id}) {
|
|
701
|
-
return Syncro.wrapSupabase(supabase.from('users')
|
|
702
|
-
.select('data')
|
|
703
|
-
.limit(1)
|
|
704
|
-
.eq('id', id)
|
|
705
|
-
)
|
|
706
|
-
.then(rows => rows.length == 1 ? rows[0] : Promise.reject(`Syncro user "${id}" not found`))
|
|
707
|
-
.then(item => item.data);
|
|
708
|
-
},
|
|
709
|
-
flushState({supabase, state, fsId}) {
|
|
710
|
-
return Syncro.wrapSupabase(supabase.rpc('syncro_merge_data', {
|
|
711
|
-
table_name: 'users',
|
|
712
|
-
entity_id: fsId,
|
|
713
|
-
new_data: state,
|
|
714
|
-
}))
|
|
715
|
-
},
|
|
716
|
-
},
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
|
|
720
668
|
/**
|
|
721
669
|
* NPM:@momsfriendlydevco/marshal Compatible module for flattening arrays
|
|
722
670
|
* @type {MarshalModule}
|
package/lib/terafy.client.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {diff} from 'just-diff';
|
|
2
1
|
import {cloneDeep} from 'lodash-es';
|
|
3
2
|
import Mitt from 'mitt';
|
|
4
3
|
import {nanoid} from 'nanoid';
|
|
@@ -61,8 +60,8 @@ export default class TeraFy {
|
|
|
61
60
|
'allow-scripts',
|
|
62
61
|
'allow-top-navigation',
|
|
63
62
|
],
|
|
64
|
-
handshakeInterval:
|
|
65
|
-
handshakeTimeout:
|
|
63
|
+
handshakeInterval: 1_000, // ~1s
|
|
64
|
+
handshakeTimeout: 10_000, // ~10s
|
|
66
65
|
debugPaths: null, // Transformed into a Array<String> (in Lodash dotted notation) on init()
|
|
67
66
|
};
|
|
68
67
|
|
|
@@ -127,10 +126,6 @@ export default class TeraFy {
|
|
|
127
126
|
'setProjectState',
|
|
128
127
|
'setProjectStateDefaults',
|
|
129
128
|
'setProjectStateRefresh',
|
|
130
|
-
'saveProjectState',
|
|
131
|
-
'replaceProjectState',
|
|
132
|
-
// 'applyProjectStatePatch', - Handled below (applies behaviour to watch for `settings.debugPaths`)
|
|
133
|
-
// For bindProjectState() - See individual plugins
|
|
134
129
|
|
|
135
130
|
// Project files
|
|
136
131
|
// 'selectProjectFile', - Handled below (requires return collection mapped to ProjectFile)
|
|
@@ -348,7 +343,7 @@ export default class TeraFy {
|
|
|
348
343
|
*
|
|
349
344
|
* @returns {Promise} A promise which resolves when the mount operation has completed
|
|
350
345
|
*/
|
|
351
|
-
_mountNamespace(name) {
|
|
346
|
+
_mountNamespace(name) { // eslint-disable-line no-unused-vars
|
|
352
347
|
console.warn('teraFy._mountNamespace() has not been overriden by a TERA-fy plugin, load one to add this functionality for your preferred framework');
|
|
353
348
|
throw new Error('teraFy._mountNamespace() is not supported');
|
|
354
349
|
}
|
|
@@ -378,80 +373,11 @@ export default class TeraFy {
|
|
|
378
373
|
*
|
|
379
374
|
* @returns {Promise} A promise which resolves when the operation has completed
|
|
380
375
|
*/
|
|
381
|
-
_unmountNamespace(name) {
|
|
376
|
+
_unmountNamespace(name) { // eslint-disable-line no-unused-vars
|
|
382
377
|
console.warn('teraFy.unbindNamespace() has not been overriden by a TERA-fy plugin, load one to add this functionality for your preferred framework');
|
|
383
378
|
}
|
|
384
379
|
// }}}
|
|
385
380
|
|
|
386
|
-
// Project state - createProjectStatePatch(), applyProjectStatePatchLocal() {{{
|
|
387
|
-
/**
|
|
388
|
-
* Create + transmit a new project state patch base on the current and previous states
|
|
389
|
-
* The transmitted patch follows the [JSPatch](http://jsonpatch.com) standard
|
|
390
|
-
* This function accepts an entire projectState instance, computes the delta and transmits that to the server for merging
|
|
391
|
-
*
|
|
392
|
-
* @param {Object} newState The local projectState to accept
|
|
393
|
-
* @param {Object} oldState The previous projectState to examine against
|
|
394
|
-
*
|
|
395
|
-
* @returns {Promise} A promise which will resolve when the operation has completed
|
|
396
|
-
*/
|
|
397
|
-
createProjectStatePatch(newState, oldState) {
|
|
398
|
-
let patch = diff(oldState, newState);
|
|
399
|
-
if (patch.length == 0) {
|
|
400
|
-
this.debug('INFO', 4, 'Skipping empty project patch', {patch, newState, oldState});
|
|
401
|
-
return Promise.resolve();
|
|
402
|
-
} else {
|
|
403
|
-
this.debug('INFO', 3, 'Created project patch', {patch, newState, oldState});
|
|
404
|
-
return this.applyProjectStatePatch(patch);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Transmit a patch to the remote server
|
|
411
|
-
* This function also enters debugging mode if any of the `settings.debugPaths` are operated on
|
|
412
|
-
*
|
|
413
|
-
* @param {Array} patch Patch to apply
|
|
414
|
-
* @returns {Promise} A promise which resolves when the operation has completed
|
|
415
|
-
*/
|
|
416
|
-
applyProjectStatePatch(patch) {
|
|
417
|
-
// watchedPaths guards {{{
|
|
418
|
-
if (this.settings.devMode && this.settings.debugPaths) {
|
|
419
|
-
if (!Array.isArray(this.settings.debugPaths)) throw new Error('teraFyClient.settings.debugPaths should be either null or an Array<String>');
|
|
420
|
-
|
|
421
|
-
let watchedPaths = patch
|
|
422
|
-
.filter(patch => this.settings.debugPaths.some(debugPath =>
|
|
423
|
-
patch.path.join('.').slice(0, debugPath.length) == debugPath
|
|
424
|
-
))
|
|
425
|
-
.map(patch => patch.path.join('.'));
|
|
426
|
-
|
|
427
|
-
if (watchedPaths.length > 0) {
|
|
428
|
-
console.info('Detected writes to', watchedPaths, '- entering debugging mode');
|
|
429
|
-
debugger; // eslint-disable-line no-debugger
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
// }}}
|
|
433
|
-
|
|
434
|
-
return this.rpc('applyProjectStatePatch', patch, {
|
|
435
|
-
session: this.settings.session,
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
// eslint-disable-next-line jsdoc/require-returns-check
|
|
441
|
-
/**
|
|
442
|
-
* Client function which accepts a patch from the server and applies it to local project state
|
|
443
|
-
* The patch should follow the [JSPatch](http://jsonpatch.com) standard
|
|
444
|
-
* This function is expected to be sub-classed by a plugin
|
|
445
|
-
*
|
|
446
|
-
* @param {Array} patch A JSPatch patch to apply
|
|
447
|
-
*
|
|
448
|
-
* @returns {Promise} A promise which will resolve when the operation has completed
|
|
449
|
-
*/
|
|
450
|
-
applyProjectStatePatchLocal(patch) { // eslint-disable-line
|
|
451
|
-
throw new Error('applyProjectStatePatchLocal() has not been sub-classed by a plugin');
|
|
452
|
-
}
|
|
453
|
-
// }}}
|
|
454
|
-
|
|
455
381
|
// Init - constructor(), init(), inject*() {{{
|
|
456
382
|
|
|
457
383
|
/**
|
|
@@ -1228,44 +1154,6 @@ export default class TeraFy {
|
|
|
1228
1154
|
*/
|
|
1229
1155
|
|
|
1230
1156
|
|
|
1231
|
-
/**
|
|
1232
|
-
* Force-Save the currently active project state
|
|
1233
|
-
*
|
|
1234
|
-
* @function saveProjectState
|
|
1235
|
-
* @returns {Promise} A promise which resolves when the operation has completed
|
|
1236
|
-
*/
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
/**
|
|
1240
|
-
* Overwrite the entire project state with a new object
|
|
1241
|
-
* You almost never want to use this function directly, see `setProjectState(path, value)` for a nicer wrapper
|
|
1242
|
-
*
|
|
1243
|
-
* @function replaceProjectState
|
|
1244
|
-
* @see setProjectState()
|
|
1245
|
-
* @param {Object} newState The new state to replace the current state with
|
|
1246
|
-
* @returns {Promise} A promise which resolves when the operation has completed
|
|
1247
|
-
*/
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
/**
|
|
1251
|
-
* Apply a computed `just-diff` patch to the current project state
|
|
1252
|
-
*
|
|
1253
|
-
* @function applyProjectStatePatch
|
|
1254
|
-
* @param {Object} Patch to apply
|
|
1255
|
-
* @returns {Promise} A promise which resolves when the operation has completed
|
|
1256
|
-
*/
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
/**
|
|
1260
|
-
* Subscribe to project state changes
|
|
1261
|
-
* This will dispatch an RPC call to the source object `applyProjectStatePatchLocal()` function with the patch
|
|
1262
|
-
* If the above call fails the subscriber is assumed as dead and unsubscribed from the polling list
|
|
1263
|
-
*
|
|
1264
|
-
* @function subscribeProjectState
|
|
1265
|
-
* @returns {Promise<Function>} A promise which resolves when a subscription has been created, call the resulting function to unsubscribe
|
|
1266
|
-
*/
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
1157
|
/**
|
|
1270
1158
|
* Data structure for a file filter
|
|
1271
1159
|
* @name FileFilters
|