@muze-nl/simplystore 0.3.2 → 0.3.3

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/src/server.mjs CHANGED
@@ -1,83 +1,40 @@
1
1
  import express from 'express'
2
2
  import fs from 'fs'
3
- import pointer from 'json-pointer'
4
3
  import JSONTag from '@muze-nl/jsontag'
5
- import {JSONPath} from 'jsonpath-plus'
6
- import {VM} from 'vm2'
7
- import {_,from,not,anyOf,allOf} from 'array-where-select'
8
4
  import { fileURLToPath } from 'url'
9
5
  import path from 'path'
6
+ import { spawn, Pool, Worker } from 'threads'
7
+ import commands from './commands.mjs'
8
+ import {appendFile} from './util.mjs'
10
9
 
11
10
  const __dirname = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
12
11
 
13
12
  const server = express()
14
13
 
15
- function deepFreeze(obj) {
16
- Object.freeze(obj)
17
- Object.keys(obj).forEach(prop => {
18
- if (typeof obj[prop] === 'object' && !Object.isFrozen(obj[prop])) {
19
- deepFreeze(obj[prop])
20
- }
21
- })
22
- return obj
23
- }
24
-
25
- function isString(s)
26
- {
27
- return typeof s === 'string' || s instanceof String
28
- }
29
-
30
- function joinArgs(args) {
31
- return args = args.map(arg => {
32
- if (isString(arg)) {
33
- return arg
34
- } else {
35
- return JSONTag.stringify(arg)
36
- }
37
- }).join(' ')
38
- }
39
-
40
- function connectConsole(res) {
41
- return {
42
- log: function(...args) {
43
- res.append('X-Console-Log', joinArgs(args))
44
- },
45
- warning: function(...args) {
46
- res.append('X-Console-Warning', joinArgs(args))
47
- },
48
- error: function(...args) {
49
- res.append('X-Console-Error', joinArgs(args))
50
- }
51
- }
52
- }
53
-
54
14
  async function main(options) {
55
15
  if (!options) {
56
16
  options = {}
57
17
  }
58
- const port = options.port || 3000
59
- const datafile = options.datafile || 'data.jsontag'
60
- const wwwroot = options.wwwroot || __dirname+'/www'
61
- let meta = options.meta || {}
62
- let dataspace = options.dataspace || null
18
+ const port = options.port || 3000
19
+ const datafile = options.datafile || 'data.jsontag'
20
+ const wwwroot = options.wwwroot || __dirname+'/www'
21
+ const commandLog = options.commandlog || 'commandlog.jsontag'
22
+ let meta = options.meta || {}
63
23
 
64
- const originalJSON = JSON
65
- JSON = JSONTag // monkeypatching
24
+ let jsontag = fs.readFileSync(datafile, 'utf-8')
66
25
 
67
- if (!dataspace) {
68
- console.log('loading data...')
69
- let file = fs.readFileSync(datafile)
70
- try {
71
- dataspace = JSONTag.parse(file.toString(), null, meta)
72
- } catch(e) {
73
- console.error(e)
74
- process.exit()
75
- }
26
+ function initWorkerPool(workerName, size=null) {
27
+ return Pool(() => {
28
+ return spawn(new Worker(workerName)).then(worker => {
29
+ worker.initialize(jsontag)
30
+ return worker
31
+ })
32
+ }, size)
76
33
  }
77
- deepFreeze(dataspace)
78
34
 
79
- let used = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
80
- console.log(`data loaded (${used} MB)`);
35
+ let queryWorkerpool = initWorkerPool('./worker-query')
36
+
37
+ const commandWorkerpool = initWorkerPool('./worker-command',1) // only one update worker so no changes can get lost
81
38
 
82
39
  server.get('/', (req,res) => {
83
40
  res.send('<h1>SimplyStore</h1>') //TODO: implement something nice
@@ -103,35 +60,34 @@ async function main(options) {
103
60
  return true
104
61
  }
105
62
 
106
- function getDataSpace(req, res, dataspace) {
107
- let path = req.path.substr(6); // cut '/query'
108
- if (!path) {
109
- path = '';
63
+ function sendResponse(response, res) {
64
+ if (response.code) {
65
+ res.status(response.code)
110
66
  }
111
- if (path.substring(path.length-1)==='/') {
112
- //jsonpointer doesn't allow a trailing '/'
113
- path = path.substring(0, path.length-1)
67
+ if (response.jsontag) {
68
+ res.setHeader('content-type','application/jsontag')
69
+ } else {
70
+ res.setHeader('content-type','application/json')
114
71
  }
115
- let result
116
- if (path) {
117
- //jsonpointer doesn't allow an empty pointer
118
- try {
119
- if (pointer.has(dataspace, path)) {
120
- result = pointer.get(dataspace, path)
121
- } else {
122
- result = JSONTag.parse('<object class="Error">{"message":"Not found", "code":404}')
123
- }
124
- } catch(err) {
125
- result = JSONTag.parse('<object class="Error">{"message":'+originalJSON.stringify(err.message)+', "code":500}')
126
- }
72
+ res.send(response.body)+"\n"
73
+ }
74
+
75
+ function sendCommandResponse(result, req, res) {
76
+ if (result.code) {
77
+ res.status(result.code)
78
+ }
79
+ if (req.accepts('application/jsontag')) {
80
+ res.setHeader('content-type','application/jsontag')
81
+ res.send(JSONTag.stringify(result, null, 4)+"\n")
127
82
  } else {
128
- result = dataspace
83
+ res.setHeader('content-type','application/json')
84
+ res.send(JSON.stringify(result, null, 4)+"\n")
129
85
  }
130
- return [result,path]
131
86
  }
132
-
87
+
133
88
  server.get('/query/*', (req, res, next) =>
134
89
  {
90
+ console.log('express query')
135
91
  let start = Date.now()
136
92
 
137
93
  if ( !accept(req,res,
@@ -151,21 +107,20 @@ async function main(options) {
151
107
  // done
152
108
  return
153
109
  }
154
-
155
- let [result,path] = getDataSpace(req, res, dataspace)
156
- if (JSONTag.getAttribute(result, 'class')==='Error') {
157
- res.status(result.code)
158
- }
159
- result = linkReplacer(result, path+'/')
160
- if (req.accepts('application/jsontag')) {
161
- res.setHeader('content-type','application/jsontag+json')
162
- res.send(JSONTag.stringify(result, null, 4)+"\n")
163
- } else {
164
- res.setHeader('content-type','application/json')
165
- res.send(originalJSON.stringify(result, null, 4)+"\n")
110
+ let path = req.path.substr(6); // cut '/query'
111
+ let request = {
112
+ method: req.method,
113
+ url: req.originalUrl,
114
+ query: req.query,
115
+ jsontag: req.accepts('application/jsontag')
166
116
  }
167
- let end = Date.now()
168
- console.log(path, (end-start), process.memoryUsage())
117
+ queryWorkerpool.queue(queryWorker => {
118
+ return queryWorker.runQuery(path, request)
119
+ }).then(response => {
120
+ sendResponse(response, res)
121
+ let end = Date.now()
122
+ console.log(path, (end-start), process.memoryUsage())
123
+ })
169
124
  })
170
125
 
171
126
  /**
@@ -178,62 +133,112 @@ async function main(options) {
178
133
  ) {
179
134
  return
180
135
  }
181
- let error
182
- let [result,path] = getDataSpace(req, res, dataspace)
183
- if (result) {
184
- // do the query here
185
- let query = req.body.toString() // raw body through express.raw()
186
- // @todo add text search: https://github.com/nextapps-de/flexsearch
187
- // @todo add tree walk map/reduce/find/filter style functions
188
- // @todo add arc tree dive function?
189
- const vm = new VM({
190
- // timeout: 1000,
191
- allowAsync: false,
192
- sandbox: {
193
- root: dataspace,
194
- data: result,
195
- meta: meta,
196
- _: _,
197
- from: from,
198
- not: not,
199
- anyOf: anyOf,
200
- allOf: allOf,
201
- console: connectConsole(res),
202
- JSONTag: JSONTag,
203
- request: {
204
- method: req.method,
205
- url: req.originalUrl,
206
- query: req.query
207
- }
208
- },
209
- wasm: false
210
- })
211
- try {
212
- result = vm.run(query)
213
- let used = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
214
- console.log(`(${used} MB)`);
215
- } catch(err) {
216
- console.log(err)
217
- error = JSONTag.parse('<object class="Error">{"message":'+originalJSON.stringify(''+err)+',"code":422}')
218
- }
136
+ let query = req.body.toString() // raw body through express.raw()
137
+ let path = req.path.substr(6); // cut '/query'
138
+ let request = {
139
+ method: req.method,
140
+ url: req.originalUrl,
141
+ query: req.query,
142
+ jsontag: req.accepts('application/jsontag')
219
143
  }
144
+ queryWorkerpool.queue(queryWorker => {
145
+ return queryWorker.runQuery(path, request, query)
146
+ }).then(response => {
147
+ sendResponse(response, res)
148
+ let end = Date.now()
149
+ console.log(path, (end-start), process.memoryUsage())
150
+ })
151
+ })
220
152
 
221
- if (error) {
222
- res.status(error.code)
223
- result = error
224
- }
225
- if (req.accepts('application/jsontag')) {
226
- res.setHeader('content-type','application/jsontag+json')
227
- res.send(JSONTag.stringify(result, null, 4)+"\n")
153
+ let status = new Map()
154
+
155
+ server.get('/command/:id', (req, res) => {
156
+ //@TODO: find the status of command with :id
157
+ //return that
158
+ if (status.has(req.params.id)) {
159
+ let result = status.get(req.params.id)
160
+ sendResponse({
161
+ jsontag: false,
162
+ body: JSON.stringify(result)
163
+ },res)
228
164
  } else {
229
- res.setHeader('content-type','application/json')
230
- res.send(originalJSON.stringify(result, null, 4)+"\n")
165
+ sendResponse({
166
+ code: 404,
167
+ jsontag: false,
168
+ body: JSON.stringify({"code":404,"message":"Command not found"})
169
+ }, res)
231
170
  }
232
- let end = Date.now()
233
- console.log(path, (end-start), process.memoryUsage())
234
171
  })
235
172
 
173
+ server.post('/command', async (req, res) => {
174
+ let start = Date.now()
175
+ if ( !accept(req,res,
176
+ ['application/jsontag','application/json'])
177
+ ) {
178
+ return
179
+ }
180
+ let error, result
181
+
182
+ let commandStr = req.body.toString() // raw body through express.raw()
183
+ let command = JSONTag.parse(commandStr)
184
+ if (!command.id) {
185
+ error = {
186
+ code: 422,
187
+ message: "Command has no id"
188
+ }
189
+ sendCommandResponse(error, req, res)
190
+ return
191
+ } else if (status.has(command.id)) {
192
+ result = "OK"
193
+ sendCommandResponse(result, req, res)
194
+ return
195
+ } else if (!command.name || !commands[command.name]) {
196
+ error = {
197
+ code: 422,
198
+ message: "Command has no name or is unknown"
199
+ }
200
+ sendCommandResponse(error, req, res)
201
+ return
202
+ }
203
+ await appendFile(commandLog, JSONTag.stringify(command))
204
+
205
+ status.set(command.id, 'queued')
206
+ console.log('command',command)
207
+
208
+ result = "OK"
209
+ sendCommandResponse(result, req, res)
210
+ let request = {
211
+ method: req.method,
212
+ url: req.originalUrl,
213
+ query: req.query,
214
+ jsontag: req.accepts('application/jsontag')
215
+ }
216
+
217
+ commandWorkerpool
218
+ .queue(commandWorker => commandWorker.runCommand(request, commandStr))
219
+ .then(response => {
220
+ //@TODO store response status, if response.code => error
221
+ if (!response.code) {
222
+ jsontag = response.body // global jsontag
223
+ let dataspace = JSONTag.parse(jsontag)
224
+ //@TODO: make sure queryWorkerpool is only replaced after
225
+ //workers are initialized, to prevent hickups if initialization takes a long time
226
+ let newQueryWorkerpool = initWorkerPool('./worker-query')
227
+ queryWorkerpool.terminate() // gracefully
228
+ queryWorkerpool = newQueryWorkerpool
229
+ //@TODO: write dataspace to disk
230
+ status.set(command.id, 'done')
231
+ let end = Date.now()
232
+ console.log(command.name, (end-start), process.memoryUsage())
233
+ }
234
+ })
235
+ .catch(err => {
236
+ console.error(err)
237
+ //@TODO: set status for this command to error with this err
238
+ status.set(command.id, err)
239
+ })
236
240
 
241
+ })
237
242
 
238
243
  function handleWebRequest(req,res,options)
239
244
  {
package/src/share.mjs ADDED
@@ -0,0 +1,159 @@
1
+ import JSONTag from '@muze-nl/jsontag'
2
+
3
+ const handler = function(root, index, buffer)
4
+ {
5
+ return {
6
+ get(target, key, receiver)
7
+ {
8
+ //@FIXME: array.length needs to be handled
9
+ //@FIXME: array functions need to be handled
10
+ let keyNumber = index.key[key]
11
+ if (root[keyNumber]) {
12
+ let keyStart = root[keyNumber].s + root.s
13
+ let keyEnd = root[keyNumber].e + root.s
14
+ // subobjecten hoeven niet geparsed...
15
+ if (root[keyNumber].c) {
16
+ // object
17
+ return new jsontagProxy(root[keyNumber], index, buffer)
18
+ }
19
+ return JSON.parse(buffer.slice(keyStart,keyEnd))
20
+ }
21
+ },
22
+
23
+ set() {
24
+ throw new Error('This data is immutable')
25
+ },
26
+
27
+ deleteProperty() {
28
+ throw new Error('This data is immutable')
29
+ },
30
+
31
+ has(target, key)
32
+ {
33
+ let keyNumber = index.key[key]
34
+ return typeof root[keyNumber] !== 'undefined'
35
+ },
36
+
37
+ ownKeys(target)
38
+ {
39
+ return root.c.map(n => index.reverse[n])
40
+ }
41
+ }
42
+ }
43
+
44
+ export function getJsontagProxy(root, index, buffer) {
45
+ return new Proxy({}, handler(root, index, buffer))
46
+ }
47
+
48
+ export function share(data)
49
+ {
50
+ let jsontag = JSONTag.stringify(data)
51
+ let buffer = new SharedArrayBuffer(jsontag.length)
52
+ let dv = new DataView(buffer)
53
+ let encoder = new TextEncoder('utf-8')
54
+ let root = {}
55
+ let index = {
56
+ key: {},
57
+ reverse: [],
58
+ types: {}
59
+ }
60
+ let current = 0
61
+ let seen = new Map()
62
+
63
+ function getKey(key) {
64
+ let l;
65
+ if (typeof index.key[key] == 'undefined') {
66
+ l = Object.keys(index.key).length
67
+ index.key[key] = l
68
+ index.reverse[l] = key
69
+ }
70
+ return index.key[key]
71
+ }
72
+
73
+ function getType(type) {
74
+ if (typeof index.types[type] == 'undefined') {
75
+ let l = Object.keys(index.types).length;
76
+ index.types[type] = l
77
+ }
78
+
79
+ }
80
+
81
+ function store(container, key, value) {
82
+ let l = getKey(key)
83
+ if (seen.has(value)) {
84
+ container[l] = seen.get(value)
85
+ return
86
+ }
87
+ container[l] = {
88
+ s: current
89
+ }
90
+ container = container[l]
91
+ let attributes = JSONTag.getAttributes(value)
92
+ if (attributes) {
93
+ container.a = {
94
+ s: current,
95
+ c: {}
96
+ }
97
+ Object.entries(attributes).forEach(([k,v]) => {
98
+ let l = getKey(k)
99
+ let t = getType('string')
100
+ container.a.c[k] = {
101
+ s: current,
102
+ t
103
+ }
104
+ v = JSON.stringify(v)
105
+ let {r,w} = encoder.encodeInto(v, dv, current)
106
+ current += w
107
+ container.a.c[k].e = current
108
+ })
109
+ }
110
+ let type = JSONTag.getType(value)
111
+ let t = getType(type)
112
+ container.t = t
113
+ let v = JSON.stringify(value)
114
+ let uint8buffer = new Uint8Array(buffer)
115
+ let {r,w} = encoder.encodeInto(v, uint8buffer, current)
116
+ current+= w
117
+ container.e = current
118
+ }
119
+
120
+
121
+ function walk(node, parentKey, parent, save)
122
+ {
123
+ if (seen.has(node)) {
124
+ Object.assign(save, seen.get(node))
125
+ return
126
+ }
127
+ let type = JSONTag.getType(node)
128
+ let t = getType(type)
129
+ switch (type) {
130
+ case 'array':
131
+ seen.set(node,save)
132
+ save.s = current
133
+ save.t = t
134
+ save.c = []
135
+ node.forEach((v,k) => walk(v, k, node, save.c))
136
+ save.e = current
137
+ break
138
+ case 'object':
139
+ seen.set(node, save)
140
+ save.s = current
141
+ save.c = []
142
+ save.t = t
143
+ Object.entries(node).forEach(([k,v]) => walk(v, k, node, save.c))
144
+ save.e = current
145
+ break
146
+ default:
147
+ store(save, parentKey, node)
148
+ break
149
+ }
150
+ }
151
+
152
+ walk(data, null, null, root)
153
+ console.log(root)
154
+ return {
155
+ root: getJsontagProxy(root, index, buffer),
156
+ index,
157
+ buffer
158
+ }
159
+ }
package/src/util.mjs ADDED
@@ -0,0 +1,46 @@
1
+ import JSONTag from '@muze-nl/jsontag'
2
+ import fs from 'node:fs/promises'
3
+
4
+ export function deepFreeze(obj) {
5
+ Object.freeze(obj)
6
+ Object.keys(obj).forEach(prop => {
7
+ if (typeof obj[prop] === 'object' && !Object.isFrozen(obj[prop])) {
8
+ deepFreeze(obj[prop])
9
+ }
10
+ })
11
+ return obj
12
+ }
13
+
14
+ export function isString(s)
15
+ {
16
+ return typeof s === 'string' || s instanceof String
17
+ }
18
+
19
+ export function joinArgs(args) {
20
+ return args = args.map(arg => {
21
+ if (isString(arg)) {
22
+ return arg
23
+ } else {
24
+ return JSONTag.stringify(arg)
25
+ }
26
+ }).join(' ')
27
+ }
28
+
29
+ /**
30
+ * atomic append to a log file, adds newline after each write
31
+ * @param {string} filename The filename to append to
32
+ * @param {string} data The line to write
33
+ * @return {void}
34
+ */
35
+ export async function appendFile(filename, data) {
36
+ console.log('appending command')
37
+ let handle;
38
+ try {
39
+ handle = await fs.open(filename, 'a')
40
+ await handle.appendFile(data+"\n")
41
+ await handle.datasync()
42
+ return true
43
+ } finally {
44
+ await handle.close()
45
+ }
46
+ }
@@ -0,0 +1,49 @@
1
+ import {expose} from 'threads/worker'
2
+ import JSONTag from '@muze-nl/jsontag'
3
+ import commands from './commands.mjs'
4
+
5
+ /**
6
+ * Command Worker for threads.js library
7
+ * returns JSONTag strings, since otherwise JSON.stringify is used
8
+ * and type+attribute data gets lost
9
+ */
10
+
11
+ let dataspace
12
+
13
+ /**
14
+ * @TODO: check for valid command id
15
+ * @TODO: write valid commands to command log, emit 'ok', check in server.mjs for that
16
+ * @TODO: setTimeout or do above checks in server.mjs before queueing
17
+ */
18
+ const command = {
19
+ initialize(jsontag) {
20
+ if (!jsontag) { throw new Error('missing jsontag parameter')}
21
+ dataspace = JSONTag.parse(jsontag)
22
+ console.log('initialized command worker thread')
23
+ return true
24
+ },
25
+ runCommand(request, commandStr) {
26
+ if (!commandStr) { throw new Error('missing command parameter')}
27
+ if (!request) { throw new Error('missing request parameter')}
28
+ let response = {
29
+ jsontag: true
30
+ }
31
+ let command = JSONTag.parse(commandStr) // raw body through express.raw()
32
+ if (command && command.name && commands[command.name]) {
33
+ try {
34
+ commands[command.name](dataspace, command, request)
35
+ response.body = JSONTag.stringify(dataspace) //@TODO: this is inefficient, patch would be better
36
+ } catch(err) {
37
+ console.log(err)
38
+ response.code = 422;
39
+ response.body = '<object class="Error">{"message":'+JSON.stringify(''+err)+',"code":422}'
40
+ }
41
+ } else {
42
+ response.code = 404
43
+ response.body = '<object class="Error">{"message":"Command '+command.name+' not found","code":404}'
44
+ }
45
+ return response
46
+ }
47
+ }
48
+
49
+ expose(command)