@iebh/tera-fy 1.14.2 → 1.15.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.
@@ -1,5 +1,5 @@
1
1
  import {diff} from 'just-diff';
2
- import {cloneDeep, merge} from 'lodash-es';
2
+ import {cloneDeep} from 'lodash-es';
3
3
  import Mitt from 'mitt';
4
4
  import {nanoid} from 'nanoid';
5
5
  import ProjectFile from './projectFile.js';
@@ -17,11 +17,12 @@ export default class TeraFy {
17
17
  * Various settings to configure behaviour
18
18
  *
19
19
  * @type {Object}
20
+ * @property {String} session Unique session signature for this instance of TeraFy, used to sign server messages, if falsy `getEntropicString(16)` is used to populate
20
21
  * @property {Boolean} devMode Operate in Dev-Mode - i.e. force outer refresh when encountering an existing TeraFy instance
21
22
  * @property {Number} verbosity Verbosity level, the higher the more chatty TeraFY will be. Set to zero to disable all `debug()` call output
22
23
  * @property {'detect'|'parent'|'child'|'popup'} mode How to communicate with TERA. 'parent' assumes that the parent of the current document is TERA, 'child' spawns an iFrame and uses TERA there, 'detect' tries parent and switches to `modeFallback` if communication fails
23
24
  * @property {String} modeFallback Method to use when all method detection fails
24
- * @property {Object<Object<Object>>} modeOverrides Settings to merge when in specific, named modes
25
+ * @property {Object<Object<Function>>} modeOverrides Functions to run when switching to specific modes, these are typically used to augment config. Called as `(config:Object)`
25
26
  * @property {Number} modeTimeout How long entities have in 'detect' mode to identify themselves
26
27
  * @property {String} siteUrl The TERA URL to connect to
27
28
  * @property {String} restrictOrigin URL to restrict communications to
@@ -31,15 +32,19 @@ export default class TeraFy {
31
32
  * @property {Array<String|Array<String>>} [debugPaths] List of paths (in either dotted or array notation) to enter debugging mode if a change is detected in dev mode e.g. `{debugPaths: ['foo.bar.baz']}`. This really slows down state writes so should only be used for debugging
32
33
  */
33
34
  settings = {
35
+ session: null,
34
36
  devMode: true,
35
37
  verbosity: 1,
36
38
  mode: 'detect',
37
39
  modeTimeout: 300,
38
40
  modeFallback: 'child', // ENUM: 'child' (use iframes), 'popup' (use popup windows)
39
41
  modeOverrides: {
40
- child: { // When we're in child mode assume a local dev environment and use the dev.tera-tools.com site instead to work around CORS restrictions
41
- siteUrl: 'https://dev.tera-tools.com/embed',
42
- restrictOrigin: '*',
42
+ child(config) { // When we're in child mode assume a local dev environment and use the dev.tera-tools.com site instead to work around CORS restrictions
43
+ if (config.siteUrl == 'https://tera-tools.com/embed') { // Only if we're using the default URL...
44
+ config.siteUrl = 'https://dev.tera-tools.com/embed'; // Repoint URL to dev site
45
+ } else { // If we're using some weird upstream allow all origins for postMessage
46
+ config.restrictOrigin = '*'; // Allow all upstream iframes
47
+ }
43
48
  },
44
49
  },
45
50
  siteUrl: 'https://tera-tools.com/embed',
@@ -312,6 +317,13 @@ export default class TeraFy {
312
317
  }
313
318
 
314
319
 
320
+ /**
321
+ * Transmit a patch to the remote server
322
+ * This function also enters debugging mode if any of the `settings.debugPaths` are operated on
323
+ *
324
+ * @param {Array} patch Patch to apply
325
+ * @returns {Promise} A promise which resolves when the operation has completed
326
+ */
315
327
  applyProjectStatePatch(patch) {
316
328
  if (this.settings.devMode && this.settings.debugPaths) {
317
329
  if (!Array.isArray(this.settings.debugPaths)) throw new Error('teraFyClient.settings.debugPaths should be either null or an Array<String>');
@@ -328,7 +340,9 @@ export default class TeraFy {
328
340
  }
329
341
  }
330
342
 
331
- return this.rpc('applyProjectStatePatch', patch);
343
+ return this.rpc('applyProjectStatePatch', patch, {
344
+ session: this.settings.session,
345
+ });
332
346
  }
333
347
 
334
348
 
@@ -373,7 +387,8 @@ export default class TeraFy {
373
387
 
374
388
  const context = this;
375
389
  return this.init.promise = Promise.resolve()
376
- .then(()=> this.debug('INFO', 4, '[0/6] Init', this.settings.siteUrl))
390
+ .then(()=> this.settings.session ||= 'tfy-' + this.getEntropicString(16))
391
+ .then(()=> this.debug('INFO', 4, '[0/6] Init', 'Session', this.settings.session, 'against', this.settings.siteUrl))
377
392
  .then(()=> { // Init various options for optimized access
378
393
  if (!this.settings.devMode) return; // Not in dev mode
379
394
  this.settings.debugPaths =
@@ -390,10 +405,12 @@ export default class TeraFy {
390
405
  .then(()=> this.detectMode())
391
406
  .then(mode => {
392
407
  this.debug('INFO', 4, '[1/6] Setting client mode to', mode);
393
- merge(this.settings, {
394
- mode,
395
- ...this.settings.modeOverrides[mode], // Merge specific modeOverrides over the top of settings
396
- });
408
+ this.settings.mode = mode;
409
+
410
+ if (this.settings.modeOverrides[mode]) {
411
+ this.debug('INFO', 4, '[1/6] Applying specific config overrides for mode', mode);
412
+ return this.settings.modeOverrides[mode](this.settings);
413
+ }
397
414
  })
398
415
  .then(()=> this.debug('INFO', 4, '[2/6] Injecting comms + styles + methods'))
399
416
  .then(()=> Promise.all([
@@ -493,7 +510,7 @@ export default class TeraFy {
493
510
 
494
511
  case 'parent':
495
512
  this.debug('INFO', 2, 'Using TERA window parent');
496
- break;
513
+ return Promise.resolve();
497
514
 
498
515
  case 'popup':
499
516
  this.debug('INFO', 2, 'Injecting TERA site as a popup window');
@@ -863,6 +880,22 @@ export default class TeraFy {
863
880
  this.debug('INFO', 2, 'Request focus', {isFocused});
864
881
  globalThis.document.body.classList.toggle('tera-fy-focus', isFocused === 'toggle' ? undefined : isFocused);
865
882
  }
883
+
884
+
885
+
886
+ /**
887
+ * Generate random entropic character string in Base64
888
+ *
889
+ * @param {Number} [maxLength=32] Maximum lengh of the genrated string
890
+ * @returns {String}
891
+ */
892
+ getEntropicString(maxLength = 32) {
893
+ const array = new Uint32Array(4);
894
+ window.crypto.getRandomValues(array);
895
+ return btoa(String.fromCharCode(...new Uint8Array(array.buffer)))
896
+ .replace(/[+/]/g, '') // Remove + and / characters
897
+ .slice(0, maxLength) // Trim
898
+ }
866
899
  // }}}
867
900
 
868
901
  // Stub documentation carried over from ./terafy.server.js {{{
@@ -1341,6 +1374,14 @@ export default class TeraFy {
1341
1374
  */
1342
1375
 
1343
1376
 
1377
+ /**
1378
+ * Trigger a fatal error, killing the outer TERA site
1379
+ *
1380
+ * @function uiDie
1381
+ * @param {String} [text] Text to display
1382
+ */
1383
+
1384
+
1344
1385
  /**
1345
1386
  * Display, update or dispose of windows for long running tasks
1346
1387
  * All options are cumulative - i.e. they are merged with other options previously provided
@@ -878,12 +878,29 @@ export default class TeraFyServer {
878
878
  * Apply a computed `just-diff` patch to the current project state
879
879
  *
880
880
  * @param {Object} patch Patch to apply
881
+ *
882
+ * @param {Object} [options] Additional options to mutate behaviour
883
+ * @param {String} [options.session] The transmitting session, if available. Avoids that session recieving a patch downwards after this one
884
+ *
881
885
  * @returns {Promise} A promise which resolves when the operation has completed
882
886
  */
883
- applyProjectStatePatch(patch) {
887
+ applyProjectStatePatch(patch, options) {
884
888
  if (!app.service('$projects').active) throw new Error('No active project to patch');
885
889
  this.debug('INFO', 1, 'Applying', patch.length, 'project state patch', patch);
886
- diffApply(app.service('$projects').active, patch);
890
+
891
+ let $projects = app.service('$projects');
892
+ diffApply($projects.active, patch);
893
+
894
+ // Populate information about the last patch
895
+ $projects.active.lastPatch = {
896
+ date: (new Date).toJSON(),
897
+ user: app.service('$auth').user.id,
898
+ ...(options?.session && { // Have session info?
899
+ session: options.session,
900
+ }),
901
+ patches: patch.length,
902
+ };
903
+ this.debug('INFO', 3, 'Applied patch to lastPatch status', $projects.active.lastPatch);
887
904
 
888
905
  return Promise.resolve();
889
906
  }
@@ -1504,6 +1521,17 @@ export default class TeraFyServer {
1504
1521
  }
1505
1522
 
1506
1523
 
1524
+ /**
1525
+ * Trigger a fatal error, killing the outer TERA site
1526
+ *
1527
+ * @function uiDie
1528
+ * @param {String} [text] Text to display
1529
+ */
1530
+ uiDie(text) {
1531
+ window.die(text);
1532
+ }
1533
+
1534
+
1507
1535
  /**
1508
1536
  * Display, update or dispose of windows for long running tasks
1509
1537
  * All options are cumulative - i.e. they are merged with other options previously provided
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iebh/tera-fy",
3
- "version": "1.14.2",
3
+ "version": "1.15.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=.",
package/plugins/vue2.js CHANGED
@@ -1,4 +1,4 @@
1
- import {cloneDeep, isPlainObject} from 'lodash-es';
1
+ import {cloneDeep, debounce, isPlainObject} from 'lodash-es';
2
2
  import TeraFyPluginBase from './base.js';
3
3
 
4
4
  /**
@@ -45,7 +45,9 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
45
45
  * @param {String} [options.componentKey] Key within the component to attach the state. Defaults to a random string
46
46
  * @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
47
47
  * @param {String|Boolean} [options.bindKey='project'] If set, creates the binding also as the specified key within the main Tera object, if falsy just returns the observable
48
+ * @param {Boolean} [options.read=true] Allow remote reactivity - update the local state when the server changes
48
49
  * @param {Boolean} [options.write=true] Allow local reactivity to writes - send these to the server
50
+ * @param {Object} [options.throttle] Lodash debounce options + `wait` key used to throttle all writes, set to falsy to disable
49
51
  *
50
52
  * @returns {Promie<VueObservable<Object>>} A Vue.Observable object representing the project state
51
53
  */
@@ -54,7 +56,14 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
54
56
  component: null,
55
57
  componentKey: null,
56
58
  autoRequire: true,
59
+ read: true,
57
60
  write: true,
61
+ throttle: {
62
+ wait: 200,
63
+ maxWait: 2000,
64
+ leading: false,
65
+ trailing: true,
66
+ },
58
67
  ...options,
59
68
  };
60
69
 
@@ -89,11 +98,17 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
89
98
  settings.component[settings.componentKey] = stateObservable;
90
99
 
91
100
  // Watch for remote changes and update
92
- let skipUpdate = 0; // How many subsequent WRITE operations to ignore (set when reading)
93
101
  if (settings.read) {
94
102
  this.events.on(`update:projects/${stateObservable.id}`, newState => {
95
- skipUpdate++; // Skip next update as we're updating our own state anyway
103
+ if (
104
+ newState?.lastPatch?.session // Last state change had a session worth noting
105
+ && newState.lastPatch.session == this.settings.session // The last state update was made FROM INSIDE THE BUILDING! BUWHAHAHA!
106
+ )
107
+ return; // Discard it, we don't care
108
+
109
+ // Everything else - patch the remote state locally
96
110
  this.debug('INFO', 5, 'Update Vue2 Remote->Local', {new: newState, old: stateObservable});
111
+
97
112
  this.merge(stateObservable, newState);
98
113
  });
99
114
  }
@@ -107,19 +122,19 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
107
122
  // snapshot
108
123
  let oldVal = cloneDeep(snapshot);
109
124
 
125
+ // Function to handle the state update (can be debounced)
126
+ let watchHandle = newVal => {
127
+ this.debug('INFO', 5, 'Update Vue2 Local->Remote', {new: newVal, old: oldVal});
128
+ this.createProjectStatePatch(newVal, oldVal);
129
+ oldVal = cloneDeep(snapshot);
130
+ };
131
+
110
132
  settings.component.$watch(
111
- settings.componentKey,
112
- newVal => {
113
- if (skipUpdate > 0) {
114
- skipUpdate--;
115
- return;
116
- }
117
-
118
- this.debug('INFO', 5, 'Update Vue2 Local->Remote', {new: newVal, old: oldVal});
119
- this.createProjectStatePatch(newVal, oldVal);
120
- oldVal = cloneDeep(snapshot);
121
- },
122
- {
133
+ settings.componentKey, // State to watch
134
+ settings.throttle // Pointer to watchHandle which takes the new state (optionally throttled)
135
+ ? debounce(watchHandle, settings.throttle.wait, settings.throttle)
136
+ : watchHandle,
137
+ { // Watch options
123
138
  deep: true,
124
139
  },
125
140
  );
package/plugins/vue3.js CHANGED
@@ -1,4 +1,4 @@
1
- import {cloneDeep} from 'lodash-es';
1
+ import {cloneDeep, debounce} from 'lodash-es';
2
2
  import TeraFyPluginBase from './base.js';
3
3
  import {reactive, watch} from 'vue';
4
4
 
@@ -33,6 +33,7 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
33
33
  * @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
34
34
  * @param {Boolean} [options.read=true] Allow remote reactivity - update the local state when the server changes
35
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
36
37
  *
37
38
  * @returns {Promie<Reactive<Object>>} A reactive object representing the project state
38
39
  */
@@ -41,6 +42,12 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
41
42
  autoRequire: true,
42
43
  read: true,
43
44
  write: true,
45
+ throttle: {
46
+ wait: 200,
47
+ maxWait: 2000,
48
+ leading: false,
49
+ trailing: true,
50
+ },
44
51
  ...options,
45
52
  };
46
53
 
@@ -53,10 +60,15 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
53
60
  let stateReactive = reactive(snapshot);
54
61
 
55
62
  // Watch for remote changes and update
56
- let skipUpdate = 0; // How many subsequent WRITE operations to ignore (set when reading)
57
63
  if (settings.read) {
58
64
  this.events.on(`update:projects/${stateReactive.id}`, newState => {
59
- skipUpdate++; // Skip next update as we're updating our own state anyway
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
60
72
  Object.assign(stateReactive, newState);
61
73
  });
62
74
  }
@@ -69,18 +81,18 @@ export default class TeraFyPluginVue extends TeraFyPluginBase {
69
81
  // snapshot
70
82
  let oldVal = cloneDeep(snapshot);
71
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
+
72
90
  watch(
73
- stateReactive,
74
- newVal => {
75
- if (skipUpdate > 0) {
76
- skipUpdate--;
77
- return;
78
- }
79
-
80
- this.createProjectStatePatch(newVal, oldVal);
81
- oldVal = cloneDeep(newVal); // Update old state the the last seen value
82
- },
83
- {
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
84
96
  deep: true,
85
97
  },
86
98
  );