@muze-nl/simplystore 0.7.4 → 0.9.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.
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  SimplyStore is a radically simpler backend storage server. It does not have a database, certainly no SQL or GraphQL, it is not REST. In return it has a well defined API that is automatically derived from your dataset. It supports JSONTag to allow for semantically meaningful data, without having to do the full switch to Linked Data and triple stores. The query format is javascript, you can post javascript queries that will run on the server. All data is read into memory and is available to these javascript queries without needing (or allowing) disk access or indexes.
4
4
 
5
- [JSONTag](https://github.com/poef/jsontag) is an enhancement over JSON that allows you to tag JSON data with metadata using HTML-like tags.
5
+ [JSONTag](https://github.com/muze-nl/jsontag) is an enhancement over JSON that allows you to tag JSON data with metadata using HTML-like tags.
6
6
  Javascript queries are run in a [VM2](https://www.npmjs.com/package/vm2) sandbox.
7
- You can query data using the [array-where-select](https://www.npmjs.com/package/array-where-select) extension.
7
+ You can query data using the [jaqt](https://github.com/muze-nl/jaqt/) library.
8
8
 
9
9
  Note: _There are known security issues in VM2, so the project will switch to V8-isolate. For now make sure SimplyStore is not publically accessible, by adding an api gateway in front of it for example_
10
10
 
@@ -168,4 +168,4 @@ In addition, SimplyStore is meant to be a real-world testcase for JSONTag.
168
168
  [MIT](LICENSE) © Muze.nl
169
169
 
170
170
  ## Contributions
171
- Contributions are welcome, but make sure that all code is MIT licensed. If you want to send a merge request, please make sure that there is a ticket that shows the bug/feature and reference it. If you find any problem, please do file a ticket, but you should not expect a timely resolution. This project is still very experimental, don't use it in production unless you are ready to fix problems yourself.
171
+ Contributions are welcome, but make sure that all code is MIT licensed. If you want to send a merge request, please make sure that there is a ticket that shows the bug/feature and reference it. If you find any problem, please do file a ticket, but you should not expect a timely resolution. This project is still very experimental, don't use it in production unless you are ready to fix problems yourself.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muze-nl/simplystore",
3
- "version": "0.7.4",
3
+ "version": "0.9.0",
4
4
  "main": "src/server.mjs",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -16,9 +16,9 @@
16
16
  "bugs": "https://github.com/simplyedit/simplystore/issues",
17
17
  "homepage": "https://github.com/simplyedit/simplystore#readme",
18
18
  "dependencies": {
19
- "@muze-nl/jaqt": "^0.10.2",
20
- "@muze-nl/jsontag": "^0.9.11",
21
- "@muze-nl/od-jsontag": "^0.3.4",
19
+ "@muze-nl/jaqt": "^0.10.3",
20
+ "@muze-nl/jsontag": "^0.10.2",
21
+ "@muze-nl/od-jsontag": "^0.4.1",
22
22
  "codemirror": "^6.0.1",
23
23
  "express": "^4.18.1",
24
24
  "json-pointer": "^0.6.2",
@@ -1,6 +1,6 @@
1
1
  import JSONTag from '@muze-nl/jsontag'
2
2
  import {getIndex, resultSet} from '@muze-nl/od-jsontag/src/symbols.mjs'
3
- import parse from '@muze-nl/od-jsontag/src/parse.mjs'
3
+ import Parser from '@muze-nl/od-jsontag/src/parse.mjs'
4
4
  import serialize from '@muze-nl/od-jsontag/src/serialize.mjs'
5
5
  import writeFileAtomic from 'write-file-atomic'
6
6
 
@@ -13,10 +13,12 @@ let metaProxy = {
13
13
  index: {
14
14
  }
15
15
  }
16
+ const parser = new Parser()
17
+ parser.immutable = false
16
18
 
17
19
  export const metaIdProxy = {
18
20
  forEach: (callback) => {
19
- meta.index.id.forEach((ref,id) => {
21
+ parser.meta.index.id.forEach((ref,id) => {
20
22
  callback({
21
23
  deref: () => {
22
24
  return resultArr[ref]
@@ -25,19 +27,19 @@ export const metaIdProxy = {
25
27
  })
26
28
  },
27
29
  set: (id,ref) => {
28
- if (!meta.index.id.has(id)) {
30
+ if (!parser.meta.index.id.has(id)) {
29
31
  if (ref[getIndex]) {
30
- meta.index.id.set(id, ref[getIndex])
32
+ parser.meta.index.id.set(id, ref[getIndex])
31
33
  } else {
32
34
  throw new Error('cannot set index.id for non-proxy')
33
35
  }
34
36
  } else {
35
- let line = meta.index.id.get(id)
37
+ let line = parser.meta.index.id.get(id)
36
38
  resultArr[line] = ref
37
39
  }
38
40
  },
39
41
  get: (id) => {
40
- let index = meta.index.id.get(id)
42
+ let index = parser.meta.index.id.get(id)
41
43
  if (index || index===0) {
42
44
  return {
43
45
  deref: () => {
@@ -47,7 +49,7 @@ export const metaIdProxy = {
47
49
  }
48
50
  },
49
51
  has: (id) => {
50
- return meta.index.id.has(id)
52
+ return parser.meta.index.id.has(id)
51
53
  }
52
54
  }
53
55
 
@@ -59,8 +61,11 @@ const metaReadProxy = {
59
61
  }
60
62
 
61
63
  export async function initialize(task) {
64
+ if (task.meta) {
65
+ parser.meta = task.meta
66
+ }
62
67
  for(let jsontag of task.data) {
63
- dataspace = parse(jsontag, task.meta, false) // false means mutable
68
+ dataspace = parser.parse(jsontag)
64
69
  }
65
70
  resultArr = dataspace[resultSet]
66
71
  meta = task.meta
@@ -88,7 +93,6 @@ export default async function runCommand(commandStr, request) {
88
93
  let time = Date.now()
89
94
  commands[task.name](dataspace, task, request, metaProxy)
90
95
  //TODO: if command/task makes no changes, skip updating data.jsontag and writing it, skip response.data
91
- JSONTag.setAttribute(dataspace, 'command', task.id)
92
96
 
93
97
  const uint8sab = serialize(dataspace, {meta, changes: true}) // serialize only changes
94
98
  response.data = uint8sab
@@ -1,9 +1,10 @@
1
1
  import { parentPort } from 'node:worker_threads'
2
2
  import JSONTag from '@muze-nl/jsontag'
3
- import parse from '@muze-nl/od-jsontag/src/parse.mjs'
3
+ import Parser from '@muze-nl/od-jsontag/src/parse.mjs'
4
4
  import fs from 'fs'
5
5
  import serialize from '@muze-nl/od-jsontag/src/serialize.mjs'
6
6
 
7
+ const parser = new Parser()
7
8
  parentPort.on('message', (files) => {
8
9
  let meta = {
9
10
  index: {
@@ -17,22 +18,20 @@ parentPort.on('message', (files) => {
17
18
  let count = 0
18
19
  let data
19
20
  let jsontag
20
- let tempMeta = {}
21
21
  let datafile = files.dataFile
22
22
  let commands = files.commands
23
23
  commands.push('done')
24
24
  do {
25
25
  if (fs.existsSync(datafile)) {
26
26
  jsontag = fs.readFileSync(datafile)
27
- data = parse(jsontag, tempMeta) // tempMeta is needed to combine the resultArray, using meta conflicts with meta.index.id
27
+ data = parser.parse(jsontag)
28
28
  count++
29
29
  }
30
30
  datafile = basefile + '.' + commands.shift() + '.' + extension
31
31
  } while(commands.length)
32
- meta.parts = count
33
32
  if (files.schemaFile) {
34
33
  jsontag = fs.readFileSync(files.schemaFile, 'utf-8')
35
- meta.schema = JSONTag.parse(jsontag, null, tempMeta)
34
+ meta.schema = JSONTag.parse(jsontag)
36
35
  }
37
36
 
38
37
  const sab = serialize(data, {meta})
@@ -2,66 +2,28 @@ 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'
6
5
  import {source} from '@muze-nl/od-jsontag/src/symbols.mjs'
7
- import parse from '@muze-nl/od-jsontag/src/parse.mjs'
6
+ import Parser from '@muze-nl/od-jsontag/src/parse.mjs'
8
7
  import {_,from,not,anyOf,allOf,asc,desc,sum,count,avg,max,min,many,one,distinct} from '@muze-nl/jaqt'
9
8
  import process from 'node:process'
10
9
 
11
10
  let dataspace
12
- let meta = {}
13
11
  let metaProxy = {
14
12
  index: {
15
13
  }
16
14
  }
17
15
 
18
- function protect(target) {
19
- if (target[source]) {
20
- throw new Error('Data is immutable')
21
- }
22
- }
23
-
24
- const myJSONTag = {
25
- getAttribute: odJSONTag.getAttribute,
26
- getAttributes: odJSONTag.getAttributes,
27
- getType: odJSONTag.getType,
28
- getTypeString: odJSONTag.getTypeString,
29
- setAttribute: (target, name, value) => {
30
- protect(target)
31
- return odJSONTag.setAttribute(target, name, value)
32
- },
33
- setType: (target, type) => {
34
- protect(target)
35
- return odJSONTag.setType(target, type)
36
- },
37
- setAttributes: (target, attributes) => {
38
- protect(target)
39
- return odJSONTag.setAttributes(target, attributes)
40
- },
41
- addAttribute: (target, name, value) => {
42
- protect(target)
43
- return odJSONTag.addAttribute(target, name, value)
44
- },
45
- removeAttribute: (target, name) => {
46
- protect(target)
47
- return odJSONTag.removeAttribute(target, name)
48
- },
49
- getAttributesString: odJSONTag.getAttributesString,
50
- isNull: odJSONTag.isNull,
51
- clone: JSONTag.clone,
52
- Link: JSONTag.Link,
53
- Null: JSONTag.Null
54
- }
16
+ const parser = new Parser()
55
17
 
56
18
  const metaIdProxy = {
57
19
  get: (id) => {
58
- let index = meta.index.id.get(id)
20
+ let index = parser.meta.index.id.get(id)
59
21
  if (index || index===0) {
60
- return meta.resultArray[index]
22
+ return parser.meta.resultArray[index]
61
23
  }
62
24
  },
63
25
  has: (id) => {
64
- return meta.index.id.has(id)
26
+ return parser.meta.index.id.has(id)
65
27
  }
66
28
  }
67
29
 
@@ -70,27 +32,27 @@ const tasks = {
70
32
  if (task.req.access) {
71
33
  task.req.access = await import(task.req.access)
72
34
  task.req.access = task.req.access.default
73
- meta.access = task.req.access
35
+ parser.meta.access = task.req.access
74
36
  }
75
37
  if (task.req.meta.index) {
76
- meta.index = task.req.meta.index
38
+ parser.meta.index = task.req.meta.index
77
39
  }
78
40
  if (task.req.meta.schema) {
79
- meta.schema = task.req.meta.schema
41
+ parser.meta.schema = task.req.meta.schema
80
42
  }
81
43
  for (let sab of task.req.body) { //body contains an array of sharedArrayBuffers with initial data and changes
82
- dataspace = parse(sab, meta)
44
+ dataspace = parser.parse(sab)
83
45
  }
84
46
  metaProxy.index.id = metaIdProxy
85
- metaProxy.schema = meta.schema
47
+ metaProxy.schema = parser.meta.schema
86
48
  //@TODO: add meta.index.references? and baseURL
87
49
  return true
88
50
  },
89
51
  update: async (task) => {
90
52
  if (task.req.meta.index) {
91
- meta.index = task.req.meta.index
53
+ parser.meta.index = task.req.meta.index
92
54
  }
93
- dataspace = parse(task.req.body, meta) //update only has a single changeset
55
+ dataspace = parser.parse(task.req.body) //update only has a single changeset
94
56
  return true
95
57
  },
96
58
  query: async (task) => {
@@ -139,7 +101,7 @@ export function runQuery(pointer, request, query) {
139
101
  one,
140
102
  distinct,
141
103
  // console: connectConsole(res),
142
- JSONTag: myJSONTag,
104
+ JSONTag,
143
105
  request
144
106
  },
145
107
  wasm: false
package/src/server.mjs CHANGED
@@ -40,7 +40,6 @@ async function main(options) {
40
40
  }))
41
41
 
42
42
  let status = loadCommandStatus(commandStatus)
43
- let commandQueue = loadCommandLog(status, commandLog)
44
43
 
45
44
  try {
46
45
  let data = await loadData(Array.from(status.keys())) // command id's (keys) are used to generate filenames of changes
@@ -51,6 +50,8 @@ async function main(options) {
51
50
  process.exit(1)
52
51
  }
53
52
 
53
+ let commandQueue = loadCommandLog(status, commandLog)
54
+
54
55
  const queryWorkerInitTask = () => {
55
56
  return {
56
57
  name: 'init',
@@ -77,6 +78,14 @@ async function main(options) {
77
78
  console.error(`Port ${port} is already occupied, aborting.`)
78
79
  process.exit()
79
80
  } catch(err) {
81
+ let result
82
+ do {
83
+ try {
84
+ result = await runNextCommand()
85
+ } catch(err) {
86
+ // console.log(err) // ignore errors here, already logged to console
87
+ }
88
+ } while(result)
80
89
  server.listen(port, () => {
81
90
  console.log('SimplyStore listening on port '+port)
82
91
  let used = Math.round(process.memoryUsage().rss / 1024 / 1024);
@@ -115,10 +124,18 @@ async function main(options) {
115
124
  let lines = log.split("\n").filter(Boolean)
116
125
  for(let line of lines) {
117
126
  let command = JSONTag.parse(line)
118
- let state = status.get(command.id)
127
+ let state = status.get(command.id)?.status
119
128
  switch(state) {
120
129
  case 'accepted': // enqueue
121
- commands.push(command)
130
+ commands.push({
131
+ id: command.id,
132
+ command: line,
133
+ request: null,
134
+ meta,
135
+ data:jsontagBuffers,
136
+ commandsFile,
137
+ datafile
138
+ })
122
139
  break;
123
140
  case 'done': // do nothing
124
141
  break;
@@ -238,8 +255,10 @@ async function main(options) {
238
255
  commandsFile,
239
256
  datafile
240
257
  })
241
-
242
- runNextCommand()
258
+ let result
259
+ do {
260
+ result = await runNextCommand()
261
+ } while(result)
243
262
  } catch(err) {
244
263
  let s = {code:err.code||500, status:'failed', message:err.message, details:err.details}
245
264
  status.set(commandId, s)
@@ -265,67 +284,80 @@ async function main(options) {
265
284
  }
266
285
 
267
286
  async function runNextCommand() {
268
- let command = commandQueue.shift()
269
- if (command) {
270
- let start = (resolve, reject) => {
271
- if (!commandWorkerInstance) {
287
+ return new Promise(async (mainResolve, mainReject) => {
288
+ if (commandWorkerInstance) {
289
+ mainReject('commandWorker already running')
290
+ return
291
+ }
292
+ let command = commandQueue.shift()
293
+ if (command) {
294
+ console.log('starting command',command.id)
295
+ let start = (resolve, reject) => {
272
296
  commandWorkerInstance = new Worker(commandWorker)
297
+ commandWorkerInstance.on('message', async result => {
298
+ await commandWorkerInstance.terminate()
299
+ commandWorkerInstance = null
300
+ resolve(result)
301
+ })
302
+ commandWorkerInstance.on('error', async error => {
303
+ await commandWorkerInstance.terminate()
304
+ commandWorkerInstance = null
305
+ reject(error)
306
+ })
307
+ commandWorkerInstance.postMessage(command)
273
308
  }
274
- commandWorkerInstance.on('message', result => {
275
- resolve(result)
276
- runNextCommand()
277
- })
278
- commandWorkerInstance.on('error', error => {
279
- reject(error)
280
- runNextCommand()
281
- })
282
- commandWorkerInstance.postMessage(command)
283
- }
284
- start(
285
- // resolve()
286
- (data) => {
287
- let s
288
- if (!data || (data.code>=300 && data.code<=499)) {
289
- console.error('ERROR: SimplyStore cannot run command ', command.id, data)
290
- if (!data?.code) {
291
- s = {code: 500, status: "failed"}
309
+ start(
310
+ // resolve()
311
+ (data) => {
312
+ let s
313
+ if (!data || (data.code>=300 && data.code<=499)) {
314
+ console.error('ERROR: SimplyStore cannot run command ', command.id, data)
315
+ if (!data?.code) {
316
+ s = {code: 500, status: "failed"}
317
+ } else {
318
+ s = {code: data.code, status: "failed", message: data.message, details: data.details}
319
+ }
320
+ status.set(command.id, s)
321
+ appendFile(commandStatus, JSONTag.stringify(Object.assign({command:command.id}, s)))
322
+ mainReject(s)
292
323
  } else {
293
- s = {code: data.code, status: "failed", message: data.message, details: data.details}
324
+ s = {code: 200, status: "done"}
325
+ status.set(command.id, s)
326
+ if (data.data) { // data has changed, commands may do other things instead of changing data
327
+ jsontagBuffers.push(data.data) // push changeset to jsontagBuffers so that new query workers get all changes from scratch
328
+ Object.assign(meta, data.meta)
329
+ queryWorkerPool.update({
330
+ name: 'update',
331
+ req: {
332
+ body: jsontagBuffers[jsontagBuffers.length-1], // only add the last change, update tasks for earlier changes have already been sent
333
+ meta
334
+ }
335
+ })
336
+ }
337
+ appendFile(commandStatus, JSONTag.stringify(Object.assign({command:command.id}, s)))
338
+ mainResolve(s)
294
339
  }
340
+ },
341
+ //reject()
342
+ (error) => {
343
+ let s = {status: "failed", code: error.code, message: error.message, details: error.details}
295
344
  status.set(command.id, s)
296
- } else {
297
- s = {code: 200, status: "done"}
298
- status.set(command.id, s)
299
- if (data.data) { // data has changed, commands may do other things instead of changing data
300
- jsontagBuffers.push(data.data) // push changeset to jsontagBuffers so that new query workers get all changes from scratch
301
- Object.assign(meta, data.meta)
302
- queryWorkerPool.update({
303
- name: 'update',
304
- req: {
305
- body: jsontagBuffers[jsontagBuffers.length-1], // only add the last change, update tasks for earlier changes have already been sent
306
- meta
307
- }
308
- })
309
- }
345
+ appendFile(commandStatus, JSONTag.stringify(Object.assign({command:command.id}, s)))
346
+ console.log('command error', command.id, error)
347
+ mainReject(s)
310
348
  }
311
- appendFile(commandStatus, JSONTag.stringify(Object.assign({command:command.id}, s)))
312
- },
313
- //reject()
314
- (error) => {
315
- console.error(error)
316
- let s = {status: "failed", code: error.code, message: error.message, details: error.details}
317
- status.set(command.id, s)
318
- appendFile(commandStatus, JSONTag.stringify(Object.assign({command:command.id}, s)))
349
+ )
350
+ } else {
351
+ console.log('no pending commands')
352
+ // this code can never be triggered from the post(/command/) route, since it always adds a command to the queue
353
+ // so you can only get here from commandWorkerInstance.on() route
354
+ // which means that the commandWorkerInstance has finished running the previous command
355
+ if (commandWorkerInstance) {
356
+ await commandWorkerInstance.terminate()
319
357
  }
320
- )
321
- } else {
322
- // this code can never be triggered from the post(/command/) route, since it always adds a command to the queue
323
- // so you can only get here from commandWorkerInstance.on() route
324
- // which means that the commandWorkerInstance has finished running the previous command
325
- await commandWorkerInstance.terminate()
326
- commandWorkerInstance.unref() // @FIXME is this needed?
327
- commandWorkerInstance = null // @FIXME or this?
328
- }
358
+ mainResolve(false)
359
+ }
360
+ })
329
361
  }
330
362
 
331
363
  function checkCommand(req, res) {
@@ -368,13 +400,12 @@ async function main(options) {
368
400
  sendResponse({code:422, body: JSON.stringify(error)}, res)
369
401
  return false
370
402
  }
371
- appendFile(commandLog, JSONTag.stringify(command))
403
+ appendFile(commandLog, JSONTag.stringify(command)) //FIXME: this loses request data
372
404
  appendFile(commandStatus, JSONTag.stringify(commandOK))
373
405
  status.set(command.id, commandOK)
374
406
  sendResponse({code: 202, body: JSON.stringify(commandOK)}, res)
375
407
  return command.id
376
408
  }
377
-
378
409
  }
379
410
 
380
411
  function sendResponse(response, res) {
@@ -435,4 +466,4 @@ function handleWebRequest(req,res,options)
435
466
  } else {
436
467
  res.sendFile('/index.html', fileOptions)
437
468
  }
438
- }
469
+ }