@muze-nl/simplystore 0.6.11 → 0.6.12
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 +8 -12
- package/package.json +2 -2
- package/src/command-worker-module.mjs +12 -8
- package/src/load-worker.mjs +15 -14
- package/src/query-worker-module.mjs +21 -9
- package/src/server.mjs +15 -13
- package/src/workerPool.mjs +19 -4
package/README.md
CHANGED
|
@@ -152,18 +152,14 @@ In addition, SimplyStore is meant to be a real-world testcase for JSONTag.
|
|
|
152
152
|
<a name="roadmap"></a>
|
|
153
153
|
## Roadmap
|
|
154
154
|
|
|
155
|
-
- allow changes to dataset by creating a new root
|
|
156
|
-
- command handling with crud commands and command log
|
|
157
|
-
- backup current dataset to JSONTag file
|
|
158
|
-
- on startup check if any commands in the log haven't been resolved, if so run them
|
|
159
|
-
|
|
160
|
-
-
|
|
161
|
-
|
|
162
|
-
-
|
|
163
|
-
- allow custom templates, instead of the default index.html
|
|
164
|
-
- add support for access control, based on webid / openid connect
|
|
165
|
-
- switch from VM2 to V8-isolate, which is more secure
|
|
166
|
-
- switch the server runtime to Rust, so SimplyStore can share immutable data between threads
|
|
155
|
+
- [x] allow changes to dataset by creating a new root
|
|
156
|
+
- [x] command handling with crud commands and command log
|
|
157
|
+
- [x] backup current dataset to JSONTag file
|
|
158
|
+
- [x] on startup check if any commands in the log haven't been resolved, if so run them
|
|
159
|
+
- [ ] improved web client with type-specific views and form elements
|
|
160
|
+
- [ ] allow custom templates, instead of the default index.html
|
|
161
|
+
- [ ] add support for access control, based on webid / openid connect
|
|
162
|
+
- [ ] switch from VM2 to V8-isolate or QuickJS, which is more secure
|
|
167
163
|
|
|
168
164
|
<a name="license"></a>
|
|
169
165
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muze-nl/simplystore",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.12",
|
|
4
4
|
"main": "src/server.mjs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"homepage": "https://github.com/simplyedit/simplystore#readme",
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@muze-nl/jsontag": "^0.9.6",
|
|
20
|
-
"@muze-nl/od-jsontag": "^0.
|
|
20
|
+
"@muze-nl/od-jsontag": "^0.2.6",
|
|
21
21
|
"codemirror": "^6.0.1",
|
|
22
22
|
"express": "^4.18.1",
|
|
23
23
|
"jaqt": "^0.6.2",
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import JSONTag from '@muze-nl/jsontag'
|
|
2
|
-
import {
|
|
2
|
+
import {getIndex, resultSet} from '@muze-nl/od-jsontag/src/symbols.mjs'
|
|
3
3
|
import parse from '@muze-nl/od-jsontag/src/parse.mjs'
|
|
4
|
-
import serialize
|
|
5
|
-
import * as FastJSONTag from '@muze-nl/od-jsontag/src/jsontag.mjs'
|
|
4
|
+
import serialize from '@muze-nl/od-jsontag/src/serialize.mjs'
|
|
6
5
|
import writeFileAtomic from 'write-file-atomic'
|
|
7
|
-
import {_,from,not,anyOf,allOf,asc,desc,sum,count,avg,max,min} from 'jaqt'
|
|
8
6
|
|
|
9
7
|
let commands = {}
|
|
10
8
|
let resultArr = []
|
|
@@ -54,7 +52,9 @@ export const metaIdProxy = {
|
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
export async function initialize(task) {
|
|
57
|
-
|
|
55
|
+
for(let jsontag of task.data) {
|
|
56
|
+
dataspace = parse(jsontag, task.meta, false) // false means mutable
|
|
57
|
+
}
|
|
58
58
|
resultArr = dataspace[resultSet]
|
|
59
59
|
meta = task.meta
|
|
60
60
|
metaProxy.index.id = metaIdProxy
|
|
@@ -76,9 +76,9 @@ export default async function runCommand(commandStr, request) {
|
|
|
76
76
|
let time = Date.now()
|
|
77
77
|
commands[task.name](dataspace, task, request, metaProxy)
|
|
78
78
|
//TODO: if command/task makes no changes, skip updating data.jsontag and writing it, skip response.data
|
|
79
|
-
|
|
79
|
+
JSONTag.setAttribute(dataspace, 'command', task.id)
|
|
80
80
|
|
|
81
|
-
const uint8sab = serialize(dataspace)
|
|
81
|
+
const uint8sab = serialize(dataspace, {meta, changes: true}) // serialize only changes
|
|
82
82
|
response.data = uint8sab
|
|
83
83
|
response.meta = {
|
|
84
84
|
index: {
|
|
@@ -86,7 +86,11 @@ export default async function runCommand(commandStr, request) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
//TODO: write data every x commands or x minutes, in seperate thread
|
|
89
|
-
|
|
89
|
+
|
|
90
|
+
let newfilename = datafile + (meta.parts ? '.'+meta.parts : '')
|
|
91
|
+
await writeFileAtomic(newfilename, uint8sab)
|
|
92
|
+
meta.parts++
|
|
93
|
+
response.meta.parts = meta.parts
|
|
90
94
|
let end = Date.now()
|
|
91
95
|
console.log('task time',end-time)
|
|
92
96
|
} else {
|
package/src/load-worker.mjs
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
import { parentPort } from 'node:worker_threads'
|
|
2
|
-
import JSONTag from '@muze-nl/jsontag'
|
|
3
2
|
import parse from '@muze-nl/od-jsontag/src/parse.mjs'
|
|
4
3
|
import fs from 'fs'
|
|
5
4
|
import serialize from '@muze-nl/od-jsontag/src/serialize.mjs'
|
|
6
|
-
import {source, resultSet} from '@muze-nl/od-jsontag/src/symbols.mjs'
|
|
7
5
|
|
|
8
6
|
parentPort.on('message', datafile => {
|
|
9
|
-
const jsontag = fs.readFileSync(datafile)
|
|
10
7
|
let meta = {
|
|
11
8
|
index: {
|
|
12
9
|
id: new Map()
|
|
13
10
|
}
|
|
14
11
|
}
|
|
15
|
-
const data = parse(jsontag)
|
|
16
|
-
const resultArr = data[resultSet]
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
let
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
meta.index.id.set(id,i)
|
|
24
|
-
}
|
|
25
|
-
}
|
|
13
|
+
let count = 0
|
|
14
|
+
let basefile = datafile
|
|
15
|
+
let data
|
|
16
|
+
let jsontag
|
|
17
|
+
let tempMeta = {}
|
|
26
18
|
|
|
27
|
-
|
|
19
|
+
do {
|
|
20
|
+
jsontag = fs.readFileSync(datafile)
|
|
21
|
+
data = parse(jsontag, tempMeta) // tempMeta is needed to combine the resultArray, using meta conflicts with meta.index.id
|
|
22
|
+
count++
|
|
23
|
+
datafile = basefile + '.' + count
|
|
24
|
+
} while(fs.existsSync(datafile))
|
|
25
|
+
meta.parts = count
|
|
26
|
+
|
|
27
|
+
const sab = serialize(data, {meta})
|
|
28
28
|
|
|
29
29
|
parentPort.postMessage({
|
|
30
30
|
data: sab,
|
|
31
31
|
meta
|
|
32
32
|
})
|
|
33
|
+
|
|
33
34
|
})
|
|
@@ -4,7 +4,6 @@ import { memoryUsage } from 'node:process'
|
|
|
4
4
|
import JSONTag from '@muze-nl/jsontag'
|
|
5
5
|
import {source, isProxy, resultSet} from '@muze-nl/od-jsontag/src/symbols.mjs'
|
|
6
6
|
import parse from '@muze-nl/od-jsontag/src/parse.mjs'
|
|
7
|
-
import * as FastJSONTag from '@muze-nl/od-jsontag/src/jsontag.mjs'
|
|
8
7
|
import {_,from,not,anyOf,allOf,asc,desc,sum,count,avg,max,min} from 'jaqt'
|
|
9
8
|
|
|
10
9
|
let resultArr = []
|
|
@@ -32,14 +31,27 @@ const tasks = {
|
|
|
32
31
|
if (task.req.access) {
|
|
33
32
|
task.req.access = await import(task.req.access)
|
|
34
33
|
task.req.access = task.req.access.default
|
|
34
|
+
meta.access = task.req.access
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
if (task.req.meta.index) {
|
|
37
|
+
meta.index = task.req.meta.index
|
|
38
|
+
}
|
|
39
|
+
for (let sab of task.req.body) { //body contains an array of sharedArrayBuffers with initial data and changes
|
|
40
|
+
dataspace = parse(sab, meta)
|
|
41
|
+
}
|
|
42
|
+
resultArr = meta.resultArray
|
|
39
43
|
metaProxy.index.id = metaIdProxy
|
|
40
44
|
//@TODO: add meta.index.references? and baseURL
|
|
41
45
|
return true
|
|
42
46
|
},
|
|
47
|
+
update: async (task) => {
|
|
48
|
+
if (task.req.meta.index) {
|
|
49
|
+
meta.index = task.req.meta.index
|
|
50
|
+
}
|
|
51
|
+
dataspace = parse(task.req.body, meta) //update only has a single changeset
|
|
52
|
+
resultArr = meta.resultArray
|
|
53
|
+
return true
|
|
54
|
+
},
|
|
43
55
|
query: async (task) => {
|
|
44
56
|
return runQuery(task.req.path, task.req, task.req.body)
|
|
45
57
|
},
|
|
@@ -83,7 +95,7 @@ export function runQuery(pointer, request, query) {
|
|
|
83
95
|
max,
|
|
84
96
|
min,
|
|
85
97
|
// console: connectConsole(res),
|
|
86
|
-
JSONTag
|
|
98
|
+
JSONTag,
|
|
87
99
|
request
|
|
88
100
|
},
|
|
89
101
|
wasm: false
|
|
@@ -145,8 +157,8 @@ export function getDataSpace(path, dataspace) {
|
|
|
145
157
|
}
|
|
146
158
|
|
|
147
159
|
export function linkReplacer(data, baseURL) {
|
|
148
|
-
let type =
|
|
149
|
-
let attributes =
|
|
160
|
+
let type = JSONTag.getType(data)
|
|
161
|
+
let attributes = JSONTag.getAttributes(data)
|
|
150
162
|
if (Array.isArray(data)) {
|
|
151
163
|
data = data.map((entry,index) => {
|
|
152
164
|
return linkReplacer(data[index], baseURL+index+'/')
|
|
@@ -162,8 +174,8 @@ export function linkReplacer(data, baseURL) {
|
|
|
162
174
|
if (Array.isArray(data[key])) {
|
|
163
175
|
data[key] = new JSONTag.Link(baseURL+key+'/')
|
|
164
176
|
} else if (data[key] && typeof data[key] === 'object') {
|
|
165
|
-
if (
|
|
166
|
-
let id=
|
|
177
|
+
if (JSONTag.getType(data[key])!=='link') {
|
|
178
|
+
let id=JSONTag.getAttribute(data[key], 'id')
|
|
167
179
|
if (!id) {
|
|
168
180
|
id = baseURL+key+'/'
|
|
169
181
|
}
|
package/src/server.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import writeFileAtomic from 'write-file-atomic'
|
|
|
11
11
|
|
|
12
12
|
const server = express()
|
|
13
13
|
const __dirname = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
|
|
14
|
-
let
|
|
14
|
+
let jsontagBuffers = null
|
|
15
15
|
let meta = {}
|
|
16
16
|
|
|
17
17
|
async function main(options) {
|
|
@@ -34,7 +34,8 @@ async function main(options) {
|
|
|
34
34
|
|
|
35
35
|
// allow access to raw body, used to parse a query send as post body
|
|
36
36
|
server.use(express.raw({
|
|
37
|
-
type: (req) => true // parse body on all requests
|
|
37
|
+
type: (req) => true, // parse body on all requests
|
|
38
|
+
limit: '50MB'
|
|
38
39
|
}))
|
|
39
40
|
|
|
40
41
|
function loadData() {
|
|
@@ -53,7 +54,7 @@ async function main(options) {
|
|
|
53
54
|
}
|
|
54
55
|
try {
|
|
55
56
|
let data = await loadData()
|
|
56
|
-
|
|
57
|
+
jsontagBuffers = [data.data]
|
|
57
58
|
meta = data.meta
|
|
58
59
|
} catch(err) {
|
|
59
60
|
console.error('ERROR: SimplyStore cannot load '+datafile, err)
|
|
@@ -64,13 +65,14 @@ async function main(options) {
|
|
|
64
65
|
return {
|
|
65
66
|
name: 'init',
|
|
66
67
|
req: {
|
|
67
|
-
body:
|
|
68
|
+
body: jsontagBuffers,
|
|
68
69
|
meta,
|
|
69
70
|
access
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
|
|
74
76
|
let queryWorkerPool = new WorkerPool(maxWorkers, queryWorker, queryWorkerInitTask())
|
|
75
77
|
|
|
76
78
|
server.get('/query/*', async (req, res, next) =>
|
|
@@ -227,18 +229,18 @@ async function main(options) {
|
|
|
227
229
|
s = {code: 200, status: "done"}
|
|
228
230
|
status.set(command.id, s)
|
|
229
231
|
if (data.data) { // data has changed, commands may do other things instead of changing data
|
|
230
|
-
|
|
232
|
+
jsontagBuffers.push(data.data) // push changeset to jsontagBuffers so that new query workers get all changes from scratch
|
|
231
233
|
meta = data.meta
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
234
|
+
queryWorkerPool.update({
|
|
235
|
+
name: 'update',
|
|
236
|
+
req: {
|
|
237
|
+
body: jsontagBuffers[jsontagBuffers.length-1], // only add the last change, update tasks for earlier changes have already been sent
|
|
238
|
+
meta
|
|
239
|
+
}
|
|
240
|
+
})
|
|
238
241
|
}
|
|
239
242
|
}
|
|
240
243
|
let l = Object.assign({command:command.id}, s)
|
|
241
|
-
console.log('appending status ',l,s)
|
|
242
244
|
appendFile(commandStatus, JSONTag.stringify(Object.assign({command:command.id}, s)))
|
|
243
245
|
},
|
|
244
246
|
//reject()
|
|
@@ -276,7 +278,7 @@ async function main(options) {
|
|
|
276
278
|
command:commandStr,
|
|
277
279
|
request,
|
|
278
280
|
meta,
|
|
279
|
-
data:
|
|
281
|
+
data:jsontagBuffers,
|
|
280
282
|
commandsFile,
|
|
281
283
|
datafile
|
|
282
284
|
})
|
package/src/workerPool.mjs
CHANGED
|
@@ -19,7 +19,6 @@ class WorkerPoolTaskInfo extends AsyncResource {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
//@TODO: only create new workers when needed, not immediately
|
|
22
|
-
//@TODO: allow initialization of newly created workers
|
|
23
22
|
|
|
24
23
|
export default class WorkerPool extends EventEmitter {
|
|
25
24
|
constructor(numThreads, workerFile, initTask) {
|
|
@@ -29,13 +28,14 @@ export default class WorkerPool extends EventEmitter {
|
|
|
29
28
|
this.initTask = initTask
|
|
30
29
|
this.workers = []
|
|
31
30
|
this.freeWorkers = []
|
|
32
|
-
|
|
31
|
+
this.priority = new Map()
|
|
33
32
|
for (let i = 0; i < numThreads; i++)
|
|
34
33
|
this.addNewWorker();
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
addNewWorker() {
|
|
38
37
|
const worker = new Worker(path.resolve(this.workerFile));
|
|
38
|
+
this.priority[worker] = []
|
|
39
39
|
worker.on('message', (result) => {
|
|
40
40
|
// In case of success: Call the callback that was passed to `runTask`,
|
|
41
41
|
// remove the `TaskInfo` associated with the Worker, and mark it as free
|
|
@@ -44,8 +44,13 @@ export default class WorkerPool extends EventEmitter {
|
|
|
44
44
|
worker[kTaskInfo].done(null, result);
|
|
45
45
|
worker[kTaskInfo] = null;
|
|
46
46
|
}
|
|
47
|
-
this.
|
|
48
|
-
|
|
47
|
+
if (this.priority[worker].length) {
|
|
48
|
+
let task = this.priority[worker].shift()
|
|
49
|
+
worker.postMessage(task)
|
|
50
|
+
} else {
|
|
51
|
+
this.freeWorkers.push(worker);
|
|
52
|
+
this.emit(kWorkerFreedEvent);
|
|
53
|
+
}
|
|
49
54
|
});
|
|
50
55
|
worker.on('error', (err) => {
|
|
51
56
|
// In case of an uncaught exception: Call the callback that was passed to
|
|
@@ -86,6 +91,16 @@ export default class WorkerPool extends EventEmitter {
|
|
|
86
91
|
worker.postMessage(task);
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
update(task) {
|
|
95
|
+
for (let worker of this.workers) {
|
|
96
|
+
if (worker[kTaskInfo]) { // worker is busy
|
|
97
|
+
this.priority[worker].push(task) // push task on priority list for this specific worker
|
|
98
|
+
} else { // worker is free
|
|
99
|
+
worker.postMessage(task) // run task immediately
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
89
104
|
memoryUsage() {
|
|
90
105
|
for (let worker of this.freeWorkers) {
|
|
91
106
|
worker[kTaskInfo] = new WorkerPoolTaskInfo((result) => {})
|