@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.
package/plugins/vue2.js CHANGED
@@ -1,17 +1,14 @@
1
- import {cloneDeep, debounce, isPlainObject} from 'lodash-es';
2
- import TeraFyPluginBase from './base.js';
1
+ import {cloneDeep} from 'lodash-es';
2
+ import TeraFyPluginFirebase from './firebase.js';
3
3
 
4
4
  /**
5
- * Vue2 observables plugin
6
- * Provides the `bindProjectState()` function for Vue based projects
5
+ * Vue@2 observables plugin
7
6
  *
8
7
  * This function is expected to be included via the `terafy.use(MODULE, OPTIONS)` syntax rather than directly
9
8
  *
10
- * @class TeraFyPluginVue
11
- * @param {Object} options Options when initalizing
12
- * @param {Vue} options.Vue Vue instance to bind against
9
+ * @class TeraFyPluginVue2
13
10
  *
14
- * @example Implementation within a Vue2 project `src/main.js`:
11
+ * @example Implementation within a Vue@2 project `src/main.js`:
15
12
  * // Include the main Tera-Fy core
16
13
  * import TeraFy from '@iebh/tera-fy';
17
14
  * import TerafyVue from '@iebh/tera-fy/plugins/vue2';
@@ -27,7 +24,7 @@ import TeraFyPluginBase from './base.js';
27
24
  * app.$mount("#app");
28
25
  * await terafy.init({app});
29
26
  */
