@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.
@@ -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:SupabaseClient, entity:String, id:String, relation?:string})` and expected to return the initial data object state
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 Syncro.wrapSupabase(supabase
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(()=> Syncro.wrapSupabase(supabase.storage // Split data.temp[toolKey] -> file {{{
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 Syncro.wrapSupabase(supabase.rpc('syncro_merge_data', {
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(()=> Syncro.wrapSupabase(supabase.storage
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 => Syncro.wrapSupabase(supabase.storage
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 Syncro.wrapSupabase(supabase.from('project_namespaces')
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
- : Syncro.wrapSupabase(supabase.from('project_namespaces') // Doesn't exist - create it
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 Syncro.wrapSupabase(supabase.from('project_namespaces')
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 Syncro.wrapSupabase(supabase.from('test')
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 Syncro.wrapSupabase(supabase.rpc('syncro_merge_data', {
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 Syncro.wrapSupabase(supabase.from('users')
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 Syncro.wrapSupabase(supabase.from('users')
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 Syncro.wrapSupabase(supabase.rpc('syncro_merge_data', {
217
+ return supabase(s => s.rpc('syncro_merge_data', {
213
218
  table_name: 'users',
214
219
  entity_id: fsId,
215
220
  new_data: state,
@@ -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 Promise.resolve()
483
- .then(()=> this.setHeartbeat(false)) // Disable any existing heartbeat - this only really applies if we're changing path for some reason
484
- .then(async ()=> { // Set up binding and wait for it to come ready
485
- this.docRef = FirestoreDocRef(Syncro.firestore, fsCollection, fsId);
486
-
487
- // Initalize state
488
- let initialState = await this.getFirestoreState();
489
-
490
- // Construct a reactive component
491
- reactive = this.getReactive(initialState);
492
- if (!reactive.doc || !reactive.setState || !reactive.getState || !reactive.watch) throw new Error('Syncro.getReactive() requires a returned `doc`, `setState()`, `getState()` + `watch()`');
493
- this.value = doc = reactive.doc;
494
-
495
- this.debug('Initial state', {doc});
496
-
497
- // Subscribe to remote updates
498
- this._destroyActions.push( // Add the unsubscribe handle to the list of destroyAction promises we call on `destroy()`
499
- FirestoreOnSnapshot(this.docRef, snapshot => {
500
- let snapshotData = Syncro.fromFirestore(snapshot.data());
501
- this.debug('Incoming snapshot', {snapshotData});
502
- reactive.setState(snapshotData);
503
- })
504
- );
505
- })
506
- .then(()=> { // Optionally create the doc if it has no content
507
- if (!isEmpty(doc)) { // Doc already has content - skip
508
- return;
509
- } else if (settings.initialState) { // Provided an intiailState - use that instead of the entities own method
510
- this.debug('Populate initial Syncro state (from provided initialState)');
511
- return this.setFirestoreState(settings.initialState, {method: 'set'});
512
- } else {
513
- this.debug(`Populate initial Syncro state (from "${entity}" Syncro worker)`);
514
-
515
- return fetch(`${this.config.syncroRegistryUrl}/${this.path}`)
516
- .then(response => response.ok || Promise.reject(`Failed to check Syncro "${fsCollection}::${fsId}" status - ${response.statusText}`))
517
- }
518
- })
519
- .then(()=> { // Setup local state watcher
520
- reactive.watch(throttle(newState => {
521
- this.debug('Local change', {newState});
522
- this.markDirty();
523
- this.setFirestoreState(newState, {method: 'merge'});
524
- }, this.throttle));
525
- })
526
- .then(()=> this.setHeartbeat(true, {
527
- immediate: true,
528
- }))
529
- .then(()=> this)
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
 
@@ -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.14",
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
  },