@muze-nl/simplystore 0.4.7 → 0.5.1

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.
@@ -0,0 +1,200 @@
1
+ import JSONTag from '@muze-nl/jsontag';
2
+ import {source,isProxy,getIndex, getBuffer} from './symbols.mjs'
3
+
4
+ // faststringify function for a fast parseable arraybuffer output
5
+ //
6
+ const encoder = new TextEncoder()
7
+ const decoder = new TextDecoder()
8
+
9
+ export default function stringify(value, meta, skipLength=false, index) {
10
+ let resultArray = []
11
+ if (!meta) {
12
+ meta = {}
13
+ }
14
+ if (!meta.index) {
15
+ meta.index = {}
16
+ }
17
+ if (!meta.index.id) {
18
+ meta.index.id = new Map()
19
+ }
20
+ let references = new WeakMap()
21
+
22
+ const innerStringify = (value) => {
23
+ let indent = ""
24
+ let gap = ""
25
+
26
+ if (typeof space === "number") {
27
+ indent += " ".repeat(space)
28
+ } else if (typeof space === "string") {
29
+ indent = space
30
+ }
31
+
32
+ const encodeProperties = (obj) => {
33
+ return Object.getOwnPropertyNames(obj).map(prop => {
34
+ let enumerable = obj.propertyIsEnumerable(prop) ? '' : '#'
35
+ return enumerable+'"'+prop+'":'+str(prop, obj)
36
+ }).join(',')
37
+ }
38
+
39
+ const encodeEntries = (arr) => {
40
+ let result = arr.map((value,index) => {
41
+ return str(index, arr)
42
+ }).join(",")
43
+ return result
44
+ }
45
+
46
+ const createId = (value) => {
47
+ if (typeof crypto === 'undefined') {
48
+ console.error('JSONTag: cannot generate uuid, crypto support is disabled.')
49
+ throw new Error('Cannot create links to resolve references, crypto support is disabled')
50
+ }
51
+ if (typeof crypto.randomUUID === 'function') {
52
+ var id = crypto.randomUUID()
53
+ } else {
54
+ var id = ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
55
+ (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
56
+ );
57
+ }
58
+ JSONTag.setAttribute(value, 'id', id)
59
+ return id
60
+ }
61
+
62
+ const encoder = new TextEncoder()
63
+ const decoder = new TextDecoder()
64
+
65
+ const str = (key, holder) => {
66
+ let value = holder[key]
67
+ let result, updateReference
68
+ // if value is a valueProxy, just copy the input slice
69
+ if (value && !JSONTag.isNull(value) && value[isProxy]) {
70
+ if (index===0) {
71
+ resultArray.push(decoder.decode(value[getBuffer](index)))
72
+ }
73
+ return decoder.decode(value[getBuffer](index))
74
+ }
75
+ if (typeof value === 'undefined' || value === null) {
76
+ return 'null'
77
+ }
78
+ if (JSONTag.getType(value) === 'object' && !Array.isArray(value)) {
79
+ let id = JSONTag.getAttribute(value, 'id')
80
+ if (!references.has(value)) {
81
+ let reference = resultArray.length
82
+ updateReference = reference
83
+ references.set(value, updateReference)
84
+ resultArray.push('')
85
+ if (id && !meta.index.id.has(id)) {
86
+ meta.index.id.set(id, updateReference)
87
+ }
88
+ } else {
89
+ return '~'+references.get(value)
90
+ }
91
+ }
92
+ if (Array.isArray(value)) {
93
+ result = JSONTag.getTypeString(value) + "["+encodeEntries(value)+"]"
94
+ } else if (value instanceof Object) {
95
+ let typeString = JSONTag.getTypeString(value)
96
+ let type = JSONTag.getType(value)
97
+ switch (type) {
98
+ case 'string':
99
+ case 'decimal':
100
+ case 'money':
101
+ case 'link':
102
+ case 'text':
103
+ case 'blob':
104
+ case 'color':
105
+ case 'email':
106
+ case 'hash':
107
+ case 'duration':
108
+ case 'phone':
109
+ case 'url':
110
+ case 'uuid':
111
+ case 'date':
112
+ case 'time':
113
+ case 'datetime':
114
+ if (JSONTag.isNull(value)) {
115
+ value = 'null'
116
+ } else {
117
+ value = JSON.stringify(''+value)
118
+ }
119
+ result = typeString + value
120
+ break
121
+ case 'int':
122
+ case 'uint':
123
+ case 'int8':
124
+ case 'uint8':
125
+ case 'int16':
126
+ case 'uint16':
127
+ case 'int32':
128
+ case 'uint32':
129
+ case 'int64':
130
+ case 'uint64':
131
+ case 'float':
132
+ case 'float32':
133
+ case 'float64':
134
+ case 'timestamp':
135
+ case 'number':
136
+ case 'boolean':
137
+ if (JSONTag.isNull(value)) {
138
+ value = 'null'
139
+ } else {
140
+ value = JSON.stringify(value)
141
+ }
142
+ result = typeString + value
143
+ break
144
+ case 'array':
145
+ let entries = encodeEntries(value) // calculate children first so parent references can add id attribute
146
+ result = typeString + '[' + entries + '}'
147
+ break
148
+ case 'object':
149
+ if (JSONTag.isNull(value)) {
150
+ result = typeString + "null"
151
+ } else {
152
+ let props = encodeProperties(value); // calculate children first so parent references can add id attribute
153
+ result = typeString + '{' + props + '}'
154
+ }
155
+ break
156
+ default:
157
+ throw new Error(JSONTag.getType(value)+' type not yet implemented')
158
+ break
159
+ }
160
+ } else {
161
+ result = JSON.stringify(value)
162
+ }
163
+ if (typeof updateReference != 'undefined') {
164
+ resultArray[updateReference] = result
165
+ if (index!==updateReference) {
166
+ result = '~'+updateReference
167
+ }
168
+ }
169
+ return result
170
+ }
171
+
172
+ return str("", {"": value})
173
+ }
174
+
175
+ const encode = (s) => {
176
+ if (skipLength) {
177
+ return s
178
+ }
179
+ let length = new Blob([s]).size
180
+ return '('+length+')'+s
181
+ }
182
+
183
+ innerStringify(value)
184
+ return resultArray.map(encode).join("\n")
185
+ }
186
+
187
+ export function stringToSAB(strData) {
188
+ const buffer = encoder.encode(strData)
189
+ const sab = new SharedArrayBuffer(buffer.length)
190
+ let uint8sab = new Uint8Array(sab)
191
+ uint8sab.set(buffer,0)
192
+ return uint8sab
193
+ }
194
+
195
+ export function resultSetStringify(resultSet) {
196
+ return resultSet.map((e,i) => {
197
+ let buffer = e[getBuffer](i)
198
+ return '('+buffer.length+')'+decoder.decode(buffer)
199
+ }).join("\n")
200
+ }
@@ -0,0 +1,33 @@
1
+ import { parentPort } from 'node:worker_threads'
2
+ import JSONTag from '@muze-nl/jsontag'
3
+ import fastParse from './fastParse.mjs'
4
+ import fs from 'fs'
5
+ import {resultSetStringify,stringToSAB} from './fastStringify.mjs'
6
+ import {source} from '../src/symbols.mjs'
7
+
8
+ parentPort.on('message', datafile => {
9
+ const jsontag = fs.readFileSync(datafile)
10
+ let meta = {
11
+ index: {
12
+ id: new Map()
13
+ }
14
+ }
15
+ const data = fastParse(jsontag)
16
+
17
+ // fastParse doesn't create meta.index.id, so do that here
18
+ let length = data.length
19
+ for (let i=0; i<length; i++) {
20
+ let id=JSONTag.getAttribute(data[i][source],'id')
21
+ if (id) {
22
+ meta.index.id.set(id,i)
23
+ }
24
+ }
25
+
26
+ const strData = resultSetStringify(data)
27
+ const sab = stringToSAB(strData)
28
+
29
+ parentPort.postMessage({
30
+ data: sab,
31
+ meta
32
+ })
33
+ })
@@ -1,96 +1,63 @@
1
- import JSONTag from "@muze-nl/jsontag"
2
- import pointer from 'json-pointer'
1
+ import JSONTag from '@muze-nl/jsontag'
2
+ import fastParse from './fastParse.mjs'
3
+ import {source, isProxy} from './symbols.mjs'
3
4
  import {_,from,not,anyOf,allOf,asc,desc,sum,count,avg,max,min} from 'array-where-select'
