@muze-nl/simplystore 0.6.10 → 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 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.10",
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.1.6",
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 = []
@@ -54,7 +52,9 @@ export const metaIdProxy = {
54
52
  }
55
53
 
56
54
  export async function initialize(task) {
57
- dataspace = parse(task.data, task.meta, false) // false means mutable
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
- FastJSONTag.setAttribute(dataspace, 'command', task.id)
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
- await writeFileAtomic(datafile, uint8sab)
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 {
@@ -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
  })
@@ -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
- dataspace = parse(task.req.body, { access: task.req.access })
37
- resultArr = dataspace[resultSet]
38
- meta = task.req.meta
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: FastJSONTag,
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 = FastJSONTag.getType(data)
149
- let attributes = FastJSONTag.getAttributes(data)
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 (FastJSONTag.getType(data[key])!=='link') {
166
- let id=FastJSONTag.getAttribute(data[key], '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 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,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
- 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()
@@ -276,7 +278,7 @@ async function main(options) {
276
278
  command:commandStr,
277
279
  request,
278
280
  meta,
279
- data:jsontagBuffer,
281
+ data:jsontagBuffers,
280
282
  commandsFile,
281
283
  datafile
282
284
  })
@@ -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) => {})