@muze-nl/simplystore 0.6.11 → 0.6.14

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 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
- - improved web client with type-specific views and form elements
161
-
162
- - add support for metadata on each JSON pointer path (or better: each object)
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.11",
3
+ "version": "0.6.14",
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.1.7",
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 {source, isChanged, getIndex, resultSet} from '@muze-nl/od-jsontag/src/symbols.mjs'
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, {stringify} from '@muze-nl/od-jsontag/src/serialize.mjs'
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 = []
@@ -53,48 +51,65 @@ export const metaIdProxy = {
53
51
  }
54
52
  }
55
53
 
54
+ const metaReadProxy = {
55
+ foreach: metaProxy.forEach,
56
+ get: metaProxy.get,
57
+ has: metaProxy.has,
58
+ set: meta.set
59
+ }
60
+
56
61
  export async function initialize(task) {
57
- dataspace = parse(task.data, task.meta, false) // false means mutable
62
+ for(let jsontag of task.data) {
63
+ dataspace = parse(jsontag, task.meta, false) // false means mutable
64
+ }
58
65
  resultArr = dataspace[resultSet]
59
66
  meta = task.meta
60
67
  metaProxy.index.id = metaIdProxy
61
68
  datafile = task.datafile
62
69
  commands = await import(task.commandsFile).then(mod => {
63
- console.log('commands loaded:',Object.keys(mod.default))
64
70
  return mod.default
65
71
  })
66
72
  }
67
73
 
