@muze-nl/simplystore 0.3.2 → 0.3.5

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,86 +1,46 @@
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 commands from './commands.mjs'
7
+ import {appendFile} from './util.mjs'
8
+ import {Piscina} from 'piscina'
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
+ const queryWorker = options.queryWorker || __dirname+'/src/worker-query-init.mjs'
23
+ const commandWorker = options.commandWorker || __dirname+'/src/worker-command-init.mjs'
63
24
 
64
- const originalJSON = JSON
65
- JSON = JSONTag // monkeypatching
25
+ let jsontag = fs.readFileSync(datafile, 'utf-8')
66
26
 
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()
27
+ function initWorkerPool(workerName, size=null) {
28
+ let options = {
29
+ filename: workerName,
30
+ workerData: jsontag
31
+ }
32
+ if (size) {
33
+ options.maxThreads = size
75
34
  }
35
+ return new Piscina(options)
76
36
  }
77
- deepFreeze(dataspace)
78
37
 
79
- let used = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
80
- console.log(`data loaded (${used} MB)`);
38
+ let queryWorkerpool = initWorkerPool(queryWorker)
39
+
40
+ // const commandWorkerpool = initWorkerPool(commandWorker,1) // only one update worker so no changes can get lost
81
41
 
82
42
  server.get('/', (req,res) => {
83
- res.send('<h1>SimplyStore</h1>') //TODO: implement something nice
43
+ res.send('<h1>SimplyStore</h1>') //@TODO: implement something nice
84
44
  })
85
45
 
86
46
  server.use(express.static(wwwroot))
@@ -103,35 +63,34 @@ async function main(options) {
103
63
  return true
104
64
  }
105
65
 