30
- export default class TeraFyPluginVue2 extends TeraFyPluginBase {
27
+ export default class TeraFyPluginVue2 extends TeraFyPluginFirebase {
31
28
 
32
29
  /**
33
30
  * Local Vue@2 library to use, set during constuctor
@@ -37,6 +34,82 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
37
34
  Vue;
38
35
 
39
36
 
37
+ /**
38
+ * The bound, reactive state of the active TERA project
39
+ *
40
+ * @type {Object}
41
+ */
42
+ project = null;
43
+
44
+
45
+ /**
46
+ * Simple incrementor to ensure unique IDs for $watch expressions
47
+ *
48
+ * @type {Number{
49
+ */
50
+ reactiveId = 1001;
51
+
52
+
53
+ /**
54
+ * Install into Vue@2
55
+ *
56
+ * @param {Object} options Additional options to mutate behaviour, see TeraFyPluginFirebase
57
+ * @param {Object} options.app Root level Vue app to bind against
58
+ * @param {Vue} options.Vue Vue@2 instance to bind against
59
+ * @param {String} [options.globalName='$tera'] Global property to allocate this service as within Vue2
60
+ * @param {*...} [options...] see TeraFyPluginFirebase
61
+ *
62
+ * @returns {Promise} A Promise which will resolve when the init process has completed
63
+ */
64
+ async init(options) {
65
+ let settings = {
66
+ app: null,
67
+ Vue: null,
68
+ globalName: '$tera',
69
+ ...options,
70
+ };
71
+
72
+ if (!settings.Vue) throw new Error('Vue instance to use must be specified in init options as `Vue`');
73
+ this.Vue = settings.Vue;
74
+
75
+ if (!settings.app) throw new Error('Vue Root / App instance to use must be specified in init options as `app`');
76
+ this.app = settings.app;
77
+
78
+ // Make this module available globally
79
+ if (settings.globalName)
80
+ this.Vue.prototype[settings.globalName] = this;
81
+
82
+ await super.init(settings); // Initalize parent class Firebase functionality
83
+
84
+ this.project = await this.mountNamespace('_PROJECT');
85
+ }
86
+
87
+
88
+ /** @override */
89
+ getReactive(value) {
90
+ let doc = this.Vue.observable(value);
91
+
92
+ let watcherPath = `_teraFy_${this.reactiveId++}`;
93
+ this.app[watcherPath] = doc; // Attach onto app so we can use $watch later on
94
+
95
+ return {
96
+ doc,
97
+ setState(state) {
98
+ // Shallow copy all sub-keys into existing object (keeping the object pointer)
99
+ Object.entries(state || {})
100
+ .forEach(([k, v]) => doc[k] = v)
101
+ },
102
+ getState() {
103
+ return cloneDeep(doc);
104
+ },
105
+ watch: cb => {
106
+ this.app.$watch(watcherPath, cb, {deep: true});
107
+ },
108
+ };
109
+ }
110
+
111
+
112
+
40
113
  /**
41
114
  * Return a Vue Observable object that can be read/written which whose changes will transparently be written back to the TERA server instance
42
115
  *
@@ -123,10 +196,14 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
123
196
  let oldVal = cloneDeep(snapshot);
124
197
 
125
198
  // Function to handle the state update (can be debounced)
126
- let watchHandle = newVal => {
199
+ let watchHandle = ()=> {
200
+ let newVal = cloneDeep(snapshot);
201
+
127
202
  this.debug('INFO', 5, 'Update Vue2 Local->Remote', {new: newVal, old: oldVal});
128
203
  this.createProjectStatePatch(newVal, oldVal);
129
- oldVal = cloneDeep(snapshot);
204
+
205
+ // Set oldVal to the deep clone we just made so we can track the diff
206
+ oldVal = newVal;
130
207
  };
131
208
 
132
209
  settings.component.$watch(
@@ -171,89 +248,4 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
171
248
  return target;
172
249
  }
173
250
 
174
-
175
-
176
- /**
177
- * List of available projects for the current session
178
- * Initalized during constructor
179
- *
180
- * @type {VueReactive<Array<Object>>}
181
- */
182
- projects;
183
-
184
-
185
- /**
186
- * The bound, reactive state of a Vue project
187
- * When loaded this represents the state of a project as an object
188
- *
189
- * @type {Object}
190
- */
191
- state = null;
192
-
193
-
194
- /**
195
- * Install into Vue@2
196
- *
197
- * @param {Object} options Additional options to mutate behaviour (defaults to the main teraFy settings)
198
- * @param {Object} options.app Root level Vue app to bind against
199
- * @param {Vue} options.Vue Vue@2 instance to bind against
200
- * @param {String} [options.globalName='$tera'] Global property to allocate this service as within Vue2
201
- * @param {Boolean} [options.requireProject=true] Automatically call requireProject() prior to any operation
202
- * @param {Boolean} [options.subscribeState=true] Setup `vm.$tera.state` as a live binding on init
203
- * @param {Boolean} [options.subscribeList=true] Setup `vm.$tera.projects` as a list of accesible projects on init
204
- * @param {Objecct} [options.stateOptions] Options passed to `bindProjectState()` when setting up the main state
205
- *
206
- * @returns {Promise} A Promise which will resolve when the init process has completed
207
- */
208
- init(options) {
209
- let settings = {
210
- app: null,
211
- Vue: null,
212
- globalName: '$tera',
213
- requireProject: true,
214
- subscribeState: true,
215
- subscribeProjects: true,
216
- stateOptions: {
217
- read: true,
218
- write: true,
219
- },
220
- ...options,
221
- };
222
-
223
- if (!settings.Vue) throw new Error('Vue instance to use must be specified in init options as `Vue`');
224
- this.Vue = options.Vue;
225
-
226
- if (!this.settings.app) throw new Error('Need to specify the root level Vue2 app during init');
227
- settings.stateOptions.app = this.settings.app;
228
-
229
- // Create observable binding for projects
230
- this.projects = this.Vue.observable([])
231
-
232
- // Make this module available globally
233
- if (settings.globalName)
234
- this.Vue.prototype[settings.globalName] = this;
235
-
236
- // Bind `state` to the active project
237
- // Initialize state to null
238
- this.state = null;
239
-
240
- // this.statePromisable becomes the promise we are waiting on to resolve
241
- return Promise.resolve()
242
- .then(()=> settings.requireProject && this.requireProject())
243
- .then(()=> Promise.all([
244
- // Bind available project and wait on it
245
- settings.subscribeState && this.bindProjectState({
246
- ...settings.stateOptions,
247
- component: this.settings.app.$root,
248
- })
249
- .then(state => this.state = state)
250
- .then(()=> this.debug('INFO', 1, 'Loaded initial project state', this.state)),
251
-
252
- // Fetch available projects
253
- settings.subscribeProjects && this.getProjects()
254
- .then(projects => this.projects = this.Vue.observable(projects))
255
- .then(()=> this.debug('INFO', 2, 'Loaded project list', this.projects)),
256
- ]))
257
- .then(()=> this.debug('INFO', 1, 'Ready'))
258
- }
259
251
  }
package/plugins/vue3.js CHANGED
@@ -1,14 +1,13 @@
1
- import {cloneDeep, debounce} from 'lodash-es';
2
- import TeraFyPluginBase from './base.js';
3
- import {reactive, watch} from 'vue';
1
+ import {cloneDeep} from 'lodash-es';
2
+ import TeraFyPluginFirebase from './firebase.js';
3
+ import {markRaw, reactive as vueReactive, watch as vueWatch} from 'vue';
4
4
 
5
5
  /**
6
6
  * Vue observables plugin
7
- * Provides the `bindProjectState()` function for Vue based projects
8
7
  *
9
8
  * This function is expected to be included via the `terafy.use(MODULE, OPTIONS)` syntax rather than directly
10
9
  *
11
- * @class TeraFyPluginVue
10
+ * @class TeraFyPluginVue3
12
11
  *
13
12
  * @example Implementation within a Vue3 / Vite project within `src/main.js`:
14
13
  * import TeraFy from '@iebh/tera-fy';
@@ -23,120 +22,51 @@ import {reactive, watch} from 'vue';
23
22
  * app.use(terafy.vuePlugin({
24
23
  * globalName: '$tera', // Install as vm.$tera into every component
25
24
  * }));
25
+ *
26
+ * @example Accessing project state - within a Vue component
27
+ * this.$tera.active
26
28
  */
27
- export default class TeraFyPluginVue extends TeraFyPluginBase {
29
+ export default class TeraFyPluginVue3 extends TeraFyPluginFirebase {
28
30
 
29
31
  /**
30
- * Return a Vue reactive object that can be read/written which whose changes will transparently be written back to the TERA server instance
31
- *
32
- * @param {Object} [options] Additional options to mutate behaviour
33
- * @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
34
- * @param {Boolean} [options.read=true] Allow remote reactivity - update the local state when the server changes
35
- * @param {Boolean} [options.write=true] Allow local reactivity to writes - send these to the server
36
- * @param {Object} [options.throttle] Lodash debounce options + `wait` key used to throttle all writes, set to falsy to disable
32
+ * The bound, reactive state of the active TERA project
37
33
  *
38
- * @returns {Promie<Reactive<Object>>} A reactive object representing the project state
39
- */
40
- bindProjectState(options) {
41
- let settings = {
42
- autoRequire: true,
43
- read: true,
44
- write: true,
45
- throttle: {
46
- wait: 200,
47
- maxWait: 2000,
48
- leading: false,
49
- trailing: true,
50
- },
51
- ...options,
52
- };
53
-
54
- return Promise.resolve()
55
- .then(()=> this.getProjectState({
56
- autoRequire: settings.autoRequire ,
57
- }))
58
- .then(snapshot => {
59
- // Create initial reactive
60
- let stateReactive = reactive(snapshot);
61
-
62
- // Watch for remote changes and update
63
- if (settings.read) {
64
- this.events.on(`update:projects/${stateReactive.id}`, newState => {
65
- if (
66
- newState?.lastPatch?.session // Last state change had a session worth noting
67
- && newState.lastPatch.session == this.settings.session // The last state update was made FROM INSIDE THE BUILDING! BUWHAHAHA!
68
- )
69
- return; // Discard it, we don't care
70
-
71
- // Everything else - patch the remote state locally
72
- Object.assign(stateReactive, newState);
73
- });
74
- }
75
-
76
- // Watch for local writes and react
77
- if (settings.write) {
78
-
79
- // NOTE: The below watch function returns two copies of the new value of the observed
80
- // so we have to keep track of what changed ourselves by initalizing against the
81
- // snapshot
82
- let oldVal = cloneDeep(snapshot);
83
-
84
- // Function to handle the state update (can be debounced)
85
- let watchHandle = newVal => {
86
- this.createProjectStatePatch(newVal, oldVal);
87
- oldVal = cloneDeep(newVal); // Update old state the the last seen value
88
- };
89
-
90
- watch(
91
- stateReactive, // State to watch
92
- settings.throttle // Pointer to watchHandle which takes the new state (optionally throttled)
93
- ? debounce(watchHandle, settings.throttle.wait, settings.throttle)
94
- : watchHandle,
95
- { // Watch options
96
- deep: true,
97
- },
98
- );
99
- }
100
-
101
- // Return Vue Reactive
102
- return stateReactive;
103
- })
104
- }
105
-
106
-
107
- /**
108
- * List of available projects for the current session
109
- * @type {VueReactive<Array<Object>>}
110
- */
111
- projects = reactive([]);
112
-
113
-
114
- /**
115
- * The bound, reactive state of a Vue project
116
- * When loaded this represents the state of a project as an object
117
34
  * @type {Object}
118
35
  */
119
- state = null;
36
+ project = null;
120
37
 
121
38
 
122
39
  /**
123
- * Promise used when binding to state
124
- * @type {Promise}
40
+ * Init the project including create a reactive mount for the active project
41
+ *
42
+ * @param {Object} options Additional options to mutate behaviour
43
+ * @param {*...} [options...] see TeraFyPluginFirebase
125
44
  */
126
- statePromisable = null;
45
+ async init(options) {
46
+ await super.init(options); // Initalize parent class Firebase functionality
127
47
 
48
+ // Mount the project namespace
49
+ this.project = await this.mountNamespace('_PROJECT');
50
+ }
128
51
 
129
- /**
130
- * Utility function which returns an awaitable promise when the state is loading or being refreshed
131
- * This is used in place of `statePromisable` as it has a slightly more logical syntax as a function
132
- *
133
- * @returns {Promise} A promise representing the loading of the project state
134
- *
135
- * @example Await the state loading
136
- * await $tera.statePromise();
137
- */
138
- statePromise() {
139
- return this.statePromisable;
52
+
53
+ /** @override */
54
+ getReactive(value) {
55
+ let doc = vueReactive(value);
56
+ return {
57
+ doc,
58
+ setState(state) {
59
+ // Shallow copy all sub-keys into existing object (keeping the object pointer)
60
+ Object.entries(state || {})
61
+ .forEach(([k, v]) => doc[k] = v)
62
+ },
63
+ getState() {
64
+ return cloneDeep(doc);
65
+ },
66
+ watch(cb) {
67
+ vueWatch(doc, cb, {deep: true});
68
+ },
69
+ };
140
70
  }
141
71
 
142
72
 
@@ -156,43 +86,14 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
156
86
  * @param {VueApp} app The Vue top-level app to install against
157
87
  *
158
88
  * @param {Object} [options] Additional options to mutate behaviour
159
- * @param {Boolean} [options.autoInit=true] Call Init() during the `statePromiseable` cycle if its not already been called
160
- * @param {String} [options.globalName='$tera'] Globa property to allocate this service as
161
- * @param {Objecct} [options.bindOptions] Options passed to `bindProjectState()`
89
+ * @param {String} [options.globalName='$tera'] Global property to allocate this service as
162
90
  */
163
91
  install(app, options) {
164
92
  let settings = {
165
- autoInit: true,
166
93
  globalName: '$tera',
167
- subscribeState: true,
168
- subscribeProjects: true,
169
- stateOptions: {
170
- write: true,
171
- },
172
94
  ...options,
173
95
  };
174
96
 
175
- // Bind $tera.state to the active project
176
- // Initialize state to null
177
- $tera.state = null;
178
-
179
- // $tera.statePromisable becomes the promise we are waiting on to resolve
180
- $tera.statePromisable = Promise.resolve()
181
- .then(()=> settings.autoInit && $tera.init())
182
- .then(()=> Promise.all([
183
- // Bind available project and wait on it
184
- settings.subscribeState && $tera.bindProjectState(settings.stateOptions)
185
- .then(state => $tera.state = state)
186
- .then(()=> $tera.debug('INFO', 1, 'Loaded initial project state', $tera.state)),
187
-
188
- // Fetch available projects
189
- settings.subscribeProjects && $tera.getProjects()
190
- .then(projects => $tera.projects = reactive(projects))
191
- .then(()=> $tera.debug('INFO', 2, 'Loaded project list', $tera.projects)),
192
- ]))
193
- .then(()=> $tera.debug('INFO', 1, 'Ready'))
194
-
195
-
196
97
  // Make this module available globally
197
98
  app.config.globalProperties[settings.globalName] = $tera;
198
99
  },