@iebh/tera-fy 1.5.0 → 1.6.1

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.
@@ -101,6 +101,8 @@ export default class TeraFy {
101
101
  'getProjectState',
102
102
  'setProjectState',
103
103
  'setProjectStateDefaults',
104
+ 'setProjectStateFlush',
105
+ 'setProjectStateRefresh',
104
106
  'saveProjectState',
105
107
  'replaceProjectState',
106
108
  'applyProjectStatePatch',
@@ -331,25 +333,34 @@ export default class TeraFy {
331
333
 
332
334
  const context = this;
333
335
  return this.init.promise = Promise.resolve()
336
+ .then(()=> this.debug('INFO', 4, '[0/5] Init using server', this.settings.siteUrl))
334
337
  .then(()=> this.detectMode())
335
- .then(mode => this.settings.mode = mode)
338
+ .then(mode => {
339
+ this.debug('INFO', 4, '[1/5] Setting client mode to', mode);
340
+ this.settings.mode = mode;
341
+ })
342
+ .then(()=> this.debug('INFO', 4, '[2/5] Injecting comms + styles + methods'))
336
343
  .then(()=> Promise.all([
337
344
  // Init core functions async
338
345
  this.injectComms(),
339
346
  this.injectStylesheet(),
340
347
  this.injectMethods(),
341
348
  ]))
349
+ .then(()=> this.debug('INFO', 4, '[3/5] Set server mode'))
342
350
  .then(()=> this.rpc('setServerMode', // Tell server what mode its in
343
351
  this.settings.mode == 'child' ? 'embedded'
344
352
  : this.settings.mode == 'parent' ? 'frame'
345
353
  : this.settings.mode == 'popup' ? 'popup'
346
354
  : (()=> { throw(`Unknown server mode "${this.settings.mode}"`) })()
347
355
  ))
356
+ .then(()=> this.debug('INFO', 4, '[4/5] Run client plugins'))
348
357
  .then(()=> Promise.all( // Init all plugins (with this outer module as the context)
349
358
  this.plugins.map(plugin =>
350
359
  plugin.init.call(context, this.settings)
351
360
  )
352
361
  ))
362
+ .then(()=> this.debug('INFO', 4, '[5/5] Init complete'))
363
+ .catch(e => this.debug('WARN', 0, 'Init caught error', e))
353
364
  }
354
365
 
355
366
 
@@ -816,6 +827,24 @@ export default class TeraFy {
816
827
  */
817
828
 
818
829
 
830
+ /**
831
+ * Force copying local changes to the server
832
+ * This is only ever needed when saving large quantities of data that need to be immediately available
833
+ *
834
+ * @function setProjectStateFlush
835
+ * @returns {Promise} A promise which resolves when the operation has completed
836
+ */
837
+
838
+
839
+ /**
840
+ * Force refetching the remote project state into local
841
+ * This is only ever needed when saving large quantities of data that need to be immediately available
842
+ *
843
+ * @function setProjectStateRefresh
844
+ * @returns {Promise} A promise which resolves when the operation has completed
845
+ */
846
+
847
+
819
848
  /**
820
849
  * Force-Save the currently active project state
821
850
  *
@@ -918,11 +947,11 @@ export default class TeraFy {
918
947
  /**
919
948
  * Fetch a project file
920
949
  *
921
- * @param {String} path File path to read
950
+ * @param {String} id File ID to read
922
951
  * @returns {Promise<Blob>} The eventual fetched file as a blob
923
952
  */
924
- getProjectFile(path) {
925
- return this.rpc('getProjectFile', path)
953
+ getProjectFile(id) {
954
+ return this.rpc('getProjectFile', id)
926
955
  .then(file => file
927
956
  ? new ProjectFile({
928
957
  tera: this,
@@ -937,7 +966,7 @@ export default class TeraFy {
937
966
  * Replace a project files contents
938
967
  *
939
968
  * @function setProjectFile
940
- * @param {String} path File path to write
969
+ * @param {String} id File to overwrite
941
970
  * @param {File|Blob|FormData|Object|Array} contents The new file contents
942
971
  * @returns {Promise} A promise which will resolve when the write operation has completed
943
972
  */
@@ -966,7 +995,7 @@ export default class TeraFy {
966
995
  * Fetch + convert a project file into a library of citations
967
996
  *
968
997
  * @function getProjectLibrary
969
- * @param {String} path File path to read, if omitted the contents of `options` are used to guess at a suitable file
998
+ * @param {String} id File ID to read
970
999
  *
971
1000
  * @param {Object} [options] Additional options to mutate behaviour
972
1001
  * @param {String} [options.format='json'] Format for the file. ENUM: 'pojo' (return a parsed JS collection), 'blob' (raw JS Blob object), 'file' (named JS File object)
@@ -982,17 +1011,17 @@ export default class TeraFy {
982
1011
  * Save back a citation library from some input
983
1012
  *
984
1013
  * @function setProjectLibrary
985
- * @param {String} [path] File path to save back to, if omitted one will be prompted for
1014
+ * @param {String} [id] File ID to save back to, if omitted a file will be prompted for
986
1015
  * @param {Array<RefLibRef>|Blob|File} [refs] Collection of references for the selected library or the raw Blob/File
987
1016
  *
988
1017
  * @param {Object} [options] Additional options to mutate behaviour
989
- * @param {String} [options.path] Alternate method to specify the path to save as, if omitted one will be prompted for
1018
+ * @param {String} [options.id] Alternate method to specify the file ID to save as, if omitted one will be prompted for
990
1019
  * @param {Array<RefLibRef>|Blob|File} [options.refs] Alternate method to specify the refs to save as an array or raw Blob/File
991
1020
  * @param {String} [options.format='json'] Input format used. ENUM: 'pojo' (return a parsed JS collection), 'blob' (raw JS Blob object), 'file' (named JS File object)
992
1021
  * @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
993
1022
  * @param {String} [options.hint] Hint to store against the library. Generally corresponds to the current operation being performed - e.g. 'deduped'
994
- * @param {String} [options.filename] Suggested filename if path is unspecified
995
- * @param {String} [options.title='Save citation library'] Dialog title if path is unspecified and we need to prompt
1023
+ * @param {String} [options.filename] Suggested filename if `id` is unspecified
1024
+ * @param {String} [options.title='Save citation library'] Dialog title if `id` is unspecified and a prompt is necessary
996
1025
  * @param {Boolean} [options.overwrite=true] Allow existing file upsert
997
1026
  * @param {Object} [options.meta] Optional meta data to merge into the file data
998
1027
  *
@@ -757,10 +757,15 @@ export default class TeraFyServer {
757
757
  * @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
758
758
  * @param {*} value The value to set as the default structure
759
759
  * @param {Object} [options] Additional options to mutate behaviour, see setProjectState() for the full list of supported options
760
+ * @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
760
761
  *
761
762
  * @returns {Promise<*>} A promise which resolves to the eventual input value after defaults have been applied
762
763
  */
763
764
  setProjectStateDefaults(path, value, options) {
765
+ let settings = {
766
+ flush: true,
767
+ ...options,
768
+ };
764
769
  if (!app.service('$projects').active) throw new Error('No active project');
765
770
 
766
771
  let target = app.service('$projects').active;
@@ -771,18 +776,52 @@ export default class TeraFyServer {
771
776
  value,
772
777
  {
773
778
  strategy: 'defaults',
774
- ...options,
779
+ ...settings,
775
780
  },
776
781
  )
782
+ .then(()=> settings.flush && this.setProjectStateFlush())
777
783
  .then(()=> pathTools.get(target, path));
778
784
  } else { // Called as (value) - Populate entire project layout
779
785
  pathTools.defaults(target, path);
786
+ this.debug('INFO', 1, 'setProjectStateDefaults', {
787
+ defaults: path,
788
+ newState: cloneDeep(target),
789
+ });
780
790
  return this.saveProjectState()
791
+ .then(()=> settings.flush && this.setProjectStateFlush())
781
792
  .then(()=> target);
782
793
  }
783
794
  }
784
795
 
785
796
 
797
+ /**
798
+ * Force copying local changes to the server
799
+ * This is only ever needed when saving large quantities of data that need to be immediately available
800
+ *
801
+ * @returns {Promise} A promise which resolves when the operation has completed
802
+ */
803
+ setProjectStateFlush() {
804
+ this.debug('INFO', 1, 'Force project state flush!');
805
+ if (!app.service('$projects').active) throw new Error('No active project');
806
+ return app.service('$projects').active.$touchLocal()
807
+ .then(()=> null)
808
+ }
809
+
810
+
811
+ /**
812
+ * Force refetching the remote project state into local
813
+ *
814
+ * @returns {Promise} A promise which resolves when the operation has completed
815
+ */
816
+ setProjectStateRefresh() {
817
+ this.debug('INFO', 1, 'Force project state refresh!');
818
+ if (!app.service('$projects').active) throw new Error('No active project');
819
+ return app.service('$projects').active.$read({force: true})
820
+ .then(()=> this.debug('INFO', 2, 'Forced project state refresh!', {state: app.service('$projects').active}))
821
+ .then(()=> null)
822
+ }
823
+
824
+
786
825
  /**
787
826
  * Force-Save the currently active project state
788
827
  *
@@ -951,11 +990,11 @@ export default class TeraFyServer {
951
990
 
952
991
  /**
953
992
  * Fetch a project file
954
- * @param {String} path File path to read
993
+ * @param {String} id File ID to read
955
994
  * @returns {Promise<Blob>} The eventual fetched file as a blob
956
995
  */
957
- getProjectFile(path) {
958
- return app.service('$supabase').fileGet(path, {
996
+ getProjectFile(id) {
997
+ return app.service('$supabase').fileGet(app.service('$projects').decodeFilePath(id), {
959
998
  toast: false,
960
999
  });
961
1000
  }
@@ -964,12 +1003,12 @@ export default class TeraFyServer {
964
1003
  /**
965
1004
  * Replace a project files contents
966
1005
  *
967
- * @param {String} path File path to write
1006
+ * @param {String} id File to overwrite
968
1007
  * @param {File|Blob|FormData|Object|Array} contents The new file contents
969
1008
  * @returns {Promise} A promise which will resolve when the write operation has completed
970
1009
  */
971
- setProjectFile(path, contents) {
972
- return app.service('$supabase').fileSet(path, contents, {
1010
+ setProjectFile(id, contents) {
1011
+ return app.service('$supabase').fileSet(app.service('$projects').decodeFilePath(id), contents, {
973
1012
  overwrite: true,
974
1013
  toast: false,
975
1014
  });
@@ -1019,7 +1058,7 @@ export default class TeraFyServer {
1019
1058
  /**
1020
1059
  * Fetch + convert a project file into a library of citations
1021
1060
  *
1022
- * @param {String} path File path to read, if omitted the contents of `options` are used to guess at a suitable file
1061
+ * @param {String} id File ID to read
1023
1062
  *
1024
1063
  * @param {Object} [options] Additional options to mutate behaviour
1025
1064
  * @param {String} [options.format='json'] Format for the file. ENUM: 'pojo' (return a parsed JS collection), 'blob' (raw JS Blob object), 'file' (named JS File object)
@@ -1029,7 +1068,7 @@ export default class TeraFyServer {
1029
1068
  *
1030
1069
  * @returns {Promise<Array<Ref>>|Promise<*>} A collection of references (default bevahiour) or a whatever format was requested
1031
1070
  */
1032
- getProjectLibrary(path, options) {
1071
+ getProjectLibrary(id, options) {
1033
1072
  let settings = {
1034
1073
  format: 'pojo',
1035
1074
  autoRequire: true,
@@ -1038,9 +1077,11 @@ export default class TeraFyServer {
1038
1077
  ...options,
1039
1078
  };
1040
1079
 
1080
+ let filePath = app.service('$projects').decodeFilePath(id);
1081
+
1041
1082
  return Promise.resolve()
1042
1083
  .then(()=> settings.autoRequire && this.requireProject())
1043
- .then(()=> app.service('$supabase').fileGet(path, {
1084
+ .then(()=> app.service('$supabase').fileGet(filePath, {
1044
1085
  toast: false,
1045
1086
  }))
1046
1087
  .then(blob => {
@@ -1050,7 +1091,7 @@ export default class TeraFyServer {
1050
1091
  return Reflib.uploadFile({
1051
1092
  file: new File(
1052
1093
  [blob],
1053
- app.service('$supabase')._parsePath(path).basename,
1094
+ app.service('$supabase')._parsePath(filePath).basename,
1054
1095
  ),
1055
1096
  });
1056
1097
  case 'blob':
@@ -1058,7 +1099,7 @@ export default class TeraFyServer {
1058
1099
  case 'file':
1059
1100
  return new File(
1060
1101
  [blob],
1061
- app.service('$supabase')._parsePath(path).basename,
1102
+ app.service('$supabase')._parsePath(filePath).basename,
1062
1103
  );
1063
1104
  default:
1064
1105
  throw new Error(`Unsupported library format "${settings.format}"`);
@@ -1070,23 +1111,23 @@ export default class TeraFyServer {
1070
1111
  /**
1071
1112
  * Save back a citation library from some input
1072
1113
  *
1073
- * @param {String} [path] File path to save back to, if omitted one will be prompted for
1114
+ * @param {String} [id] File ID to save back to, if omitted a file will be prompted for
1074
1115
  * @param {Array<RefLibRef>|Blob|File} [refs] Collection of references for the selected library or the raw Blob/File
1075
1116
  *
1076
1117
  * @param {Object} [options] Additional options to mutate behaviour
1077
- * @param {String} [options.path] Alternate method to specify the path to save as, if omitted one will be prompted for
1118
+ * @param {String} [options.id] Alternate method to specify the file ID to save as, if omitted one will be prompted for
1078
1119
  * @param {Array<RefLibRef>|Blob|File} [options.refs] Alternate method to specify the refs to save as an array or raw Blob/File
1079
1120
  * @param {String} [options.format='auto'] Input format used. ENUM: 'auto' (try to figure it out from context), 'pojo' (JS array of RefLib references), 'blob' (raw JS Blob object), 'file' (named JS File object)
1080
1121
  * @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
1081
1122
  * @param {String|Array<String>} [options.hint] Hint(s) to store against the library. Generally corresponds to the current operation being performed - e.g. 'deduped'
1082
- * @param {String} [options.filename] Suggested filename if path is unspecified
1083
- * @param {String} [options.title='Save citation library'] Dialog title if path is unspecified and we need to prompt
1123
+ * @param {String} [options.filename] Suggested filename if `id` is unspecified
1124
+ * @param {String} [options.title='Save citation library'] Dialog title if `id` is unspecified and a prompt is necessary
1084
1125
  * @param {Boolean} [options.overwrite=true] Allow existing file upsert
1085
1126
  * @param {Object} [options.meta] Optional meta data to merge into the file data
1086
1127
  *
1087
1128
  * @returns {Promise} A promise which resolves when the save operation has completed
1088
1129
  */
1089
- setProjectLibrary(path, refs, options) {
1130
+ setProjectLibrary(id, refs, options) {
1090
1131
  let settings = {
1091
1132
  format: 'auto',
1092
1133
  autoRequire: true,
@@ -1096,17 +1137,18 @@ export default class TeraFyServer {
1096
1137
  overwrite: true,
1097
1138
  meta: null,
1098
1139
  ...(
1099
- typeof path == 'string' && Array.isArray(refs) ? {path, refs, ...options} // Called as (path, refs, options?)
1100
- : Array.isArray(path) || refs instanceof Blob || refs instanceof File ? {refs: path, ...refs} // Called as (refs, options?)
1101
- : path // Called as (options?)
1140
+ typeof id == 'string' && Array.isArray(refs) ? {id, refs, ...options} // Called as (id, refs, options?)
1141
+ : Array.isArray(id) || refs instanceof Blob || refs instanceof File ? {refs: id, ...refs} // Called as (refs, options?)
1142
+ : id // Called as (options?)
1102
1143
  )
1103
1144
  };
1104
1145
  if (!settings.refs) throw new Error('No refs to save');
1105
1146
 
1147
+ let filePath; // Eventual Supabase path to use
1106
1148
  return Promise.resolve()
1107
1149
  .then(()=> settings.autoRequire && this.requireProject())
1108
1150
  .then(()=> {
1109
- if (settings.path) return; // We already have a file path specified - skip
1151
+ if (settings.id) return; // We already have a file ID specified - skip
1110
1152
 
1111
1153
  // Prompt for a save filename
1112
1154
  return this.selectProjectFile({
@@ -1119,14 +1161,11 @@ export default class TeraFyServer {
1119
1161
  },
1120
1162
  autoRequire: false, // Handled above anyway
1121
1163
  })
1122
- .then(path => settings.path = path)
1164
+ .then(file => settings.id = file.id)
1123
1165
  })
1124
- .then(()=> { // Correct filename to a full path if needed
1125
- if (!/^\/.+\/.+/.test(settings.path)) { // Not already a qualified full path for a project
1126
- settings.path = `/projects/${app.service('$projects').active.id}/${settings.path}`;
1127
- }
1128
-
1129
- this.debug('INFO', 2, 'Save citation library as', path);
1166
+ .then(()=> { // Compute filePath
1167
+ filePath = app.service('$projects').decodeFilePath(settings.id);
1168
+ debugger; // FIXME: Untested
1130
1169
  })
1131
1170
  .then(()=> {
1132
1171
  // Mutate settings.ref -> Blob or File format needed by Supabase
@@ -1145,12 +1184,12 @@ export default class TeraFyServer {
1145
1184
 
1146
1185
  // Get Reflib to encode the POJO into a Blob/File
1147
1186
  return Reflib.downloadFile(settings.refs, {
1148
- filename: settings.path,
1187
+ filename: app.service('$supabase')._parsePath(filePath).filename,
1149
1188
  promptDownload: false, // Just return the fileBlob we hand to Supabase
1150
1189
  })
1151
1190
  case 'blob':
1152
1191
  if (!(settings.refs instanceof Blob)) throw new Error("setProjectLibrary({format: 'blob'} but non-Blob provided as `refs`");
1153
- return new File([settings.refs], app.service('$supabase')._parsePath(settings.path).basename);
1192
+ return new File([settings.refs], app.service('$supabase')._parsePath(filePath).basename);
1154
1193
  case 'file':
1155
1194
  if (!(settings.ref instanceof File)) throw new Error("setProjectLibrary({format: 'file'} but non-File provided as `refs`");
1156
1195
  return settings.refs;
@@ -1158,13 +1197,7 @@ export default class TeraFyServer {
1158
1197
  throw new Error(`Unsupported library format "${settings.format}"`);
1159
1198
  }
1160
1199
  })
1161
- .then(fileBlob => app.service('$supabase').fileUpload(settings.path, {
1162
- file: fileBlob,
1163
- mode: 'encoded',
1164
- overwrite: true,
1165
- meta: settings.meta,
1166
- transcoders: false, // We can skip transcoders as we are supplying the meta information above anyway
1167
- }))
1200
+ .then(fileBlob => app.service('$supabase').fileUpload(filePath))
1168
1201
  }
1169
1202
 
1170
1203
  // }}}
@@ -1183,7 +1216,7 @@ export default class TeraFyServer {
1183
1216
  }
1184
1217
  // }}}
1185
1218
 
1186
- // Webpages - setPageUrl() {{{
1219
+ // Webpages - setPageUrl(), setPageTitle {{{
1187
1220
 
1188
1221
  /**
1189
1222
  * Set an active tools URL (or path) so that it survives a refresh
@@ -1196,7 +1229,8 @@ export default class TeraFyServer {
1196
1229
 
1197
1230
  let $routeMatched = app.service('$route').matched[0];
1198
1231
  if (!$routeMatched?.name) throw new Error('No active route');
1199
- if (!$routeMatched?.path.endsWith('/:toolPath(.*)?')) throw new Error('No tool active');
1232
+ if ($routeMatched?.path == '/embed') return this.debug('INFO', 2, 'Ignoring setPageUrl(', url, ') on when in embed mode');
1233
+ if (!$routeMatched?.path.endsWith('/:toolPath(.*)?')) return this.debug('INFO', 2, 'Ignoring setPageUrl(', url, ') - no active tool anyway');
1200
1234
 
1201
1235
  app.router.push({
1202
1236
  path: $routeMatched.path.replace(/\/:toolPath\(\.\*\)\?$/, url),
@@ -1390,6 +1424,5 @@ export default class TeraFyServer {
1390
1424
  ...msg,
1391
1425
  );
1392
1426
  }
1393
-
1394
1427
  // }}}
1395
1428
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iebh/tera-fy",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
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=.",
@@ -12,7 +12,7 @@
12
12
  "build:docs:markdown": "documentation build lib/terafy.client.js lib/projectFile.js --format md --markdown-toc --output api.md",
13
13
  "lint": "eslint lib plugins utils widgets",
14
14
  "release": "release-it",
15
- "watch": "nodemon --watch lib --exec npm run build"
15
+ "watch": "nodemon --watch lib --watch plugins --exec npm run build"
16
16
  },
17
17
  "type": "module",
18
18
  "imports": {
@@ -109,7 +109,10 @@
109
109
  },
110
110
  "hooks": {
111
111
  "before:init": "git reset HEAD -- api.md docs/ dist/",
112
- "after:bump": "npm run build && git add api.md docs/ dist/"
112
+ "before:bump": [
113
+ "npm run build",
114
+ "git add api.md docs/ dist/"
115
+ ]
113
116
  },
114
117
  "plugins": {
115
118
  "@release-it/conventional-changelog": {
package/plugins/vue2.js CHANGED
@@ -1,4 +1,4 @@
1
- import {cloneDeep} from 'lodash-es';
1
+ import {cloneDeep, isPlainObject} from 'lodash-es';
2
2
  import TeraFyPluginBase from './base.js';
3
3
 
4
4
  /**
@@ -93,6 +93,7 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
93
93
  if (settings.read) {
94
94
  this.events.on(`update:projects/${stateObservable.id}`, newState => {
95
95
  skipUpdate++; // Skip next update as we're updating our own state anyway
96
+ this.debug('INFO', 5, 'Update Vue2 Remote->Local', {new: newState, old: stateObservable});
96
97
  this.merge(stateObservable, newState);
97
98
  });
98
99
  }
@@ -114,6 +115,7 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
114
115
  return;
115
116
  }
116
117
 
118
+ this.debug('INFO', 5, 'Update Vue2 Local->Remote', {new: newVal, old: oldVal});
117
119
  this.createProjectStatePatch(newVal, oldVal);
118
120
  oldVal = cloneDeep(snapshot);
119
121
  },
@@ -140,9 +142,9 @@ export default class TeraFyPluginVue2 extends TeraFyPluginBase {
140
142
  payload.forEach(pl =>
141
143
  Object.keys(pl)
142
144
  .forEach(k => {
143
- if (typeof pl[k] == 'object') { // Setting sub-objects - need to recurse these in Vue2
145
+ if (isPlainObject(pl[k])) { // Setting sub-objects - we need to recurse these in Vue2 in case a sub-key gets glued on
144
146
  if (!(k in target)) // Destination to merge doesn't exist yet - create it
145
- this.Vue.set(target, k, Array.isArray(pl[k]) ? [] : {});
147
+ this.Vue.set(target, k, {});
146
148
 
147
149
  this.merge(target[k], pl[k]);
148
150
  } else {