@iebh/tera-fy 1.15.8 → 2.0.0

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.
@@ -105,6 +105,7 @@ export default class TeraFy {
105
105
  // Session
106
106
  'getUser',
107
107
  'requireUser',
108
+ 'getCredentials',
108
109
 
109
110
  // Projects
110
111
  'bindProject',
@@ -114,11 +115,17 @@ export default class TeraFy {
114
115
  'requireProject',
115
116
  'selectProject',
116
117
 
118
+ // Project namespaces
119
+ // 'mountNamespace', // Handled by this library
120
+ // 'unmountNamespace', // Handled by this library
121
+ 'getNamespace',
122
+ 'setNamespace',
123
+ 'listNamespaces',
124
+
117
125
  // Project State
118
126
  'getProjectState',
119
127
  'setProjectState',
120
128
  'setProjectStateDefaults',
121
- 'setProjectStateFlush',
122
129
  'setProjectStateRefresh',
123
130
  'saveProjectState',
124
131
  'replaceProjectState',
@@ -164,6 +171,16 @@ export default class TeraFy {
164
171
  plugins = [];
165
172
 
166
173
 
174
+ /**
175
+ * Active namespaces we are subscribed to
176
+ * Each key is the namespace name with the value as the local reactive \ observer \ object equivelent
177
+ * The key string is always of the form `${ENTITY}::${ID}` e.g. `projects:1234`
178
+ *
179
+ * @type {Object<Object>}
180
+ */
181
+ namespaces = {};
182
+
183
+
167
184
  // Messages - send(), sendRaw(), rpc(), acceptMessage() {{{
168
185
 
169
186
  /**
@@ -303,6 +320,68 @@ export default class TeraFy {
303
320
 
304
321
  // }}}
305
322
 
323
+ // Project namespace - mountNamespace(), unmountNamespace() {{{
324
+ /**
325
+ * Make a namespace available locally
326
+ * This generally creates whatever framework flavoured reactive/observer/object is supported locally - generally with writes automatically synced with the master state
327
+ *
328
+ * @param {String} name The alias of the namespace, this should be alphanumeric + hyphens + underscores
329
+ *
330
+ * @returns {Promise<Reactive>} A promise which resolves to the reactive object
331
+ */
332
+ mountNamespace(name) {
333
+ if (!/^[\w-]+$/.test(name)) throw new Error('Namespaces must be alphanumeric + hyphens + underscores');
334
+ if (this.namespaces[name]) return Promise.resolve(this.namespaces[name]); // Already mounted
335
+
336
+ return Promise.resolve()
337
+ .then(()=> this._mountNamespace(name))
338
+ .then(()=> this.namespaces[name] || Promise.reject(`teraFy.mountNamespace('${name}') resolved but no namespace has been mounted`))
339
+ }
340
+
341
+
342
+ /**
343
+ * @interface
344
+ * Actual namespace mounting function designed to be overriden by plugins
345
+ *
346
+ * @param {String} name The alias of the namespace, this should be alphanumeric + hyphens + underscores
347
+ *
348
+ * @returns {Promise} A promise which resolves when the mount operation has completed
349
+ */
350
+ _mountNamespace(name) {
351
+ console.warn('teraFy._mountNamespace() has not been overriden by a TERA-fy plugin, load one to add this functionality for your preferred framework');
352
+ throw new Error('teraFy._mountNamespace() is not supported');
353
+ }
354
+
355
+
356
+ /**
357
+ * Release a locally mounted namespace
358
+ * This function will remove the namespace from `namespaces`, cleaning up any memory / subscription hooks
359
+ *
360
+ * @interface
361
+ *
362
+ * @param {String} name The name of the namespace to unmount
363
+ *
364
+ * @returns {Promise} A promise which resolves when the operation has completed
365
+ */
366
+ unmountNamespace(name) {
367
+ if (!this.namespaces[name]) return Promise.resolve(); // Already unmounted
368
+ return this._unmountNamespace(name);
369
+ }
370
+
371
+
372
+ /**
373
+ * @interface
374
+ * Actual namespace unmounting function designed to be overriden by plugins
375
+ *
376
+ * @param {String} name The name of the namespace to unmount
377
+ *
378
+ * @returns {Promise} A promise which resolves when the operation has completed
379
+ */
380
+ _unmountNamespace(name) {
381
+ console.warn('teraFy.unbindNamespace() has not been overriden by a TERA-fy plugin, load one to add this functionality for your preferred framework');
382
+ }
383
+ // }}}
384
+
306
385
  // Project state - createProjectStatePatch(), applyProjectStatePatchLocal() {{{
307
386
  /**
308
387
  * Create + transmit a new project state patch base on the current and previous states
@@ -316,8 +395,13 @@ export default class TeraFy {
316
395
  */
317
396
  createProjectStatePatch(newState, oldState) {
318
397
  let patch = diff(oldState, newState);
319
- this.debug('INFO', 3, 'Created project patch', {patch, newState, oldState});
320
- return this.applyProjectStatePatch(patch);
398
+ if (patch.length == 0) {
399
+ this.debug('INFO', 4, 'Skipping empty project patch', {patch, newState, oldState});
400
+ return Promise.resolve();
401
+ } else {
402
+ this.debug('INFO', 3, 'Created project patch', {patch, newState, oldState});
403
+ return this.applyProjectStatePatch(patch);
404
+ }
321
405
  }
322
406
 
323
407
 
@@ -329,6 +413,7 @@ export default class TeraFy {
329
413
  * @returns {Promise} A promise which resolves when the operation has completed
330
414
  */
331
415
  applyProjectStatePatch(patch) {
416
+ // watchedPaths guards {{{
332
417
  if (this.settings.devMode && this.settings.debugPaths) {
333
418
  if (!Array.isArray(this.settings.debugPaths)) throw new Error('teraFyClient.settings.debugPaths should be either null or an Array<String>');
334
419
 
@@ -343,6 +428,7 @@ export default class TeraFy {
343
428
  debugger; // eslint-disable-line no-debugger
344
429
  }
345
430
  }
431
+ // }}}
346
432
 
347
433
  return this.rpc('applyProjectStatePatch', patch, {
348
434
  session: this.settings.session,
@@ -831,19 +917,35 @@ export default class TeraFy {
831
917
  * @param {Object} source Initalized source object to extend from
832
918
  */
833
919
  mixin(target, source) {
834
- Object.getOwnPropertyNames(Object.getPrototypeOf(source))
835
- .filter(prop => !['constructor', 'prototype', 'name'].includes(prop))
836
- .filter(prop => prop != 'init') // Don't allow plugin init() to override our version - these all get called during init() anyway
837
- .forEach((prop) => {
838
- Object.defineProperty(
839
- target,
840
- prop,
841
- {
842
- value: source[prop].bind(target),
843
- enumerable: false,
844
- },
845
- );
846
- });
920
+ // Iterate through the source object upwards extracting each prototype
921
+ let prototypeStack = [];
922
+ let node = source;
923
+ do {
924
+ prototypeStack.unshift(node);
925
+ } while (node = Object.getPrototypeOf(node)); // Walk upwards until we hit null (no more inherited classes)
926
+
927
+ // Iterate through stacks inheriting each prop into the target
928
+ prototypeStack.forEach(stack =>
929
+ Object.getOwnPropertyNames(stack)
930
+ .filter(prop =>
931
+ !['constructor', 'init', 'prototype', 'name'].includes(prop) // Ignore forbidden properties
932
+ && !prop.startsWith('__') // Ignore double underscore meta properties
933
+ )
934
+ .forEach(prop => {
935
+ if (typeof source[prop] == 'function') { // Inheriting function - glue onto object as non-editable, non-enumerable property
936
+ Object.defineProperty(
937
+ target,
938
+ prop,
939
+ {
940
+ enumerable: false,
941
+ value: source[prop].bind(target), // Rebind functions
942
+ },
943
+ );
944
+ } else { // Everything else, just glue onto the object
945
+ target[prop] = source[prop];
946
+ }
947
+ })
948
+ )
847
949
  }
848
950
 
849
951
 
@@ -948,6 +1050,14 @@ export default class TeraFy {
948
1050
  */
949
1051
 
950
1052
 
1053
+ /**
1054
+ * Provide an object of credentials for 3rd party services like Firebase/Supabase
1055
+ *
1056
+ * @functions getCredentials
1057
+ * @returns {Object} An object containing 3rd party service credentials
1058
+ */
1059
+
1060
+
951
1061
  /**
952
1062
  * Require a user login to TERA
953
1063
  * If there is no user OR they are not logged in a prompt is shown to go and do so
@@ -1031,6 +1141,40 @@ export default class TeraFy {
1031
1141
  */
1032
1142
 
1033
1143
 
1144
+ /**
1145
+ * Get a one-off snapshot of a namespace without mounting it
1146
+ * This can be used for simpler apps which don't have their own reactive / observer equivelent
1147
+ *
1148
+ * @function getNamespace
1149
+ * @param {String} name The alias of the namespace, this should be alphanumeric + hyphens + underscores
1150
+ *
1151
+ * @returns {Promise<Object>} A promise which resolves to the namespace POJO state
1152
+ */
1153
+
1154
+
1155
+ /**
1156
+ * Set (or merge by default) a one-off snapshot over an existing namespace
1157
+ * This can be used for simpler apps which don't have their own reactive / observer equivelent and just want to quickly set something
1158
+ *
1159
+ * @function setNamespace
1160
+ * @param {String} name The name of the namespace
1161
+ * @param {Object} state The state to merge
1162
+ * @param {Object} [options] Additional options to mutate behaviour
1163
+ * @param {'merge'|'set'} [options.method='merge'] How to handle the state. 'merge' (merge a partial state over the existing namespace state), 'set' (completely overwrite the existing namespace)
1164
+ *
1165
+ * @returns {Promise<Object>} A promise which resolves to the namespace POJO state
1166
+ */
1167
+
1168
+
1169
+ /**
1170
+ * Return a list of namespaces available to the current project
1171
+ *
1172
+ * @function listNamespaces
1173
+ * @returns {Promise<Array<Object>>} Collection of available namespaces for the current project
1174
+ * @property {String} name The name of the namespace
1175
+ */
1176
+
1177
+
1034
1178
  /**
1035
1179
  * Return the current, full snapshot state of the active project
1036
1180
  *
@@ -1074,15 +1218,6 @@ export default class TeraFy {
1074
1218
  */
1075
1219
 
1076
1220
 
1077
- /**
1078
- * Force copying local changes to the server
1079
- * This is only ever needed when saving large quantities of data that need to be immediately available
1080
- *
1081
- * @function setProjectStateFlush
1082
- * @returns {Promise} A promise which resolves when the operation has completed
1083
- */
1084
-
1085
-
1086
1221
  /**
1087
1222
  * Force refetching the remote project state into local
1088
1223
  * This is only ever needed when saving large quantities of data that need to be immediately available
@@ -1421,9 +1556,9 @@ export default class TeraFy {
1421
1556
  *
1422
1557
  * @param {Object} [options] Additional options to mutate behaviour
1423
1558
  * @param {String} [options.body] Optional additional body text
1559
+ * @param {Boolean} [options.isHtml=false] If truthy, treat the body as HTML
1424
1560
  * @param {String} [options.value] Current or default value to display pre-filled
1425
1561
  * @param {String} [options.title='Input required'] The dialog title to display
1426
- * @param {Boolean} [options.bodyHtml=false] If truthy, treat the body as HTML
1427
1562
  * @param {String} [options.placeholder] Optional placeholder text
1428
1563
  * @param {Boolean} [options.required=true] Treat nullish or empty inputs as a cancel operation
1429
1564
  *
@@ -469,6 +469,16 @@ export default class TeraFyServer {
469
469
  }
470
470
 
471
471
 
472
+ /**
473
+ * Provide an object of credentials for 3rd party services like Firebase/Supabase
474
+ *
475
+ * @returns {Object} An object containing 3rd party service credentials
476
+ */
477
+ getCredentials() {
478
+ return app.service('$auth').credentials;
479
+ }
480
+
481
+
472
482
  /**
473
483
  * In embed mode only - create a popup window and try to auth via that
474
484
  *
@@ -736,6 +746,54 @@ export default class TeraFyServer {
736
746
 
737
747
  // }}}
738
748
 
749
+ // Project namespaces - getNamespace(), setNamespace(), listNamespaces() {{{
750
+ /**
751
+ * Get a one-off snapshot of a namespace without mounting it
752
+ * This can be used for simpler apps which don't have their own reactive / observer equivelent
753
+ *
754
+ * @param {String} name The alias of the namespace, this should be alphanumeric + hyphens + underscores
755
+ *
756
+ * @returns {Promise<Object>} A promise which resolves to the namespace POJO state
757
+ */
758
+ getNamespace(name) {
759
+ if (!/^[\w-]+$/.test(name)) throw new Error('Namespaces must be alphanumeric + hyphens + underscores');
760
+
761
+ return this.$syncro.getSnapshot(`project_namespaces::${this.$projects.active.id}::${name}`);
762
+ }
763
+
764
+
765
+ /**
766
+ * Set (or merge by default) a one-off snapshot over an existing namespace
767
+ * This can be used for simpler apps which don't have their own reactive / observer equivelent and just want to quickly set something
768
+ *
769
+ * @param {String} name The name of the namespace
770
+ * @param {Object} state The state to merge
771
+ * @param {Object} [options] Additional options to mutate behaviour
772
+ * @param {'merge'|'set'} [options.method='merge'] How to handle the state. 'merge' (merge a partial state over the existing namespace state), 'set' (completely overwrite the existing namespace)
773
+ *
774
+ * @returns {Promise<Object>} A promise which resolves to the namespace POJO state
775
+ */
776
+ setNamespace(name, state, options) {
777
+ if (!/^[\w--]+$/.test(name)) throw new Error('Namespaces must be alphanumeric + hyphens + underscores');
778
+ if (typeof state != 'object') throw new Error('State must be an object');
779
+
780
+ return this.$syncro.setSnapshot(`project_namespaces::${this.$projects.active.id}}::${name}`, state, {
781
+ method: options.method,
782
+ });
783
+ }
784
+
785
+
786
+ /**
787
+ * Return a list of namespaces available to the current project
788
+ *
789
+ * @returns {Promise<Array<Object>>} Collection of available namespaces for the current project
790
+ * @property {String} name The name of the namespace
791
+ */
792
+ listNamespaces() {
793
+ return app.service('$projects').listNamespaces();
794
+ }
795
+ // }}}
796
+
739
797
  // Project State - getProjectState(), setProjectState(), setProjectStateDefaults(), replaceProjectState(), applyProjectStatePatch() {{{
740
798
 
741
799
  /**
@@ -819,13 +877,11 @@ export default class TeraFyServer {
819
877
  * @param {String|Array<String>} [path] The sub-path within the project state to set, if unspecifed the entire target is used as a target and a save operation is forced
820
878
  * @param {*} value The value to set as the default structure
821
879
  * @param {Object} [options] Additional options to mutate behaviour, see setProjectState() for the full list of supported options
822
- * @param {Boolean} [options.flush=true] Force a re-read from the server after setting, prevents race conditions with large objects. Calls `setProjectStateFlush()` after applying defaults
823
880
  *
824
881
  * @returns {Promise<*>} A promise which resolves to the eventual input value after defaults have been applied
825
882
  */
826
883
  setProjectStateDefaults(path, value, options) {
827
884
  let settings = {
828
- flush: true,
829
885
  ...options,
830
886
  };
831
887
  if (!app.service('$projects').active) throw new Error('No active project');
@@ -841,7 +897,6 @@ export default class TeraFyServer {
841
897
  ...settings,
842
898
  },
843
899
  )
844
- .then(()=> settings.flush && this.setProjectStateFlush())
845
900
  .then(()=> pathTools.get(target, path));
846
901
  } else { // Called as (value) - Populate entire project layout
847
902
  pathTools.defaults(target, path);
@@ -850,26 +905,11 @@ export default class TeraFyServer {
850
905
  newState: cloneDeep(target),
851
906
  });
852
907
  return this.saveProjectState()
853
- .then(()=> settings.flush && this.setProjectStateFlush())
854
908
  .then(()=> target);
855
909
  }
856
910
  }
857
911
 
858
912
 
859
- /**
860
- * Force copying local changes to the server
861
- * This is only ever needed when saving large quantities of data that need to be immediately available
862
- *
863
- * @returns {Promise} A promise which resolves when the operation has completed
864
- */
865
- setProjectStateFlush() {
866
- this.debug('INFO', 1, 'Force project state flush!');
867
- if (!app.service('$projects').active) throw new Error('No active project');
868
- return app.service('$projects').active.$touchLocal()
869
- .then(()=> null)
870
- }
871
-
872
-
873
913
  /**
874
914
  * Force refetching the remote project state into local
875
915
  *
@@ -928,6 +968,11 @@ export default class TeraFyServer {
928
968
  */
929
969
  applyProjectStatePatch(patch, options) {
930
970
  if (!app.service('$projects').active) throw new Error('No active project to patch');
971
+ if (patch.length == 0) {
972
+ this.debug('INFO', 4, 'Skipping empty project state patch', patch);
973
+ return Promise.resolve();
974
+ }
975
+
931
976
  this.debug('INFO', 1, 'Applying', patch.length, 'project state patch', patch);
932
977
 
933
978
  let $projects = app.service('$projects');
@@ -1639,9 +1684,9 @@ export default class TeraFyServer {
1639
1684
  *
1640
1685
  * @param {Object} [options] Additional options to mutate behaviour
1641
1686
  * @param {String} [options.body] Optional additional body text
1687
+ * @param {Boolean} [options.isHtml=false] If truthy, treat the body as HTML
1642
1688
  * @param {String} [options.value] Current or default value to display pre-filled
1643
1689
  * @param {String} [options.title='Input required'] The dialog title to display
1644
- * @param {Boolean} [options.bodyHtml=false] If truthy, treat the body as HTML
1645
1690
  * @param {String} [options.placeholder] Optional placeholder text
1646
1691
  * @param {Boolean} [options.required=true] Treat nullish or empty inputs as a cancel operation
1647
1692
  *
@@ -1650,7 +1695,7 @@ export default class TeraFyServer {
1650
1695
  uiPrompt(text, options) {
1651
1696
  let settings = {
1652
1697
  body: '',
1653
- bodyHtml: false,
1698
+ isHtml: false,
1654
1699
  title: 'Input required',
1655
1700
  value: '',
1656
1701
  placeholder: '',
@@ -1669,7 +1714,7 @@ export default class TeraFyServer {
1669
1714
  component: 'UiPrompt',
1670
1715
  componentProps: {
1671
1716
  body: settings.body,
1672
- bodyHtml: settings.bodyHtml,
1717
+ isHtml: settings.isHtml,
1673
1718
  placeholder: settings.placeholder,
1674
1719
  value: settings.value,
1675
1720
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iebh/tera-fy",
3
- "version": "1.15.8",
3
+ "version": "2.0.0",
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=.",
@@ -72,6 +72,7 @@
72
72
  "node": ">=18"
73
73
  },
74
74
  "dependencies": {
75
+ "@momsfriendlydevco/marshal": "^2.1.1",
75
76
  "detect-port": "^1.6.1",
76
77
  "filesize": "^10.1.4",
77
78
  "http-proxy": "^1.18.1",
@@ -82,15 +83,17 @@
82
83
  "release-it": "^17.6.0"
83
84
  },
84
85
  "devDependencies": {
85
- "@momsfriendlydevco/eslint-config": "^2.0.3",
86
+ "@momsfriendlydevco/eslint-config": "^2.1.0",
86
87
  "@release-it/conventional-changelog": "^8.0.1",
87
88
  "concurrently": "^8.2.2",
88
89
  "documentation": "^14.0.3",
89
90
  "esbuild": "^0.23.0",
90
- "eslint": "^9.7.0",
91
+ "eslint": "^9.18.0",
91
92
  "nodemon": "^3.1.7"
92
93
  },
93
94
  "peerDependencies": {
95
+ "@supabase/supabase-js": "^2.48.1",
96
+ "firebase": "^11.3.1",
94
97
  "vue": "^3.0.0"
95
98
  },
96
99
  "optionalDependencies": {
package/plugins/base.js CHANGED
@@ -9,8 +9,7 @@ export default class TeraFyPlugin {
9
9
  /**
10
10
  * Optional function to be included when the main TeraFyClient is initalized
11
11
  */
12
- init() {
13
- }
12
+ init() {}
14
13
 
15
14
 
16
15
  /**
@@ -19,6 +18,6 @@ export default class TeraFyPlugin {
19
18
  * @param {TeraFy} terafy The TeraFy client this plugin is being initalized against
20
19
  * @param {Object} [options] Additional options to mutate behaviour
21
20
  */
22
- constructor(terafy, options) { // eslint-disable-line
21
+ constructor(terafy, options) { // eslint-disable-line no-unused-vars
23
22
  }
24
23
  }
@@ -0,0 +1,122 @@
1
+ import {initializeApp as Firebase} from 'firebase/app';
2
+ import {getFirestore as Firestore} from 'firebase/firestore';
3
+ import {createClient as Supabase} from '@supabase/supabase-js'
4
+ import Syncro from '../lib/syncro.js';
5
+ import TeraFyPluginBase from './base.js';
6
+
7
+ /**
8
+ * Plugin which adds Firebase / Firestore support for namespace mounts
9
+ *
10
+ * @class TeraFyPluginFirebase
11
+ */
12
+ export default class TeraFyPluginFirebase extends TeraFyPluginBase {
13
+
14
+ /**
15
+ * Lookup object of mounted Syncro objects by path
16
+ *
17
+ * @type {Object<Syncro>}
18
+ */
19
+ syncros = {};
20
+
21
+
22
+ /**
23
+ * @interface
24
+ * The Syncro#reactive option to use when creating new Syncro instances
25
+ * This is expected to be overriden by other plugins
26
+ * If falsy the Syncro module will fall back to its internal (POJO only) getReactive() function
27
+ *
28
+ * @name getReactive
29
+ * @type {Function} A reactive function as defined in Syncro
30
+ */
31
+
32
+
33
+ /**
34
+ * Setup Firebase + Firestore + Supabase
35
+ * Default credentials (Firebase + Supabase) will be retrieved from `getCredentials()` unless overriden here
36
+ *
37
+ * @param {Object} options Additional options to mutate behaviour (defaults to the main teraFy settings)
38
+ * @param {String} [options.firebaseApiKey] Firebase API key
39
+ * @param {String} [options.firebaseAuthDomain] Firebase authorized domain
40
+ * @param {String} [options.firebaseProjectId] Firebase project ID
41
+ * @param {String} [options.firebaseAppId] Firebase App ID
42
+ * @param {String} [options.supabaseUrl] Supabase URL
43
+ * @param {String} [options.supabaseKey] Supabase client key
44
+ *
45
+ * @returns {Promise} A Promise which will resolve when the init process has completed
46
+ */
47
+ async init(options) {
48
+ let settings = {
49
+ firebaseApiKey: null,
50
+ firebaseAuthDomain: null,
51
+ firebaseProjectId: null,
52
+ firebaseAppId: null,
53
+ supabaseUrl: null,
54
+ supabaseKey: null,
55
+ ...await this.getCredentials(),
56
+ ...options,
57
+ };
58
+
59
+ let emptyValues = Object.keys(settings).filter(k => k === null);
60
+ if (emptyValues.length > 0)
61
+ throw new Error('Firebase plugin requires mandatory options: ' + emptyValues.join(', '));
62
+
63
+ Syncro.firebase = Firebase({
64
+ apiKey: settings.firebaseApiKey,
65
+ authDomain: settings.firebaseAuthDomain,
66
+ projectId: settings.firebaseProjectId,
67
+ appId: settings.firebaseAppId,
68
+ });
69
+ Syncro.firestore = Firestore(this.firebase);
70
+
71
+ Syncro.supabase = Supabase(settings.supabaseUrl, settings.supabaseKey);
72
+ }
73
+
74
+
75
+ /**
76
+ * Mount the given namespace against `namespaces[name]`
77
+ *
78
+ * @param {'_PROJECT'|String} name The name/Syncro path of the namespace to mount (or '_PROJECT' for the project mountpoint)
79
+ *
80
+ * @returns {Promise} A promise which resolves when the operation has completed
81
+ */
82
+ _mountNamespace(name) {
83
+ let syncro; // The eventually bootstrapped Syncro object
84
+
85
+ return Promise.resolve()
86
+ .then(()=> this.requireProject())
87
+ .then(project => {
88
+ let path = name == '_PROJECT'
89
+ ? `projects::${project.id}`
90
+ : `project_namespaces::${project.id}::${name}`;
91
+
92
+ syncro = this.syncros[name] = new Syncro(path, {
93
+ debug: (...msg) => this.debug(`SYNCRO://${path}`, ...msg),
94
+ getReactive: this.getReactive, // Try to inherit this instances getReactive prop, otherwise Syncro will fall back to its default
95
+ });
96
+
97
+ // Perform the mount action
98
+ return syncro.mount();
99
+ })
100
+ .then(()=> { // Assign local state
101
+ this.namespaces[name] = syncro.value;
102
+ })
103
+ }
104
+
105
+
106
+ /**
107
+ * Unmount the given namespace from `namespaces[name]`
108
+ *
109
+ * @param {String} name The name/Syncro path of the namespace to unmount
110
+ *
111
+ * @returns {Promise} A promise which resolves when the operation has completed
112
+ */
113
+ _unmountNamespace(name) {
114
+ let syncro = this.syncros[name]; // Create local alias for Syncro before we detach it
115
+
116
+ // Detach local state
117
+ delete this.namespaces[name];
118
+ delete this.syncros[name];
119
+
120
+ return syncro.destroy(); // Trigger Syncro distruction
121
+ }
122
+ }