@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.
@@ -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
- * Time between heartbeats
94
+ * Various Misc config for the Syncro instance
93
95
  *
94
- * @type {Number} Heartbeat time in milliseconds
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
- heartbeatInterval = 30_000; //~= 30s
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, options);
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 extracted = { .../^(?<entity>\w+?)::(?<id>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:::(?<relation>\w+?))?$/.exec(path)?.groups };
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 instances local `value` getReactive
423
+ * Mount the remote Firestore document against this Syncro instance
381
424
  *
382
- * @returns {Promise<Sync>} A promise which resolves as this sync instance when completed
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(tera) {
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 = Syncro.fromFirestore(
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)) return; // Doc already has some content at least?
418
-
419
- this.debug('Populate initial state');
420
-
421
- // Extract base data + add document and return new hook
422
- return Promise.resolve()
423
- .then(()=> syncEntities[entity] || Promise.reject(`Unknown Sync entity "${entity}"`))
424
- .then(()=> syncEntities[entity].initState({
425
- supabase: Syncro.supabase,
426
- fsCollection, fsId,
427
- entity, id, relation,
428
- tera
429
- }))
430
- .then(state => FirestoreSetDoc(
431
- this.docRef,
432
- Syncro.toFirestore(state),
433
- )) // Send new base state to Firestore
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
- FirestoreUpdateDoc(
439
- this.docRef,
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
- let timestamp = (new Date()).toISOString();
492
-
493
- let docRef = FirestoreDocRef(Syncro.firestore, 'presence', this.path);
494
- let doc = await FirestoreGetDoc(docRef);
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
- {merge: true},
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(`https://tera-tools.com/api/sync/${this.path}/flush` + (settings.destroy ? '?destroy=1' : ''), {
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}
@@ -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: 1000, // ~1s
65
- handshakeTimeout: 10000, // ~10s
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