68
74
  export default async function runCommand(commandStr, request) {
69
- let task = JSONTag.parse(commandStr, null, metaProxy)
70
- if (!task.id) { throw new Error('missing command id')}
71
- if (!task.name) { throw new Error('missing command name parameter')}
72
75
  let response = {
73
76
  jsontag: true
74
77
  }
75
- if (commands[task.name]) {
76
- let time = Date.now()
77
- commands[task.name](dataspace, task, request, metaProxy)
78
- //TODO: if command/task makes no changes, skip updating data.jsontag and writing it, skip response.data
79
- FastJSONTag.setAttribute(dataspace, 'command', task.id)
78
+ try {
79
+ let task = JSONTag.parse(commandStr, null, metaReadProxy)
80
+ if (!task.id) { throw new Error('missing command id')}
81
+ if (!task.name) { throw new Error('missing command name parameter')}
82
+ if (commands[task.name]) {
83
+ let time = Date.now()
84
+ commands[task.name](dataspace, task, request, metaProxy)
85
+ //TODO: if command/task makes no changes, skip updating data.jsontag and writing it, skip response.data
86
+ JSONTag.setAttribute(dataspace, 'command', task.id)
87
+
88
+ const uint8sab = serialize(dataspace, {meta, changes: true}) // serialize only changes
89
+ response.data = uint8sab
90
+ response.meta = {
91
+ index: {
92
+ id: meta.index.id
93
+ }
94
+ }
95
+ //TODO: write data every x commands or x minutes, in seperate thread
80
96
 
81
- const uint8sab = serialize(dataspace)
82
- response.data = uint8sab
83
- response.meta = {
84
- index: {
85
- id: meta.index.id
97
+ let newfilename = datafile + (meta.parts ? '.'+meta.parts : '')
98
+ await writeFileAtomic(newfilename, uint8sab)
99
+ meta.parts++
100
+ response.meta.parts = meta.parts
101
+ let end = Date.now()
102
+ console.log('task time',end-time)
103
+ } else {
104
+ console.error('Command not found', task.name)
105
+ throw {
106
+ code: 404,
107
+ message: "Command "+task.name+" not found"
86
108
  }
87
109
  }
88
- //TODO: write data every x commands or x minutes, in seperate thread
89
- await writeFileAtomic(datafile, uint8sab)
90
- let end = Date.now()
91
- console.log('task time',end-time)
92
- } else {
93
- console.error('Command not found', task.name)
94
- throw {
95
- code: 404,
96
- message: "Command "+task.name+" not found"
97
- }
110
+ } catch(err) {
111
+ console.error('task error', err)
112
+ throw err
98
113
  }
99
114
  return response
100
115
  }
@@ -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
- // od-jsontag/parse doesn't create meta.index.id, so do that here
19
- let length = resultArr.length
20
- for (let i=0; i<length; i++) {
21
- let id=JSONTag.getAttribute(resultArr[i][source],'id')
22
- if (id) {
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
- const sab = serialize(data)
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
  })
@@ -2,12 +2,11 @@ import pointer from 'json-pointer'
2
2
  import {VM} from 'vm2'
3
3
  import { memoryUsage } from 'node:process'
4
4
  import JSONTag from '@muze-nl/jsontag'
5
+ import * as odJSONTag from '@muze-nl/od-jsontag/src/jsontag.mjs'
5
6
  import {source, isProxy, resultSet} from '@muze-nl/od-jsontag/src/symbols.mjs'
6
7
  import parse from '@muze-nl/od-jsontag/src/parse.mjs'
7
- import * as FastJSONTag from '@muze-nl/od-jsontag/src/jsontag.mjs'
8
8
  import {_,from,not,anyOf,allOf,asc,desc,sum,count,avg,max,min} from 'jaqt'
9
9
 
10
- let resultArr = []
11
10
  let dataspace
12
11
  let meta = {}
13
12
  let metaProxy = {
@@ -15,11 +14,49 @@ let metaProxy = {
15
14
  }
16
15
  }
17
16
 
17
+ function protect(target) {
18
+ if (target[source]) {
19
+ throw new Error('Data is immutable')
20
+ }
21
+ }
22
+
23
+ const myJSONTag = {
24
+ getAttribute: odJSONTag.getAttribute,
25
+ getAttributes: odJSONTag.getAttributes,
26
+ getType: odJSONTag.getType,
27
+ getTypeString: odJSONTag.getTypeString,
28
+ setAttribute: (target, name, value) => {
29
+ protect(target)
30
+ return odJSONTag.setAttribute(target, name, value)
31
+ },
32
+ setType: (target, type) => {
33
+ protect(target)
34
+ return odJSONTag.setType(target, type)
35
+ },
36
+ setAttributes: (target, attributes) => {
37
+ protect(target)
38
+ return odJSONTag.setAttributes(target, attributes)
39
+ },
40
+ addAttribute: (target, name, value) => {
41
+ protect(target)
42
+ return odJSONTag.addAttribute(target, name, value)
43
+ },
44
+ removeAttribute: (target, name) => {
45
+ protect(target)
46
+ return odJSONTag.removeAttribute(target, name)
47
+ },
48
+ getAttributesString: odJSONTag.getAttributesString,
49
+ isNull: odJSONTag.isNull,
50
+ clone: JSONTag.clone,
51
+ Link: JSONTag.Link,
52
+ Null: JSONTag.Null
53
+ }
54
+
18
55
  const metaIdProxy = {
19
56
  get: (id) => {
20
57
  let index = meta.index.id.get(id)
21
58
  if (index || index===0) {
22
- return resultArr[index]
59
+ return meta.resultArr[index]
23
60
  }
24
61
  },
25
62
  has: (id) => {
@@ -32,14 +69,25 @@ const tasks = {
32
69
  if (task.req.access) {
33
70
  task.req.access = await import(task.req.access)
34
71
  task.req.access = task.req.access.default
72
+ meta.access = task.req.access
73
+ }
74
+ if (task.req.meta.index) {
75
+ meta.index = task.req.meta.index
76
+ }
77
+ for (let sab of task.req.body) { //body contains an array of sharedArrayBuffers with initial data and changes
78
+ dataspace = parse(sab, meta)
35
79
  }
36
- dataspace = parse(task.req.body, { access: task.req.access })
37
- resultArr = dataspace[resultSet]
38
- meta = task.req.meta
39
80
  metaProxy.index.id = metaIdProxy
40
81
  //@TODO: add meta.index.references? and baseURL
41
82
  return true
42
83
  },
84
+ update: async (task) => {
85
+ if (task.req.meta.index) {
86
+ meta.index = task.req.meta.index
87
+ }
88
+ dataspace = parse(task.req.body, meta) //update only has a single changeset
89
+ return true
90
+ },
43
91
  query: async (task) => {
44
92
  return runQuery(task.req.path, task.req, task.req.body)
45
93
  },
@@ -83,7 +131,7 @@ export function runQuery(pointer, request, query) {
83
131
  max,
84
132
  min,
85
133
  // console: connectConsole(res),
86
- JSONTag: FastJSONTag,
134
+ JSONTag: myJSONTag,
87
135
  request
88
136
  },
89
137
  wasm: false
@@ -145,8 +193,8 @@ export function getDataSpace(path, dataspace) {
145
193
  }
146
194
 
147
195
  export function linkReplacer(data, baseURL) {
148
- let type = FastJSONTag.getType(data)
149
- let attributes = FastJSONTag.getAttributes(data)
196
+ let type = JSONTag.getType(data)
197
+ let attributes = JSONTag.getAttributes(data)
150
198
  if (Array.isArray(data)) {
151
199
  data = data.map((entry,index) => {
152
200
  return linkReplacer(data[index], baseURL+index+'/')
@@ -162,8 +210,8 @@ export function linkReplacer(data, baseURL) {
162
210
  if (Array.isArray(data[key])) {
163
211
  data[key] = new JSONTag.Link(baseURL+key+'/')
164
212
  } else if (data[key] && typeof data[key] === 'object') {
165
- if (FastJSONTag.getType(data[key])!=='link') {
166
- let id=FastJSONTag.getAttribute(data[key], 'id')
213
+ if (JSONTag.getType(data[key])!=='link') {
214
+ let id=JSONTag.getAttribute(data[key], 'id')
167
215
  if (!id) {
168
216
  id = baseURL+key+'/'
169
217
  }
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 jsontagBuffer = null
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
- jsontagBuffer = data.data
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: jsontagBuffer,
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,22 +229,23 @@ 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
- jsontagBuffer = data.data
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
- // restart query workers with new data
233
- let oldPool = queryWorkerPool
234
- queryWorkerPool = new WorkerPool(maxWorkers, queryWorker, queryWorkerInitTask())
235
- setTimeout(() => {
236
- oldPool.close()
237
- }, 2000)
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()
245
247
  (error) => {
248
+ console.error(error)
246
249
  let s = {status: "failed", code: error.code, message: error.message, details: error.details}
247
250
  status.set(command.id, s)
248
251
  appendFile(commandStatus, JSONTag.stringify(Object.assign({command:command.id}, s)))
@@ -276,7 +279,7 @@ async function main(options) {
276
279
  command:commandStr,
277
280
  request,
278
281
  meta,
279
- data:jsontagBuffer,
282
+ data:jsontagBuffers,
280
283
  commandsFile,
281
284
  datafile
282
285
  })
@@ -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.freeWorkers.push(worker);
48
- this.emit(kWorkerFreedEvent);
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) => {})