4
- import {deepFreeze} from './util.mjs'
5
+ import pointer from 'json-pointer'
5
6
  import {VM} from 'vm2'
7
+ import { memoryUsage } from 'node:process'
6
8
 
7
- let dataspace, meta = {};
8
-
9
- export function setDataspace(d, m) {
10
- dataspace = d
11
- if(m) {
12
- meta = m
9
+ let resultSet = []
10
+ let dataspace
11
+ let meta = {}
12
+ let metaProxy = {
13
+ index: {
13
14
  }
14
15
  }
15
16
 
16
- export function getDataSpace(path, dataspace) {
17
- if (path.substring(path.length-1)==='/') {
18
- //jsonpointer doesn't allow a trailing '/'
19
- path = path.substring(0, path.length-1)
20
- }
21
- let result
22
- if (path) {
23
- //jsonpointer doesn't allow an empty pointer
24
- try {
25
- if (pointer.has(dataspace, path)) {
26
- result = pointer.get(dataspace, path)
27
- } else {
28
- result = JSONTag.parse('<object class="Error">{"message":"Not found", "code":404}')
29
- }
30
- } catch(err) {
31
- result = JSONTag.parse('<object class="Error">{"message":'+JSON.stringify(err.message)+', "code":500}')
17
+ const metaIdProxy = {
18
+ get: (id) => {
19
+ let index = meta.index.id.get(id)
20
+ if (index || index===0) {
21
+ return resultSet[index]
32
22
  }
33
- } else {
34
- result = dataspace
23
+ },
24
+ has: (id) => {
25
+ return meta.index.id.has(id)
35
26
  }
36
- return [result,path]
37
27
  }
38
28
 
39
- export function linkReplacer(data, baseURL) {
40
- let type = JSONTag.getType(data)
41
- let attributes = JSONTag.getAttributes(data)
42
- if (Array.isArray(data)) {
43
- data = data.map((entry,index) => {
44
- return linkReplacer(data[index], baseURL+index+'/')
45
- })
46
- } else if (type === 'link') {
47
- // do nothing
48
- } else if (data && typeof data === 'object') {
49
- data = JSONTag.clone(data)
50
- Object.keys(data).forEach(key => {
51
- if (Array.isArray(data[key])) {
52
- data[key] = new JSONTag.Link(baseURL+key+'/')
53
- } else if (data[key] && typeof data[key] === 'object') {
54
- if (JSONTag.getType(data[key])!=='link') {
55
- let id=JSONTag.getAttribute(data[key], 'id')
56
- if (!id) {
57
- id = baseURL+key+'/'
58
- }
59
- data[key] = new JSONTag.Link(id)
60
- }
61
- }
62
- })
63
- }
64
- return data
29
+ const FastJSONTag = {
30
+ getType: (obj) => JSONTag.getType(obj[source]),
31
+ getAttribute: (obj, attr) => JSONTag.getAttribute(obj[source],attr),
32
+ getAttributes: (obj) => JSONTag.getAttributes(obj[source]),
33
+ getAttributeString: (obj) => JSONTag.getAttributesString(obj[source]),
34
+ getTypeString: (obj) => JSONTag.getTypeString(obj[source])
65
35
  }
66
36
 
67
- //@TODO: emit console events that server.mjs picks up
68
- function connectConsole(res) {
69
- return {
70
- log: function(...args) {
71
- // res.append('X-Console-Log', joinArgs(args))
72
- },
73
- warning: function(...args) {
74
- // res.append('X-Console-Warning', joinArgs(args))
75
- },
76
- error: function(...args) {
77
- // res.append('X-Console-Error', joinArgs(args))
78
- }
37
+ const tasks = {
38
+ init: async (task) => {
39
+ resultSet = fastParse(task.req.body)
40
+ dataspace = resultSet[0]
41
+ meta = task.req.meta
42
+ metaProxy.index.id = metaIdProxy
43
+ //@TODO: add references and baseURL
44
+ return true
45
+ },
46
+ query: async (task) => {
47
+ return runQuery(task.req.path, task.req, task.req.body)
48
+ },
49
+ memoryUsage: async () => {
50
+ let result = memoryUsage()
51
+ console.log('memory',result)
52
+ return result
79
53
  }
80
54
  }
81
55
 
82
- export async function initialize(jsontag, m) {
83
- if (!jsontag) { throw new Error('missing jsontag parameter')}
84
- dataspace = jsontag
85
- meta = m
86
- deepFreeze(dataspace)
87
- return true
88
- }
56
+ export default tasks
89
57
 
90
- export function runQuery({pointer, request, query}) {
58
+ export function runQuery(pointer, request, query) {
91
59
  if (!pointer) { throw new Error('missing pointer parameter')}
92
60
  if (!request) { throw new Error('missing request parameter')}
93
- console.log('query',pointer)
94
61
  let response = {
95
62
  jsontag: request.jsontag
96
63
  }
@@ -98,8 +65,6 @@ export function runQuery({pointer, request, query}) {
98
65
 
99
66
  if (query) {
100
67
  // @todo add text search: https://github.com/nextapps-de/flexsearch
101
- // @todo add tree walk map/reduce/find/filter style functions
102
- // @todo add arc tree dive function?
103
68
  // @todo replace VM with V8 isolate
104
69
  const vm = new VM({
105
70
  timeout: 1000,
@@ -107,7 +72,7 @@ export function runQuery({pointer, request, query}) {
107
72
  sandbox: {
108
73
  root: dataspace, //@TODO: if we don't pass the root, we can later shard
109
74
  data: result,
110
- meta,
75
+ meta: metaProxy,
111
76
  _,
112
77
  from,
113
78
  not,
@@ -119,15 +84,15 @@ export function runQuery({pointer, request, query}) {
119
84
  count,
120
85
  avg,
121
86
  max,
122
- min,
87
+ min,
123
88
  // console: connectConsole(res),
124
- JSONTag,
89
+ JSONTag: FastJSONTag,
125
90
  request
126
91
  },
127
92
  wasm: false
128
93
  })
129
94
  try {
130
- result = vm.run(query)
95
+ result = deProxy(vm.run(query))
131
96
  let used = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
132
97
  console.log(`(${used} MB)`);
133
98
  } catch(err) {
@@ -144,10 +109,100 @@ export function runQuery({pointer, request, query}) {
144
109
  }
145
110
  if (!response.code) {
146
111
  if (response.jsontag) {
147
- response.body = JSONTag.stringify(result)
112
+ try {
113
+ response.body = JSONTag.stringify(result)
114
+ } catch(err) {
115
+ console.log(err)
116
+ response.code = 500
117
+ response.body = '<object class="Error">{"message":'+JSON.stringify(''+err)+',"code":500}'
118
+ }
148
119
  } else {
120
+ //@FIXME: replace recursive links
149
121
  response.body = JSON.stringify(result)
150
122
  }
151
123
  }
152
124
  return response
153
- }
125
+ }
126
+
127
+ export function getDataSpace(path, dataspace) {
128
+ if (path.substring(path.length-1)==='/') {
129
+ //jsonpointer doesn't allow a trailing '/'
130
+ path = path.substring(0, path.length-1)
131
+ }
132
+ let result
133
+ if (path) {
134
+ //jsonpointer doesn't allow an empty pointer
135
+ try {
136
+ if (pointer.has(dataspace, path)) {
137
+ result = pointer.get(dataspace, path)
138
+ } else {
139
+ result = JSONTag.parse('<object class="Error">{"message":"Not found", "code":404}')
140
+ }
141
+ } catch(err) {
142
+ result = JSONTag.parse('<object class="Error">{"message":'+JSON.stringify(err.message)+', "code":500}')
143
+ }
144
+ } else {
145
+ result = dataspace
146
+ }
147
+ return [result,path]
148
+ }
149
+
150
+ export function linkReplacer(data, baseURL) {
151
+ let type = JSONTag.getType(data)
152
+ let attributes = JSONTag.getAttributes(data)
153
+ if (Array.isArray(data)) {
154
+ data = data.map((entry,index) => {
155
+ return linkReplacer(data[index], baseURL+index+'/')
156
+ })
157
+ } else if (type === 'link') {
158
+ // do nothing
159
+ } else if (data && typeof data === 'object') {
160
+ if (data[source]) {
161
+ data = data[source]
162
+ }
163
+ data = JSONTag.clone(data)
164
+ Object.keys(data).forEach(key => {
165
+ if (Array.isArray(data[key])) {
166
+ data[key] = new JSONTag.Link(baseURL+key+'/')
167
+ } else if (data[key] && typeof data[key] === 'object') {
168
+ if (JSONTag.getType(data[key])!=='link') {
169
+ let id=JSONTag.getAttribute(data[key], 'id')
170
+ if (!id) {
171
+ id = baseURL+key+'/'
172
+ }
173
+ data[key] = new JSONTag.Link(id)
174
+ }
175
+ }
176
+ })
177
+ }
178
+ return data
179
+ }
180
+
181
+ let seen = new WeakMap()
182
+ function deProxy(o) {
183
+ if (!o) {
184
+ return o
185
+ }
186
+ if (typeof o !== 'object') {
187
+ return o
188
+ }
189
+ if (seen.has(o)) {
190
+ return seen.get(o)
191
+ }
192
+ let result
193
+ if (Array.isArray(o)) {
194
+ result = o.map(deProxy)
195
+ } else if (JSONTag.isNull(o)) {
196
+ return o
197
+ } else if (JSONTag.getType(o)==='object' && o[source]) {
198
+ result = JSONTag.clone(o[source])
199
+ seen.set(o, result)
200
+ Object.entries(o[source]).forEach(([i,v]) => {
201
+ result[i] = deProxy(v)
202
+ })
203
+ } else {
204
+ seen.set(o, o)
205
+ result = o
206
+ }
207
+ return result
208
+ }
@@ -0,0 +1,12 @@
1
+ import { parentPort } from 'node:worker_threads'
2
+ import tasks from './query-worker-module.mjs'
3
+
4
+ parentPort.on('message', async task => {
5
+ let result
6
+ if (tasks[task.name]) {
7
+ result = await tasks[task.name].call(tasks, task)
8
+ } else {
9
+ result = new Error('Unknown task '+task.name)
10
+ }
11
+ parentPort.postMessage(result)
12
+ })