@iebh/tera-fy 2.0.14 → 2.0.16
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 +15 -0
- package/dist/plugin.vue2.es2019.js +13 -13
- package/lib/syncro/entities.js +20 -15
- package/lib/syncro/syncro.js +77 -48
- package/lib/terafy.server.js +2 -1
- package/package.json +2 -1
package/lib/syncro/entities.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import Reflib from '@iebh/reflib';
|
|
2
2
|
import {v4 as uuid4} from 'uuid';
|
|
3
3
|
import {nanoid} from 'nanoid';
|
|
4
|
-
import Syncro from './syncro.js';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Entities we support Syncro paths for, each should correspond directly with a Firebase/Firestore collection name
|
|
@@ -9,14 +8,14 @@ import Syncro from './syncro.js';
|
|
|
9
8
|
* @type {Object} An object lookup of entities
|
|
10
9
|
*
|
|
11
10
|
* @property {String} singular The singular noun for the item
|
|
12
|
-
* @property {Function} initState Function called to initialize state when Firestore has no existing document. Called as `({supabase:
|
|
11
|
+
* @property {Function} initState Function called to initialize state when Firestore has no existing document. Called as `({supabase:Supabasey, entity:String, id:String, relation?:string})` and expected to return the initial data object state
|
|
13
12
|
* @property {Function} flushState Function called to flush state from Firebase to Supabase. Called the same as `initState` + `{state:Object}`
|
|
14
13
|
*/
|
|
15
14
|
export default {
|
|
16
15
|
projects: { // {{{
|
|
17
16
|
singular: 'project',
|
|
18
17
|
async initState({supabase, id}) {
|
|
19
|
-
let data = await
|
|
18
|
+
let data = await supabase(s => s
|
|
20
19
|
.from('projects')
|
|
21
20
|
.select('data')
|
|
22
21
|
.maybeSingle()
|
|
@@ -43,7 +42,7 @@ export default {
|
|
|
43
42
|
console.log('[MIGRATION] Creating filename:', fileName);
|
|
44
43
|
|
|
45
44
|
return Promise.resolve()
|
|
46
|
-
.then(()=>
|
|
45
|
+
.then(()=> supabase(s => s // Split data.temp[toolKey] -> file {{{
|
|
47
46
|
.from('projects')
|
|
48
47
|
.upload(
|
|
49
48
|
`${id}/${fileName}`,
|
|
@@ -82,7 +81,7 @@ export default {
|
|
|
82
81
|
return data;
|
|
83
82
|
},
|
|
84
83
|
flushState({supabase, state, fsId}) {
|
|
85
|
-
return
|
|
84
|
+
return supabase(s => s.rpc('syncro_merge_data', {
|
|
86
85
|
table_name: 'projects',
|
|
87
86
|
entity_id: fsId,
|
|
88
87
|
new_data: state,
|
|
@@ -97,13 +96,13 @@ export default {
|
|
|
97
96
|
let fileId = relation.replace(/_\*$/, '');
|
|
98
97
|
|
|
99
98
|
return Promise.resolve()
|
|
100
|
-
.then(()=>
|
|
99
|
+
.then(()=> supabase(s => s.storage
|
|
101
100
|
.from('projects')
|
|
102
101
|
.list(id)
|
|
103
102
|
))
|
|
104
103
|
.then(files => files.find(f => f.id == fileId))
|
|
105
104
|
.then(file => file || Promise.reject(`Invalid file ID "${fileId}"`))
|
|
106
|
-
.then(file =>
|
|
105
|
+
.then(file => supabase(s => s.storage
|
|
107
106
|
.from('projects')
|
|
108
107
|
.download(`${id}/${file.name}`)
|
|
109
108
|
)
|
|
@@ -130,7 +129,8 @@ export default {
|
|
|
130
129
|
singular: 'project namespace',
|
|
131
130
|
initState({supabase, id, relation}) {
|
|
132
131
|
if (!relation) throw new Error('Project namespace relation missing, path should resemble "project_namespaces::${PROJECT}::${RELATION}"');
|
|
133
|
-
return
|
|
132
|
+
return supabase(s => s
|
|
133
|
+
.from('project_namespaces')
|
|
134
134
|
.select('data')
|
|
135
135
|
.limit(1)
|
|
136
136
|
.eq('project', id)
|
|
@@ -138,7 +138,8 @@ export default {
|
|
|
138
138
|
)
|
|
139
139
|
.then(rows => rows.length == 1
|
|
140
140
|
? rows[0]
|
|
141
|
-
:
|
|
141
|
+
: supabase(s => s
|
|
142
|
+
.from('project_namespaces') // Doesn't exist - create it
|
|
142
143
|
.insert({
|
|
143
144
|
project: id,
|
|
144
145
|
name: relation,
|
|
@@ -150,7 +151,8 @@ export default {
|
|
|
150
151
|
.then(item => item.data);
|
|
151
152
|
},
|
|
152
153
|
flushState({supabase, state, id, relation}) {
|
|
153
|
-
return
|
|
154
|
+
return supabase(s => s
|
|
155
|
+
.from('project_namespaces')
|
|
154
156
|
.update({
|
|
155
157
|
edited_at: new Date(),
|
|
156
158
|
data: state,
|
|
@@ -163,7 +165,8 @@ export default {
|
|
|
163
165
|
test: { // {{{
|
|
164
166
|
singular: 'test',
|
|
165
167
|
initState({supabase, id}) {
|
|
166
|
-
return
|
|
168
|
+
return supabase(s => s
|
|
169
|
+
.from('test')
|
|
167
170
|
.select('data')
|
|
168
171
|
.limit(1)
|
|
169
172
|
.eq('id', id)
|
|
@@ -172,7 +175,7 @@ export default {
|
|
|
172
175
|
.then(item => item.data);
|
|
173
176
|
},
|
|
174
177
|
flushState({supabase, state, fsId}) {
|
|
175
|
-
return
|
|
178
|
+
return supabase(s => s.rpc('syncro_merge_data', {
|
|
176
179
|
table_name: 'test',
|
|
177
180
|
entity_id: fsId,
|
|
178
181
|
new_data: state,
|
|
@@ -182,7 +185,8 @@ export default {
|
|
|
182
185
|
users: { // {{{
|
|
183
186
|
singular: 'user',
|
|
184
187
|
initState({supabase, id}) {
|
|
185
|
-
return
|
|
188
|
+
return supabase(s => s
|
|
189
|
+
.from('users')
|
|
186
190
|
.select('data')
|
|
187
191
|
.limit(1)
|
|
188
192
|
.maybeSingle()
|
|
@@ -192,7 +196,8 @@ export default {
|
|
|
192
196
|
if (user) return user.data; // User is valid and already exists
|
|
193
197
|
|
|
194
198
|
// User row doesn't already exist - need to create stub
|
|
195
|
-
return
|
|
199
|
+
return supabase(s => s
|
|
200
|
+
.from('users')
|
|
196
201
|
.insert({
|
|
197
202
|
id,
|
|
198
203
|
data: {
|
|
@@ -209,7 +214,7 @@ export default {
|
|
|
209
214
|
})
|
|
210
215
|
},
|
|
211
216
|
flushState({supabase, state, fsId}) {
|
|
212
|
-
return
|
|
217
|
+
return supabase(s => s.rpc('syncro_merge_data', {
|
|
213
218
|
table_name: 'users',
|
|
214
219
|
entity_id: fsId,
|
|
215
220
|
new_data: state,
|
package/lib/syncro/syncro.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from 'firebase/firestore';
|
|
15
15
|
import marshal from '@momsfriendlydevco/marshal';
|
|
16
16
|
import {nanoid} from 'nanoid';
|
|
17
|
+
import PromiseRetry from 'p-retry';
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -135,6 +136,18 @@ export default class Syncro {
|
|
|
135
136
|
debug(...msg) {} // eslint-disable-line no-unused-vars
|
|
136
137
|
|
|
137
138
|
|
|
139
|
+
/**
|
|
140
|
+
* @interface
|
|
141
|
+
* Debugging printer specifically for error messages
|
|
142
|
+
* Defaults to using console.log()
|
|
143
|
+
*
|
|
144
|
+
* @param {*...} [msg] The message to output
|
|
145
|
+
*/
|
|
146
|
+
debugError(...msg) {
|
|
147
|
+
console.log(`[Syncro ${this.path}]`, ...msg);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
138
151
|
/**
|
|
139
152
|
* Instance constructor
|
|
140
153
|
*
|
|
@@ -467,11 +480,14 @@ export default class Syncro {
|
|
|
467
480
|
*
|
|
468
481
|
* @param {Object} [options] Additional options to mutate behaviour
|
|
469
482
|
* @param {Object} [options.initalState] State to use if no state is already loaded, overrides the entities own `initState` function fetcher
|
|
483
|
+
* @param {Number} [options.retries=3] Number of times to retry if a mounted Syncro fails its sanity checks
|
|
470
484
|
* @returns {Promise<Syncro>} A promise which resolves as this syncro instance when completed
|
|
471
485
|
*/
|
|
472
486
|
mount(options) {
|
|
473
487
|
let settings = {
|
|
474
488
|
initialState: null,
|
|
489
|
+
retries: 5,
|
|
490
|
+
retryMinTime: 250,
|
|
475
491
|
...options,
|
|
476
492
|
};
|
|
477
493
|
|
|
@@ -479,54 +495,67 @@ export default class Syncro {
|
|
|
479
495
|
let reactive; // Eventual response from reactive() with the intitial value
|
|
480
496
|
let doc; // Eventual Firebase document
|
|
481
497
|
|
|
482
|
-
return
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
498
|
+
return PromiseRetry(
|
|
499
|
+
()=> Promise.resolve()
|
|
500
|
+
.then(()=> this.setHeartbeat(false)) // Disable any existing heartbeat - this only really applies if we're changing path for some reason
|
|
501
|
+
.then(async ()=> { // Set up binding and wait for it to come ready
|
|
502
|
+
this.docRef = FirestoreDocRef(Syncro.firestore, fsCollection, fsId);
|
|
503
|
+
|
|
504
|
+
// Initalize state
|
|
505
|
+
let initialState = await this.getFirestoreState();
|
|
506
|
+
|
|
507
|
+
// Construct a reactive component
|
|
508
|
+
reactive = this.getReactive(initialState);
|
|
509
|
+
if (!reactive.doc || !reactive.setState || !reactive.getState || !reactive.watch) throw new Error('Syncro.getReactive() requires a returned `doc`, `setState()`, `getState()` + `watch()`');
|
|
510
|
+
this.value = doc = reactive.doc;
|
|
511
|
+
|
|
512
|
+
this.debug('Initial state', {doc});
|
|
513
|
+
|
|
514
|
+
// Subscribe to remote updates
|
|
515
|
+
this._destroyActions.push( // Add the unsubscribe handle to the list of destroyAction promises we call on `destroy()`
|
|
516
|
+
FirestoreOnSnapshot(this.docRef, snapshot => {
|
|
517
|
+
let snapshotData = Syncro.fromFirestore(snapshot.data());
|
|
518
|
+
this.debug('Incoming snapshot', {snapshotData});
|
|
519
|
+
reactive.setState(snapshotData);
|
|
520
|
+
})
|
|
521
|
+
);
|
|
522
|
+
})
|
|
523
|
+
.then(()=> { // Optionally create the doc if it has no content
|
|
524
|
+
if (!isEmpty(doc)) { // Doc already has content - skip
|
|
525
|
+
return;
|
|
526
|
+
} else if (settings.initialState) { // Provided an intiailState - use that instead of the entities own method
|
|
527
|
+
this.debug('Populate initial Syncro state (from provided initialState)');
|
|
528
|
+
return this.setFirestoreState(settings.initialState, {method: 'set'});
|
|
529
|
+
} else {
|
|
530
|
+
this.debug(`Populate initial Syncro state (from "${entity}" Syncro worker)`);
|
|
531
|
+
|
|
532
|
+
return fetch(`${this.config.syncroRegistryUrl}/${this.path}`)
|
|
533
|
+
.then(response => response.ok || Promise.reject(`Failed to check Syncro "${fsCollection}::${fsId}" status - ${response.statusText}`))
|
|
534
|
+
}
|
|
535
|
+
})
|
|
536
|
+
.then(()=> { // Setup local state watcher
|
|
537
|
+
reactive.watch(throttle(newState => {
|
|
538
|
+
this.debug('Local change', {newState});
|
|
539
|
+
this.markDirty();
|
|
540
|
+
this.setFirestoreState(newState, {method: 'merge'});
|
|
541
|
+
}, this.throttle));
|
|
542
|
+
})
|
|
543
|
+
.then(()=> this.setHeartbeat(true, {
|
|
544
|
+
immediate: true,
|
|
545
|
+
}))
|
|
546
|
+
.then(()=> this)
|
|
547
|
+
.catch(async (e) => {
|
|
548
|
+
await this.destroy();
|
|
549
|
+
throw e;
|
|
550
|
+
}),
|
|
551
|
+
{ // PromiseRetry / p-retry options
|
|
552
|
+
retries: settings.retries,
|
|
553
|
+
minTimeout: settings.retryMinTime,
|
|
554
|
+
randomize: true,
|
|
555
|
+
factor: 3,
|
|
556
|
+
onFailedAttempt: e => this.debugError(`[Attempt ${e.attemptNumber}/${e.attemptNumber + e.retriesLeft - 1}] to mount syncro`, e),
|
|
557
|
+
},
|
|
558
|
+
);
|
|
530
559
|
}
|
|
531
560
|
|
|
532
561
|
|
package/lib/terafy.server.js
CHANGED
|
@@ -1578,6 +1578,7 @@ export default class TeraFyServer {
|
|
|
1578
1578
|
close: false,
|
|
1579
1579
|
progress: 0,
|
|
1580
1580
|
progressMax: 0,
|
|
1581
|
+
backdrop: true,
|
|
1581
1582
|
...options,
|
|
1582
1583
|
});
|
|
1583
1584
|
} else { // Merge options with existing uiProgress window
|
|
@@ -1595,7 +1596,7 @@ export default class TeraFyServer {
|
|
|
1595
1596
|
this._uiProgress.promise = this.requestFocus(()=>
|
|
1596
1597
|
app.service('$prompt').dialog({
|
|
1597
1598
|
title: this._uiProgress.options.title,
|
|
1598
|
-
backdrop: this._uiProgress.options.backdrop, // pass backdrop to allow 'static' dialog
|
|
1599
|
+
backdrop: this._uiProgress.options.backdrop ?? true, // pass backdrop to allow 'static' dialog
|
|
1599
1600
|
component: 'uiProgress',
|
|
1600
1601
|
componentProps: this._uiProgress.options,
|
|
1601
1602
|
closeable: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iebh/tera-fy",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.16",
|
|
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=.",
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
"lodash-es": "^4.17.21",
|
|
82
82
|
"mitt": "^3.0.1",
|
|
83
83
|
"nanoid": "^5.1.2",
|
|
84
|
+
"p-retry": "^6.2.1",
|
|
84
85
|
"release-it": "^18.1.2",
|
|
85
86
|
"uuid": "^11.1.0"
|
|
86
87
|
},
|