@iebh/tera-fy 1.0.8 → 1.0.10
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/README.md +9 -4
- package/api.md +445 -431
- package/dist/terafy.js +2 -2
- package/dist/terafy.js.map +4 -4
- package/docs/assets/anchor.js +350 -0
- package/docs/assets/bass-addons.css +12 -0
- package/docs/assets/bass.css +544 -0
- package/docs/assets/fonts/EOT/SourceCodePro-Bold.eot +0 -0
- package/docs/assets/fonts/EOT/SourceCodePro-Regular.eot +0 -0
- package/docs/assets/fonts/LICENSE.txt +93 -0
- package/docs/assets/fonts/OTF/SourceCodePro-Bold.otf +0 -0
- package/docs/assets/fonts/OTF/SourceCodePro-Regular.otf +0 -0
- package/docs/assets/fonts/TTF/SourceCodePro-Bold.ttf +0 -0
- package/docs/assets/fonts/TTF/SourceCodePro-Regular.ttf +0 -0
- package/docs/assets/fonts/WOFF/OTF/SourceCodePro-Bold.otf.woff +0 -0
- package/docs/assets/fonts/WOFF/OTF/SourceCodePro-Regular.otf.woff +0 -0
- package/docs/assets/fonts/WOFF/TTF/SourceCodePro-Bold.ttf.woff +0 -0
- package/docs/assets/fonts/WOFF/TTF/SourceCodePro-Regular.ttf.woff +0 -0
- package/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Bold.otf.woff2 +0 -0
- package/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Regular.otf.woff2 +0 -0
- package/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Bold.ttf.woff2 +0 -0
- package/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2 +0 -0
- package/docs/assets/fonts/source-code-pro.css +23 -0
- package/docs/assets/github.css +123 -0
- package/docs/assets/site.js +168 -0
- package/docs/assets/split.css +15 -0
- package/docs/assets/split.js +782 -0
- package/docs/assets/style.css +147 -0
- package/docs/index.html +3636 -0
- package/{index.html → docs/playground.html} +48 -12
- package/documentation.yml +12 -0
- package/lib/terafy.client.js +230 -6
- package/lib/terafy.server.js +192 -10
- package/package.json +7 -6
- package/plugins/vue2.js +173 -0
- package/plugins/{vue.js → vue3.js} +20 -11
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
</style>
|
|
21
21
|
|
|
22
22
|
<script type="module">
|
|
23
|
-
import TeraFy from '
|
|
23
|
+
import TeraFy from 'https://esm.run/@iebh/tera-fy';
|
|
24
24
|
|
|
25
25
|
Vue.createApp({
|
|
26
26
|
data() { return {
|
|
@@ -97,6 +97,32 @@
|
|
|
97
97
|
</head>
|
|
98
98
|
<body>
|
|
99
99
|
<div id="app">
|
|
100
|
+
<!-- Nav header {{{ -->
|
|
101
|
+
<nav class="navbar navbar-expand-lg navbar-light bg-light px-4">
|
|
102
|
+
<a href="https://github.com/IEBH/TERA-fy" class="navbar-brand">TERA-fy</a>
|
|
103
|
+
<button type="button" data-toggle="collapse" data-target="#navbarAreas" class="navbar-toggler">
|
|
104
|
+
<span class="navbar-toggler-icon"></span>
|
|
105
|
+
</button>
|
|
106
|
+
<div id="navbarAreas" class="collapse navbar-collapse">
|
|
107
|
+
<ul class="navbar-nav">
|
|
108
|
+
<li class="navbar-item me-1">
|
|
109
|
+
<a href="https://iebh.github.io/TERA-fy" class="btn btn-light">API docs</a>
|
|
110
|
+
</li>
|
|
111
|
+
<li class="navbar-item mr-1">
|
|
112
|
+
<a href="https://iebh.github.io/TERA-fy/playground.html" class="btn btn-primary text-white">Playground</a>
|
|
113
|
+
</li>
|
|
114
|
+
</ul>
|
|
115
|
+
</div>
|
|
116
|
+
<ul class="navbar-nav">
|
|
117
|
+
<li class="navbar-item">
|
|
118
|
+
<a href="https://github.com/IEBH/TERA-fy" class="btn btn-light">
|
|
119
|
+
GitHub
|
|
120
|
+
</a>
|
|
121
|
+
</li>
|
|
122
|
+
</ul>
|
|
123
|
+
</nav>
|
|
124
|
+
<!-- }}} -->
|
|
125
|
+
|
|
100
126
|
<div class="container pt-4">
|
|
101
127
|
<div class="row">
|
|
102
128
|
<div class="col-sm-12 col-md-6">
|
|
@@ -161,12 +187,6 @@
|
|
|
161
187
|
<div class="card-header">Projects</div>
|
|
162
188
|
<div class="card-body">
|
|
163
189
|
<div class="list-group">
|
|
164
|
-
<a
|
|
165
|
-
@click="run('bindProject')"
|
|
166
|
-
class="list-group-item list-group-item-action disabled"
|
|
167
|
-
>
|
|
168
|
-
terafy.bindProject()
|
|
169
|
-
</a>
|
|
170
190
|
<a
|
|
171
191
|
@click="run('getProject')"
|
|
172
192
|
class="list-group-item list-group-item-action"
|
|
@@ -186,7 +206,7 @@
|
|
|
186
206
|
>
|
|
187
207
|
<div>terafy.setActiveProject(</div>
|
|
188
208
|
<select
|
|
189
|
-
v-if="projects"
|
|
209
|
+
v-if="projects && projects.length > 0"
|
|
190
210
|
v-model="project"
|
|
191
211
|
class="form-control"
|
|
192
212
|
placeholder="Select project..."
|
|
@@ -231,16 +251,32 @@
|
|
|
231
251
|
terafy.getProjectState()
|
|
232
252
|
</a>
|
|
233
253
|
<a
|
|
234
|
-
@click="run('
|
|
254
|
+
@click="run('applyProjectStatePatch')"
|
|
235
255
|
class="list-group-item list-group-item-action disabled"
|
|
236
256
|
>
|
|
237
|
-
terafy.
|
|
257
|
+
terafy.applyProjectStatePatch()
|
|
238
258
|
</a>
|
|
239
259
|
<a
|
|
240
|
-
@click="run('
|
|
260
|
+
@click="run('subscribeProjectState')"
|
|
241
261
|
class="list-group-item list-group-item-action disabled"
|
|
242
262
|
>
|
|
243
|
-
terafy.
|
|
263
|
+
terafy.subscribeProjectState()
|
|
264
|
+
</a>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
<!-- }}} -->
|
|
269
|
+
|
|
270
|
+
<!-- Project Files {{{ -->
|
|
271
|
+
<div class="card mb-2">
|
|
272
|
+
<div class="card-header">Project Files</div>
|
|
273
|
+
<div class="card-body">
|
|
274
|
+
<div class="list-group">
|
|
275
|
+
<a
|
|
276
|
+
@click="run('getProjectFiles')"
|
|
277
|
+
class="list-group-item list-group-item-action"
|
|
278
|
+
>
|
|
279
|
+
terafy.getProjectFiles()
|
|
244
280
|
</a>
|
|
245
281
|
</div>
|
|
246
282
|
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
toc:
|
|
2
|
+
- name: TeraFy Playground
|
|
3
|
+
description: |
|
|
4
|
+
See [the TeraFy Playground](./playground.html) to expriment with the various TeraFy API calls.
|
|
5
|
+
- name: Data entities
|
|
6
|
+
children:
|
|
7
|
+
- User
|
|
8
|
+
- Project
|
|
9
|
+
- ProjectFile
|
|
10
|
+
- name: TeraFy
|
|
11
|
+
description: |
|
|
12
|
+
API reference for all methods exposed by the TeraFy client library.
|
package/lib/terafy.client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {diff, jsonPatchPathConverter as jsPatchConverter} from 'just-diff';
|
|
1
2
|
import {cloneDeep} from 'lodash-es';
|
|
2
3
|
import {nanoid} from 'nanoid';
|
|
3
4
|
|
|
@@ -61,9 +62,12 @@ export default class TeraFy {
|
|
|
61
62
|
'bindProject', 'getProject', 'getProjects', 'setActiveProject', 'requireProject', 'selectProject',
|
|
62
63
|
|
|
63
64
|
// Project state
|
|
64
|
-
'getProjectState', 'applyProjectStatePatch',
|
|
65
|
+
'getProjectState', 'applyProjectStatePatch', 'subscribeProjectState',
|
|
65
66
|
// bindProjectState() - See below
|
|
66
67
|
|
|
68
|
+
// Project files
|
|
69
|
+
'getProjectFiles',
|
|
70
|
+
|
|
67
71
|
// Project Libraries
|
|
68
72
|
'getProjectLibrary', 'setProjectLibrary',
|
|
69
73
|
];
|
|
@@ -201,6 +205,39 @@ export default class TeraFy {
|
|
|
201
205
|
|
|
202
206
|
// }}}
|
|
203
207
|
|
|
208
|
+
// Project state - createProjectStatePatch(), applyProjectStatePatchLocal() {{{
|
|
209
|
+
/**
|
|
210
|
+
* Create + transmit a new project state patch base on the current and previous states
|
|
211
|
+
* The transmitted patch follows the [JSPatch](http://jsonpatch.com) standard
|
|
212
|
+
* This function accepts an entire projectState instance, computes the delta and transmits that to the server for merging
|
|
213
|
+
*
|
|
214
|
+
* @param {Object} newState The local projectState to accept
|
|
215
|
+
* @param {Object} oldState The previous projectState to examine against
|
|
216
|
+
*
|
|
217
|
+
* @returns {Promise} A promise which will resolve when the operation has completed
|
|
218
|
+
*/
|
|
219
|
+
createProjectStatePatch(newState, oldState) {
|
|
220
|
+
let patch = diff(oldState, newState, jsPatchConverter);
|
|
221
|
+
this.debug('INFO', 'Created project patch', {newState, oldState});
|
|
222
|
+
return this.applyProjectStatePatch(patch);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Client function which accepts a patch from the server and applies it to local project state
|
|
228
|
+
* The patch should follow the [JSPatch](http://jsonpatch.com) standard
|
|
229
|
+
* This function is expected to be sub-classed by a plugin
|
|
230
|
+
*
|
|
231
|
+
* @param {Array} patch A JSPatch patch to apply
|
|
232
|
+
*
|
|
233
|
+
* @returns {Promise} A promise which will resolve when the operation has completed
|
|
234
|
+
*/
|
|
235
|
+
applyProjectStatePatchLocal(patch) {
|
|
236
|
+
this.debug('WARN', 'Accepted project patch', patch, 'but applyProjectStatePatchLocal() has not been sub-classed');
|
|
237
|
+
return Promise.resolve();
|
|
238
|
+
}
|
|
239
|
+
// }}}
|
|
240
|
+
|
|
204
241
|
// Init - constructor(), init(), inject*() {{{
|
|
205
242
|
|
|
206
243
|
/**
|
|
@@ -217,13 +254,16 @@ export default class TeraFy {
|
|
|
217
254
|
* Initalize the TERA client singleton
|
|
218
255
|
* This function can only be called once and will return the existing init() worker Promise if its called againt
|
|
219
256
|
*
|
|
257
|
+
* @param {Object} [options] Additional options to merge into `settings` via `set`
|
|
220
258
|
* @returns {Promise<TeraFy>} An eventual promise which will resovle with this terafy instance
|
|
221
259
|
*/
|
|
222
|
-
init() {
|
|
260
|
+
init(options) {
|
|
261
|
+
if (options) this.set(options);
|
|
223
262
|
if (this.init.promise) return this.init.promise; // Aleady been called - return init promise
|
|
224
263
|
|
|
225
264
|
window.addEventListener('message', this.acceptMessage.bind(this));
|
|
226
265
|
|
|
266
|
+
const context = this;
|
|
227
267
|
return this.init.promise = Promise.resolve()
|
|
228
268
|
.then(()=> this.detectMode())
|
|
229
269
|
.then(mode => this.settings.mode = mode)
|
|
@@ -232,11 +272,12 @@ export default class TeraFy {
|
|
|
232
272
|
this.injectComms(),
|
|
233
273
|
this.injectStylesheet(),
|
|
234
274
|
this.injectMethods(),
|
|
235
|
-
|
|
236
|
-
// Init all plugins
|
|
237
|
-
...this.plugins
|
|
238
|
-
.map(plugin => plugin.init()),
|
|
239
275
|
]))
|
|
276
|
+
.then(()=> Promise.all( // Init all plugins (with this outer module as the context)
|
|
277
|
+
this.plugins.map(plugin =>
|
|
278
|
+
plugin.init.call(context)
|
|
279
|
+
)
|
|
280
|
+
))
|
|
240
281
|
}
|
|
241
282
|
|
|
242
283
|
|
|
@@ -450,6 +491,7 @@ export default class TeraFy {
|
|
|
450
491
|
mixin(target, source) {
|
|
451
492
|
Object.getOwnPropertyNames(Object.getPrototypeOf(source))
|
|
452
493
|
.filter(prop => !['constructor', 'prototype', 'name'].includes(prop))
|
|
494
|
+
.filter(prop => prop != 'init') // Don't allow plugin init() to override our version - these all get called during init() anyway
|
|
453
495
|
.forEach((prop) => {
|
|
454
496
|
Object.defineProperty(
|
|
455
497
|
target,
|
|
@@ -493,4 +535,186 @@ export default class TeraFy {
|
|
|
493
535
|
globalThis.document.body.classList.toggle('tera-fy-focus', isFocused === 'toggle' ? undefined : isFocused);
|
|
494
536
|
}
|
|
495
537
|
// }}}
|
|
538
|
+
|
|
539
|
+
// Stub documentation carried over from ./terafy.server.js {{{
|
|
540
|
+
/**
|
|
541
|
+
* Return basic server information as a form of validation
|
|
542
|
+
*
|
|
543
|
+
* @function handshake
|
|
544
|
+
* @returns {Promise<Object>} Basic promise result
|
|
545
|
+
* @property {Date} date Server date
|
|
546
|
+
*/
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* User / active session within TERA
|
|
551
|
+
*
|
|
552
|
+
* @name User
|
|
553
|
+
* @property {String} id Unique identifier of the user
|
|
554
|
+
* @property {String} email The email address of the current user
|
|
555
|
+
* @property {String} name The provided full name of the user
|
|
556
|
+
* @property {Boolean} isSubscribed Whether the active user has a TERA subscription
|
|
557
|
+
*/
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Fetch the current session user
|
|
562
|
+
*
|
|
563
|
+
* @function getUser
|
|
564
|
+
* @returns {Promise<User>} The current logged in user or null if none
|
|
565
|
+
*/
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Project entry within TERA
|
|
570
|
+
*
|
|
571
|
+
* @name Project
|
|
572
|
+
* @url https://tera-tools.com/help/api/schema
|
|
573
|
+
*/
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Get the currently active project, if any
|
|
578
|
+
*
|
|
579
|
+
* @function getProject
|
|
580
|
+
* @returns {Promise<Project|null>} The currently active project, if any
|
|
581
|
+
*/
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Get a list of projects the current session user has access to
|
|
586
|
+
*
|
|
587
|
+
* @function getProjects
|
|
588
|
+
* @returns {Promise<Array<Project>>} Collection of projects the user has access to
|
|
589
|
+
*/
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Set the currently active project within TERA
|
|
594
|
+
*
|
|
595
|
+
* @function setActiveProject
|
|
596
|
+
* @param {Object|String} project The project to set as active - either the full Project object or its ID
|
|
597
|
+
*/
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Ask the user to select a project from those available - if one isn't already active
|
|
602
|
+
* Note that this function will percist in asking the uesr even if they try to cancel
|
|
603
|
+
*
|
|
604
|
+
* @function requireProject
|
|
605
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
606
|
+
* @param {Boolean} [options.autoSetActiveProject=true] After selecting a project set that project as active in TERA
|
|
607
|
+
* @param {String} [options.title="Select a project to work with"] The title of the dialog to display
|
|
608
|
+
* @param {String} [options.noSelectTitle='Select project'] Dialog title when warning the user they need to select something
|
|
609
|
+
* @param {String} [options.noSelectBody='A project needs to be selected to continue'] Dialog body when warning the user they need to select something
|
|
610
|
+
*
|
|
611
|
+
* @returns {Promise<Project>} The active project
|
|
612
|
+
*/
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Prompt the user to select a project from those available
|
|
617
|
+
*
|
|
618
|
+
* @function selectProject
|
|
619
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
620
|
+
* @param {String} [options.title="Select a project to work with"] The title of the dialog to display
|
|
621
|
+
* @param {Boolean} [options.allowCancel=true] Advertise cancelling the operation, the dialog can still be cancelled by closing it
|
|
622
|
+
* @param {Boolean} [options.setActive=false] Also set the project as active when selected
|
|
623
|
+
*
|
|
624
|
+
* @returns {Promise<Project>} The active project
|
|
625
|
+
*/
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Return the current, full snapshot state of the active project
|
|
630
|
+
*
|
|
631
|
+
* @function getProjectState
|
|
632
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
633
|
+
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
634
|
+
* @param {Array<String>} Paths to subscribe to e.g. ['/users/'],
|
|
635
|
+
*
|
|
636
|
+
* @returns {Promise<Object>} The current project state snapshot
|
|
637
|
+
*/
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Apply a computed `just-diff` patch to the current project state
|
|
642
|
+
*
|
|
643
|
+
* @function applyProjectStatePatch
|
|
644
|
+
* @param {Object} Patch to apply
|
|
645
|
+
* @returns {Promise} A promise which resolves when the operation has completed
|
|
646
|
+
*/
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Subscribe to project state changes
|
|
651
|
+
* This will dispatch an RPC call to the source object `applyProjectStatePatchLocal()` function with the patch
|
|
652
|
+
* If the above call fails the subscriber is assumed as dead and unsubscribed from the polling list
|
|
653
|
+
*
|
|
654
|
+
* @function subscribeProjectState
|
|
655
|
+
* @returns {Promise<Function>} A promise which resolves when a subscription has been created, call the resulting function to unsubscribe
|
|
656
|
+
*/
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Data structure for a project file
|
|
661
|
+
*
|
|
662
|
+
* @name ProjectFile
|
|
663
|
+
*
|
|
664
|
+
* @property {String} id A UUID string representing the unique ID of the file
|
|
665
|
+
* @property {String} name Relative name path (can contain prefix directories) for the human readable file name
|
|
666
|
+
* @property {Object} parsedName An object representing meta file parts of a file name
|
|
667
|
+
* @property {String} parsedName.basename The filename + extention (i.e. everything without directory name)
|
|
668
|
+
* @property {String} parsedName.filename The file portion of the name (basename without the extension)
|
|
669
|
+
* @property {String} parsedName.ext The extension portion of the name (always lower case)
|
|
670
|
+
* @property {String} parsedName.dirName The directory path portion of the name
|
|
671
|
+
* @property {Date} created A date representing when the file was created
|
|
672
|
+
* @property {Date} modified A date representing when the file was created
|
|
673
|
+
* @property {Date} accessed A date representing when the file was last accessed
|
|
674
|
+
* @property {Number} size Size, in bytes, of the file
|
|
675
|
+
* @property {String} mime The associated mime type for the file
|
|
676
|
+
*/
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Fetch the files associated with a given project
|
|
681
|
+
*
|
|
682
|
+
* @function getProjectFiles
|
|
683
|
+
* @param {Object} options Options which mutate behaviour
|
|
684
|
+
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
685
|
+
* @param {Boolean} [options.meta=true] Pull meta information for each file entity
|
|
686
|
+
*
|
|
687
|
+
* @returns {Promise<ProjectFile>} A collection of project files for the given project
|
|
688
|
+
*/
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Fetch the active projects citation library
|
|
693
|
+
*
|
|
694
|
+
* @function getProjectLibrary
|
|
695
|
+
* @param {String} [path] Optional file path to use, if omitted the contents of `options` are used to guess at a suitable file
|
|
696
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
697
|
+
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
698
|
+
* @param {Boolean} [options.multiple=false] Allow selection of multiple libraries
|
|
699
|
+
* @param {Function} [options.filter] Optional async file filter, called each time as `(File:ProjectFile)`
|
|
700
|
+
* @param {Function} [options.find] Optional async final stage file filter to reduce all candidates down to one subject file
|
|
701
|
+
* @param {String|Array<String>} [options.hint] Hints to identify the library to select in array order of preference. Generally corresponds to the previous stage - e.g. 'deduped', 'review1', 'review2', 'dedisputed'
|
|
702
|
+
*
|
|
703
|
+
* @returns {Promise<Array<ProjectFile>>} Collection of references for the selected library matching the given hint + filter, this could be a zero length array
|
|
704
|
+
*/
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Save back a projects citation library
|
|
709
|
+
*
|
|
710
|
+
* @function setProjectLibrary
|
|
711
|
+
* @param {Array<RefLibRef>} Collection of references for the selected library
|
|
712
|
+
*
|
|
713
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
714
|
+
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
715
|
+
* @param {String} [options.hint] Hint to store against the library. Generally corresponds to the current operation being performed - e.g. 'deduped'
|
|
716
|
+
*
|
|
717
|
+
* @returns {Promise} A promise which resolves when the save operation has completed
|
|
718
|
+
*/
|
|
719
|
+
// }}}
|
|
496
720
|
}
|
package/lib/terafy.server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {cloneDeep} from 'lodash-es';
|
|
1
|
+
import {cloneDeep, set as pathSet} from 'lodash-es';
|
|
2
|
+
import {diffApply, jsonPatchPathConverter as jsPatchConverter} from 'just-diff-apply';
|
|
2
3
|
import {nanoid} from 'nanoid';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -15,10 +16,15 @@ export default class TeraFyServer {
|
|
|
15
16
|
*
|
|
16
17
|
* @type {Object}
|
|
17
18
|
* @property {Boolean} devMode Operate in devMode - i.e. force outer refresh when encountering an existing TeraFy instance
|
|
19
|
+
* @property {Number} subscribeTimeout Acceptable timeout period for subscribers to acklowledge a project change event, failing to respond will result in the subscriber being removed from the available subscriber list
|
|
18
20
|
* @property {String} restrictOrigin URL to restrict communications to
|
|
21
|
+
* @property {String} projectId The project to use as the default reference when calling various APIs
|
|
19
22
|
*/
|
|
20
23
|
settings = {
|
|
24
|
+
devMode: false,
|
|
21
25
|
restrictOrigin: '*',
|
|
26
|
+
subscribeTimeout: 2000,
|
|
27
|
+
projectId: null,
|
|
22
28
|
};
|
|
23
29
|
|
|
24
30
|
// Contexts - createContext(), messageEvent, senderRpc() {{{
|
|
@@ -237,7 +243,7 @@ export default class TeraFyServer {
|
|
|
237
243
|
$auth.promise(),
|
|
238
244
|
$subscriptions.promise(),
|
|
239
245
|
])
|
|
240
|
-
.then(()=>
|
|
246
|
+
.then(()=> $auth.user.id ? {
|
|
241
247
|
id: $auth.user.id,
|
|
242
248
|
email: $auth.user.email,
|
|
243
249
|
name: [
|
|
@@ -245,7 +251,7 @@ export default class TeraFyServer {
|
|
|
245
251
|
$auth.user.family_name,
|
|
246
252
|
].filter(Boolean).join(' '),
|
|
247
253
|
isSubscribed: $subscriptions.isSubscribed,
|
|
248
|
-
})
|
|
254
|
+
} : null)
|
|
249
255
|
}
|
|
250
256
|
|
|
251
257
|
// }}}
|
|
@@ -403,7 +409,7 @@ export default class TeraFyServer {
|
|
|
403
409
|
|
|
404
410
|
// }}}
|
|
405
411
|
|
|
406
|
-
// Project State - getProjectState(), applyProjectStatePatch() {{{
|
|
412
|
+
// Project State - getProjectState(), setProjectState(), saveProjectState(), applyProjectStatePatch(), subscribeProjectState() {{{
|
|
407
413
|
|
|
408
414
|
/**
|
|
409
415
|
* Return the current, full snapshot state of the active project
|
|
@@ -427,13 +433,166 @@ export default class TeraFyServer {
|
|
|
427
433
|
}
|
|
428
434
|
|
|
429
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Set a nested value within the project state
|
|
438
|
+
* Paths can be any valid Lodash.set() value such as:
|
|
439
|
+
*
|
|
440
|
+
* - Dotted notation - e.g. `foo.bar.1.baz`
|
|
441
|
+
* - Array path segments e.g. `['foo', 'bar', 1, 'baz']`
|
|
442
|
+
*
|
|
443
|
+
*
|
|
444
|
+
* @param {String|Array<String>} path The sub-path within the project state to set
|
|
445
|
+
* @param {*} value The value to set
|
|
446
|
+
*
|
|
447
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
448
|
+
* @param {Boolean} [options.save=true] Save the changes to the server immediately, disable to queue up multiple writes
|
|
449
|
+
* @param {Boolean} [options.sync=false] Wait for the server to acknowledge the write, you almost never need to do this
|
|
450
|
+
*
|
|
451
|
+
* @returns {Promise} A promise which resolves when the operation has synced with the server
|
|
452
|
+
*/
|
|
453
|
+
setProjectState(path, value, options) {
|
|
454
|
+
let settings = {
|
|
455
|
+
save: true,
|
|
456
|
+
sync: false,
|
|
457
|
+
...options,
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
if (!app.service('$projects').active) throw new Error('No active project');
|
|
461
|
+
|
|
462
|
+
pathSet(app.service('$projects').active, path, value)
|
|
463
|
+
|
|
464
|
+
return (
|
|
465
|
+
this.save && this.sync ? this.saveProjectState()
|
|
466
|
+
: this.save ? void this.saveProjectState()
|
|
467
|
+
: (()=> { throw new Error('setProjectState({sync: true, save: false}) makes no sense') })()
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Force-Save the currently active project state
|
|
474
|
+
*
|
|
475
|
+
* @returns {Promise} A promise which resolves when the operation has completed
|
|
476
|
+
*/
|
|
477
|
+
saveProjectState() {
|
|
478
|
+
if (!app.service('$projects').active) throw new Error('No active project');
|
|
479
|
+
|
|
480
|
+
// TODO: Would be nice if we compared against a sanity hash or something before just clobbering
|
|
481
|
+
this.debug('FIXME: Force saving projects is not yet supported - this should occur in realtime anyway');
|
|
482
|
+
return Promise.resolve();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
|
|
430
486
|
/**
|
|
431
487
|
* Apply a computed `just-diff` patch to the current project state
|
|
488
|
+
*
|
|
489
|
+
* @param {Object} Patch to apply
|
|
490
|
+
* @returns {Promise} A promise which resolves when the operation has completed
|
|
432
491
|
*/
|
|
433
492
|
applyProjectStatePatch(patch) {
|
|
434
|
-
|
|
493
|
+
if (!app.service('$projects').active) throw new Error('No active project to patch');
|
|
494
|
+
this.debug('Applying', patch.length, 'project state patches', {patch});
|
|
495
|
+
diffApply(app.service('$projects').active, patch, jsPatchConverter);
|
|
496
|
+
|
|
497
|
+
return Promise.resolve();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Subscribe to project state changes
|
|
503
|
+
* This will dispatch an RPC call to the source object `applyProjectStatePatchLocal()` function with the patch
|
|
504
|
+
* If the above call fails the subscriber is assumed as dead and unsubscribed from the polling list
|
|
505
|
+
*
|
|
506
|
+
* @returns {Promise<Function>} A promise which resolves when a subscription has been created, call the resulting function to unsubscribe
|
|
507
|
+
*/
|
|
508
|
+
subscribeProjectState() {
|
|
509
|
+
if (!this.messageEvent) throw new Error('senderRpc() can only be used if given a context from `createContext()`');
|
|
510
|
+
|
|
511
|
+
let subscriber = {
|
|
512
|
+
id: nanoid(),
|
|
513
|
+
origin: this.messageEvent.origin,
|
|
514
|
+
sendPatch: patch => new Promise((resolve, reject) => {
|
|
515
|
+
let senderTimeout = setTimeout(()=> {
|
|
516
|
+
reject(`Timed out sending to project-state subscriber "${subscriber.origin}"`);
|
|
517
|
+
}, this.subscribeTimeout);
|
|
518
|
+
|
|
519
|
+
return this.senderRpc.call(this, 'applyProjectStatePatchLocal', patch)
|
|
520
|
+
.then(()=> {
|
|
521
|
+
clearTimeout(senderTimeout);
|
|
522
|
+
resolve()
|
|
523
|
+
})
|
|
524
|
+
.catch(e => {
|
|
525
|
+
subscriber.unsubscribe();
|
|
526
|
+
reject(`Rejected calling RPC:applyProjectStatePatchLocal() with project-state subscriber "${subscriber.origin}" -`, e)
|
|
527
|
+
|
|
528
|
+
})
|
|
529
|
+
}),
|
|
530
|
+
unsubscribe: ()=> {
|
|
531
|
+
this.debug('Unsubscribing project-state subscriber', subscriber.origin);
|
|
532
|
+
this.projectStateSubscribers = this.projectStateSubscribers.filter(ps => ps.id != subscriber.id);
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// Append to subscriber list
|
|
537
|
+
this.projectStateSubscribers.push(subscriber)
|
|
435
538
|
}
|
|
436
539
|
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Subscribers to server project state changes
|
|
543
|
+
* @type {Array<Object>}}
|
|
544
|
+
* @property {String} id A unique ID for this subscriber state
|
|
545
|
+
* @property {String} source The human readable source for each subscriber
|
|
546
|
+
* @property {Function} sendPatch Function used to send a patch to a subscriber
|
|
547
|
+
* @property {Function} unsubscribe Function to release the client subscription
|
|
548
|
+
*/
|
|
549
|
+
projectStateSubscribers = [];
|
|
550
|
+
// }}}
|
|
551
|
+
|
|
552
|
+
// Project files - getProjectFiles() {{{
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Data structure for a project file
|
|
556
|
+
* @class ProjectFile
|
|
557
|
+
*
|
|
558
|
+
* @property {String} id A UUID string representing the unique ID of the file
|
|
559
|
+
* @property {String} name Relative name path (can contain prefix directories) for the human readable file name
|
|
560
|
+
* @property {Object} parsedName An object representing meta file parts of a file name
|
|
561
|
+
* @property {String} parsedName.basename The filename + extention (i.e. everything without directory name)
|
|
562
|
+
* @property {String} parsedName.filename The file portion of the name (basename without the extension)
|
|
563
|
+
* @property {String} parsedName.ext The extension portion of the name (always lower case)
|
|
564
|
+
* @property {String} parsedName.dirName The directory path portion of the name
|
|
565
|
+
* @property {Date} created A date representing when the file was created
|
|
566
|
+
* @property {Date} modified A date representing when the file was created
|
|
567
|
+
* @property {Date} accessed A date representing when the file was last accessed
|
|
568
|
+
* @property {Number} size Size, in bytes, of the file
|
|
569
|
+
* @property {String} mime The associated mime type for the file
|
|
570
|
+
*/
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Fetch the files associated with a given project
|
|
575
|
+
*
|
|
576
|
+
* @param {Object} options Options which mutate behaviour
|
|
577
|
+
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
578
|
+
* @param {Boolean} [options.meta=true] Pull meta information for each file entity
|
|
579
|
+
*
|
|
580
|
+
* @returns {Promise<ProjectFile>} A collection of project files for the given project
|
|
581
|
+
*/
|
|
582
|
+
getProjectFiles(options) {
|
|
583
|
+
let settings = {
|
|
584
|
+
autoRequire: true,
|
|
585
|
+
meta: true,
|
|
586
|
+
...options,
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
return Promise.resolve()
|
|
590
|
+
.then(()=> app.service('$projects').promise())
|
|
591
|
+
.then(()=> settings.autoRequire && this.requireProject())
|
|
592
|
+
.then(project => app.service('$supabase').fileList(`/projects/${project.id}`, {
|
|
593
|
+
meta: settings.meta,
|
|
594
|
+
}))
|
|
595
|
+
}
|
|
437
596
|
// }}}
|
|
438
597
|
|
|
439
598
|
// Project Libraries - getProjectLibrary(), setProjectLibrary() {{{
|
|
@@ -441,24 +600,47 @@ export default class TeraFyServer {
|
|
|
441
600
|
/**
|
|
442
601
|
* Fetch the active projects citation library
|
|
443
602
|
*
|
|
603
|
+
* @param {String} [path] Optional file path to use, if omitted the contents of `options` are used to guess at a suitable file
|
|
444
604
|
* @param {Object} [options] Additional options to mutate behaviour
|
|
445
605
|
* @param {Boolean} [options.autoRequire=true] Run `requireProject()` automatically before continuing
|
|
446
606
|
* @param {Boolean} [options.multiple=false] Allow selection of multiple libraries
|
|
607
|
+
* @param {Function} [options.filter] Optional async file filter, called each time as `(File:ProjectFile)`
|
|
608
|
+
* @param {Function} [options.find] Optional async final stage file filter to reduce all candidates down to one subject file
|
|
447
609
|
* @param {String|Array<String>} [options.hint] Hints to identify the library to select in array order of preference. Generally corresponds to the previous stage - e.g. 'deduped', 'review1', 'review2', 'dedisputed'
|
|
448
610
|
*
|
|
449
|
-
* @returns {Promise<Array<
|
|
611
|
+
* @returns {Promise<Array<ProjectFile>>} Collection of references for the selected library matching the given hint + filter, this could be a zero length array
|
|
450
612
|
*/
|
|
451
|
-
getProjectLibrary(options) {
|
|
613
|
+
getProjectLibrary(path, options) {
|
|
452
614
|
let settings = {
|
|
615
|
+
path,
|
|
453
616
|
autoRequire: true,
|
|
454
617
|
multiple: false,
|
|
455
618
|
hint: null,
|
|
456
|
-
|
|
619
|
+
filter: file => true,
|
|
620
|
+
find: files => files.at(0),
|
|
621
|
+
...(typeof path == 'string' ? {path, ...options} : path),
|
|
457
622
|
};
|
|
458
623
|
|
|
459
624
|
return Promise.resolve()
|
|
460
|
-
.then(()=>
|
|
461
|
-
|
|
625
|
+
.then(()=> {
|
|
626
|
+
if (settings.path) { // Already have a file name picked
|
|
627
|
+
if (!settings.path.startsWith('/')) throw new Error('All file names must start with a forward slash');
|
|
628
|
+
return settings.path;
|
|
629
|
+
} else { // Try to guess the file from the options structure
|
|
630
|
+
return this.getProjectFiles({
|
|
631
|
+
autoRequire: settings.autoRequire,
|
|
632
|
+
})
|
|
633
|
+
.then(files => files.filter(file =>
|
|
634
|
+
settings.filter(file)
|
|
635
|
+
))
|
|
636
|
+
.then(files => settings.find(files))
|
|
637
|
+
.then(file => file.path)
|
|
638
|
+
}
|
|
639
|
+
})
|
|
640
|
+
.then(filePath => app.service('$supabase').fileGet(filePath, {
|
|
641
|
+
json: true,
|
|
642
|
+
toast: false,
|
|
643
|
+
}))
|
|
462
644
|
}
|
|
463
645
|
|
|
464
646
|
|