106
- function getDataSpace(req, res, dataspace) {
107
- let path = req.path.substr(6); // cut '/query'
108
- if (!path) {
109
- path = '';
66
+ function sendResponse(response, res) {
67
+ if (response.code) {
68
+ res.status(response.code)
110
69
  }
111
- if (path.substring(path.length-1)==='/') {
112
- //jsonpointer doesn't allow a trailing '/'
113
- path = path.substring(0, path.length-1)
70
+ if (response.jsontag) {
71
+ res.setHeader('content-type','application/jsontag')
72
+ } else {
73
+ res.setHeader('content-type','application/json')
114
74
  }
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
- }
75
+ res.send(response.body)+"\n"
76
+ }
77
+
78
+ function sendCommandResponse(result, req, res) {
79
+ if (result.code) {
80
+ res.status(result.code)
81
+ }
82
+ if (req.accepts('application/jsontag')) {
83
+ res.setHeader('content-type','application/jsontag')
84
+ res.send(JSONTag.stringify(result, null, 4)+"\n")
127
85
  } else {
128
- result = dataspace
86
+ res.setHeader('content-type','application/json')
87
+ res.send(JSON.stringify(result, null, 4)+"\n")
129
88
  }
130
- return [result,path]
131
89
  }
132
-
90
+
133
91
  server.get('/query/*', (req, res, next) =>
134
92
  {
93
+ console.log('express query')
135
94
  let start = Date.now()
136
95
 
137
96
  if ( !accept(req,res,
@@ -151,21 +110,19 @@ async function main(options) {
151
110
  // done
152
111
  return
153
112
  }
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")
113
+ let path = req.path.substr(6); // cut '/query'
114
+ let request = {
115
+ method: req.method,
116
+ url: req.originalUrl,
117
+ query: req.query,
118
+ jsontag: req.accepts('application/jsontag')
166
119
  }
167
- let end = Date.now()
168
- console.log(path, (end-start), process.memoryUsage())
120
+ queryWorkerpool.run({pointer:path, request})
121
+ .then(response => {
122
+ sendResponse(response, res)
123
+ let end = Date.now()
124
+ console.log(path, (end-start), process.memoryUsage())
125
+ })
169
126
  })
170
127
 
171
128
  /**
@@ -178,62 +135,111 @@ async function main(options) {
178
135
  ) {
179
136
  return
180
137
  }
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
- }
138
+ let query = req.body.toString() // raw body through express.raw()
139
+ let path = req.path.substr(6); // cut '/query'
140
+ let request = {
141
+ method: req.method,
142
+ url: req.originalUrl,
143
+ query: req.query,
144
+ jsontag: req.accepts('application/jsontag')
219
145
  }
146
+ queryWorkerpool.run({pointer:path, request, query})
147
+ .then(response => {
148
+ sendResponse(response, res)
149
+ let end = Date.now()
150
+ console.log(path, (end-start), process.memoryUsage())
151
+ })
152
+ })
220
153
 
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")
154
+ let status = new Map()
155
+
156
+ server.get('/command/:id', (req, res) => {
157
+ //@TODO: find the status of command with :id
158
+ //return that
159
+ if (status.has(req.params.id)) {
160
+ let result = status.get(req.params.id)
161
+ sendResponse({
162
+ jsontag: false,
163
+ body: JSON.stringify(result)
164
+ },res)
228
165
  } else {
229
- res.setHeader('content-type','application/json')
230
- res.send(originalJSON.stringify(result, null, 4)+"\n")
166
+ sendResponse({
167
+ code: 404,
168
+ jsontag: false,
169
+ body: JSON.stringify({"code":404,"message":"Command not found"})
170
+ }, res)
231
171
  }
232
- let end = Date.now()
233
- console.log(path, (end-start), process.memoryUsage())
234
172
  })
235
173
 
174
+ server.post('/command', async (req, res) => {
175
+ let start = Date.now()
176
+ if ( !accept(req,res,
177
+ ['application/jsontag','application/json'])
178
+ ) {
179
+ return
180
+ }
181
+ let error, result
182
+
183
+ let commandStr = req.body.toString() // raw body through express.raw()
184
+ let command = JSONTag.parse(commandStr)
185
+ if (!command.id) {
186
+ error = {
187
+ code: 422,
188
+ message: "Command has no id"
189
+ }
190
+ sendCommandResponse(error, req, res)
191
+ return
192
+ } else if (status.has(command.id)) {
193
+ result = "OK"
194
+ sendCommandResponse(result, req, res)
195
+ return
196
+ } else if (!command.name || !commands[command.name]) {
197
+ error = {
198
+ code: 422,
199
+ message: "Command has no name or is unknown"
200
+ }
201
+ sendCommandResponse(error, req, res)
202
+ return
203
+ }
204
+ await appendFile(commandLog, JSONTag.stringify(command))
205
+
206
+ status.set(command.id, 'queued')
207
+ console.log('command',command)
208
+
209
+ result = "OK"
210
+ sendCommandResponse(result, req, res)
211
+ let request = {
212
+ method: req.method,
213
+ url: req.originalUrl,
214
+ query: req.query,
215
+ jsontag: req.accepts('application/jsontag')
216
+ }
217
+
218
+ commandWorkerpool
219
+ .run({request, commandStr})
220
+ .then(response => {
221
+ //@TODO store response status, if response.code => error
222
+ if (!response.code) {
223
+ jsontag = response.body // global jsontag
224
+ let dataspace = JSONTag.parse(jsontag)
225
+ //@TODO: make sure queryWorkerpool is only replaced after
226
+ //workers are initialized, to prevent hickups if initialization takes a long time
227
+ let newQueryWorkerpool = initWorkerPool('./worker-query')
228
+ queryWorkerpool.terminate() // gracefully
229
+ queryWorkerpool = newQueryWorkerpool
230
+ //@TODO: write dataspace to disk
231
+ status.set(command.id, 'done')
232
+ let end = Date.now()
233
+ console.log(command.name, (end-start), process.memoryUsage())
234
+ }
235
+ })
236
+ .catch(err => {
237
+ console.error(err)
238
+ //@TODO: set status for this command to error with this err
239
+ status.set(command.id, err)
240
+ })
236
241
 
242
+ })
237
243
 
238
244
  function handleWebRequest(req,res,options)
239
245
  {
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,11 @@
1
+ import * as commandWorker from './worker-command.mjs'
2
+ import worker_threads from 'node:worker_threads'
3
+
4
+ async function initialize() {
5
+ let meta = {}
6
+ let dataspace = JSONTag.parse(worker_threads.workerData, null, meta)
7
+ await commandWorker.initialize(worker_threads.workerData)
8
+ return commandWorker.runCommand
9
+ }
10
+
11
+ export default initialize()
@@ -0,0 +1,51 @@
1
+ import JSONTag from '@muze-nl/jsontag'
2
+ import commands from './commands.mjs'
3
+
4
+ /**
5
+ * Command Worker for threads.js library
6
+ * returns JSONTag strings, since otherwise JSON.stringify is used
7
+ * and type+attribute data gets lost
8
+ */
9
+
10
+ let dataspace
11
+
12
+ export function setDataspace(d) {
13
+ dataspace = d
14
+ }
15
+
16
+ /**
17
+ * @TODO: check for valid command id
18
+ * @TODO: write valid commands to command log, emit 'ok', check in server.mjs for that
19
+ * @TODO: setTimeout or do above checks in server.mjs before queueing
20
+ */
21
+
22
+ export function async initialize(jsontag) {
23
+ if (!jsontag) { throw new Error('missing jsontag parameter')}
24
+ dataspace = jsontag
25
+ console.log('initialized command worker thread')
26
+ return true
27
+ }
28
+
29
+ export function runCommand({request, commandStr}) {
30
+ if (!commandStr) { throw new Error('missing command parameter')}
31
+ if (!request) { throw new Error('missing request parameter')}
32
+ let response = {
33
+ jsontag: true
34
+ }
35
+ let command = JSONTag.parse(commandStr) // raw body through express.raw()
36
+ if (command && command.name && commands[command.name]) {
37
+ try {
38
+ commands[command.name](dataspace, command, request)
39
+ response.body = JSONTag.stringify(dataspace) //@TODO: this is inefficient, patch would be better
40
+ } catch(err) {
41
+ console.log(err)
42
+ response.code = 422;
43
+ response.body = '<object class="Error">{"message":'+JSON.stringify(''+err)+',"code":422}'
44
+ }
45
+ } else {
46
+ response.code = 404
47
+ response.body = '<object class="Error">{"message":"Command '+command.name+' not found","code":404}'
48
+ }
49
+ return response
50
+ }
51
+
@@ -0,0 +1,14 @@
1
+ import JSONTag from '@muze-nl/jsontag';
2
+ import worker_threads from 'node:worker_threads';
3
+ import * as queryWorker from './worker-query.mjs';
4
+
5
+ async function initialize() {
6
+ let meta = {}
7
+ let dataspace = JSONTag.parse(worker_threads.workerData, null, meta)
8
+ //console.log('starting')
9
+ await queryWorker.initialize(dataspace,meta)
10
+ //console.log('initialized')
11
+ return queryWorker.runQuery
12
+ }
13
+
14
+ export default initialize()