@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/.npmignore~ ADDED
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ .git
3
+ .gitmodules
4
+ tests
5
+ example
6
+ design
7
+ docs
8
+ test-command.jsontag
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@muze-nl/simplystore",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "main": "src/server.mjs",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "start": "node src/run.mjs"
7
+ "start": "node src/run.mjs",
8
+ "test": "tap test/*.mjs"
8
9
  },
9
10
  "author": "auke@muze.nl",
10
11
  "license": "MIT",
@@ -15,15 +16,17 @@
15
16
  "bugs": "https://github.com/simplyedit/simplystore/issues",
16
17
  "homepage": "https://github.com/simplyedit/simplystore#readme",
17
18
  "dependencies": {
18
- "@muze-nl/jsontag": "^0.8.4",
19
+ "@muze-nl/jsontag": "^0.8.5",
19
20
  "array-where-select": "^0.3.1",
20
21
  "codemirror": "^6.0.1",
21
22
  "express": "^4.18.1",
22
23
  "json-pointer": "^0.6.2",
23
24
  "jsonpath-plus": "^7.2.0",
25
+ "threads": "^1.7.0",
24
26
  "vm2": "^3.9.13"
25
27
  },
26
28
  "devDependencies": {
27
- "process": "^0.11.10"
29
+ "process": "^0.11.10",
30
+ "tap": "^16.3.8"
28
31
  }
29
32
  }
package/package.json~ ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@muze-nl/simplystore",
3
+ "version": "0.3.2",
4
+ "main": "src/server.mjs",
5
+ "type": "module",
6
+ "scripts": {
7
+ "start": "node src/run.mjs",
8
+ "test": "tap test/*.mjs"
9
+ },
10
+ "author": "auke@muze.nl",
11
+ "license": "MIT",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/simplyedit/simplystore.git"
15
+ },
16
+ "bugs": "https://github.com/simplyedit/simplystore/issues",
17
+ "homepage": "https://github.com/simplyedit/simplystore#readme",
18
+ "dependencies": {
19
+ "@muze-nl/jsontag": "^0.8.5",
20
+ "array-where-select": "^0.3.1",
21
+ "codemirror": "^6.0.1",
22
+ "express": "^4.18.1",
23
+ "json-pointer": "^0.6.2",
24
+ "jsonpath-plus": "^7.2.0",
25
+ "threads": "^1.7.0",
26
+ "vm2": "^3.9.13"
27
+ },
28
+ "devDependencies": {
29
+ "process": "^0.11.10",
30
+ "tap": "^16.3.8"
31
+ }
32
+ }
@@ -0,0 +1,7 @@
1
+ export default {
2
+ addPerson: (dataspace, command) => {
3
+ dataspace.persons.push(command.value)
4
+ // dataspace.persons[0].name = 'Jan';
5
+ return 'foo'
6
+ }
7
+ }
@@ -0,0 +1,354 @@
1
+ import {types} from 'node:util'
2
+ import {clone} from '@muze-nl/jsontag/src/lib/functions.mjs'
3
+
4
+ /**
5
+ * This library implements a version of [immer](https://immerjs.github.io/immer/)
6
+ * with one important difference: it works on graphs instead of just trees.
7
+ * This does mean we take a performance hit, the first time produce is called it creates
8
+ * a complete index of which object references which other object (and in which property)
9
+ * Additionally it uses the JSONTag clone method to also copy type/attributes from JSONTag
10
+ *
11
+ * --------------------------------------------------------
12
+ *
13
+ * Implementation details:
14
+ *
15
+ * produce starts an update function, where the baseState is replaced with a proxy. This proxy
16
+ * automatically creates mutable clones whenever you set/delete or otherwise update something
17
+ * on the proxy. Each change creates a clone, and each reference to the original base object is
18
+ * replaced with the clone, which triggers creating clones of the objects containing that
19
+ * reference. This means that any change will always create a new clone of the root baseState
20
+ * object. This object represents the changed nextState, where baseState is not changed as it
21
+ * is immutable. Before finishing, all clones are made immutable again.
22
+ */
23
+
24
+ /**
25
+ * Contains a list of references for each child or value object
26
+ * Usage:
27
+ * const refs = references.get(valueObject)
28
+ * Returns an array of references in the form:
29
+ * [
30
+ * {
31
+ * source: WeakRef,
32
+ * prop: String|Number
33
+ * }
34
+ * ]
35
+ * You need to call source.deref() and then check that the result is not null
36
+ * @type {WeakMap}
37
+ */
38
+ let references = new WeakMap()
39
+
40
+ /**
41
+ * Keeps track of objects to make sure addReference doesn't go into an infinite loop
42
+ * @type {WeakMap}
43
+ */
44
+ let seen = new WeakMap()
45
+
46
+ /**
47
+ * Adds a reference to the references index, which source object and property
48
+ * refer to which value object
49
+ * @param {object} source The source or parent object
50
+ * @param {string|number} key The property that refers the child or value object
51
+ * @param {object} value The value or child object
52
+ * @return {void}
53
+ */
54
+ function addReference(source,key,value) {
55
+ if (value && typeof value === 'object' && Object.isFrozen(value)) {
56
+ if (!references.has(value)) {
57
+ references.set(value, [])
58
+ }
59
+ let list = references.get(value)
60
+ list.push({
61
+ source: new WeakRef(source),
62
+ prop: key
63
+ })
64
+ if (!seen.has(value)) {
65
+ seen.set(value,true)
66
+ index(value)
67
+ }
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Removes a reference from the references index, for the given value
73
+ * and source and key
74
+ * @param {object} source The source or parent object
75
+ * @param {string|number} key The property that refers the child or value object
76
+ * @param {object} value The value or child object
77
+ * @return {void}
78
+ */
79
+ function removeReference(source, key, value) {
80
+ if (value && typeof value === 'object' && Object.isFrozen(value) && references.has(value)) {
81
+ // only do the expensive work if needed, references only keeps track of frozen objects
82
+ let list = references.get(value)
83
+ list = list.filter(r => {
84
+ if (r.source.deref()===source && r.prop===key) {
85
+ return false
86
+ }
87
+ return true
88
+ })
89
+ references.set(value, list)
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Adds the given object and all its children to the references index
95
+ * The references index contains parent+property references that refer
96
+ * to each child object. References are added as WeakRef's, so need
97
+ * to be deref() to be used. Only objects are added to the reference,
98
+ * as literal values cannot be shared / referenced anyway.
99
+ * @param {object} root The object to index
100
+ * @return {void}
101
+ */
102
+ export function index(root) {
103
+ if (root && typeof root === 'object' && Object.isFrozen(root)) {
104
+ if (Array.isArray(root)) {
105
+ root.forEach((element, index) => {
106
+ addReference(root, index, element)
107
+ })
108
+ } else {
109
+ Object.entries(root).forEach(([prop, element]) => {
110
+ addReference(root, prop, element)
111
+ })
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Returns an array with references for the child or value object
118
+ * If no references exist, it will return an empty array
119
+ * @param {object} value The value or child object
120
+ * @return {array} The list of references or an empty array
121
+ */
122
+ export function findReferences(value) {
123
+ return references.get(value) || []
124
+ }
125
+
126
+
127
+ /**
128
+ * This function creates a new immutable datastructure from an existing one
129
+ * and an update function. The update function receives a single parameter,
130
+ * which is a draft for the immutable datastructure. Each change set in the draft
131
+ * will appear in the new state
132
+ * This function is built to be compatible with [immer](https://immerjs.github.io/immer/)
133
+ * Except it will work on graphs, not just on trees. Also it uses @muze-nl/jsontag's clone
134
+ * function, so attributes and types will survive in the resulting dataset.
135
+ *
136
+ * @param {object} baseState immutable datastructure to change
137
+ * @param {function} updateFn function that makes changes in the datastructure
138
+ * @return {object} new immutable datastructure which incorporates the changes
139
+ */
140
+ export function produce(baseState, updateFn) {
141
+ /**
142
+ * This contains a reference from a Proxy to the original object being proxied
143
+ * @type {Map}
144
+ */
145
+ let values = new Map()
146
+
147
+ /**
148
+ * This contains a reference from an original, immutable object, to its mutable clone
149
+ * @type {Map}
150
+ */
151
+ let clones = new Map()
152
+
153
+ function shallowClone(o) {
154
+ if (o instanceof Number) {
155
+ return new Number(o)
156
+ }
157
+ if (o instanceof Boolean) {
158
+ return new Boolean(o)
159
+ }
160
+ if (o instanceof String) {
161
+ return new String(o)
162
+ }
163
+ if (Array.isArray(o)) {
164
+ return [ ...o ]
165
+ }
166
+ return { ...o }
167
+ }
168
+
169
+ /**
170
+ * Returns the clone for this value object.
171
+ * There is only ever one clone for each immutable baseState object
172
+ * If the value object is immutable, it will create a clone or return the existing clone.
173
+ * @param {object} baseState The base value object
174
+ * @return {object} The clone for this value object
175
+ */
176
+ function getClone(baseState) {
177
+ if (clones.has(baseState)) {
178
+ return clones.get(baseState)
179
+ }
180
+ if (Object.isFrozen(baseState)) {
181
+ const c = clone(baseState)
182
+ clones.set(baseState, c)
183
+ // add c to references index for any property value that is immutable
184
+ index(c)
185
+ // find references to baseState
186
+ const refs = findReferences(baseState)
187
+ // replace them with clone
188
+ refs.forEach(r => {
189
+ let source = r.source.deref()
190
+ if (!source) {
191
+ return // continue
192
+ }
193
+ innerProduce(source, (draft) => {
194
+ let current = getRealValue(draft[r.prop])
195
+ if (current===getRealValue(baseState)) { // reference hasn't been changed yet
196
+ draft[r.prop] = c
197
+ }
198
+ })
199
+ })
200
+ return c
201
+ }
202
+ return baseState // baseState is already a clone, since it is mutable
203
+ }
204
+
205
+ /**
206
+ * Given a baseState (frozen) object, this will return a Proxy for it.
207
+ * given a literal, it will just return the literal
208
+ * given a proxy, it will just return the proxy
209
+ * given a mutable object, it will return the object as it needs no proxy
210
+ * @param {any} value The baseState immutable value to proxy
211
+ * @return {any|Proxy} The proxy, if given an immutable object
212
+ */
213
+ function getProxyValue(value) {
214
+ if (types.isProxy(value)) {
215
+ return value
216
+ }
217
+ //@FIXME: it would be nice if there is only ever one proxy of a given value
218
+ if (value && typeof value === 'object' && Object.isFrozen(value)) {
219
+ const proxy = new Proxy({baseState:value}, updateHandler)
220
+ values.set(proxy, value)
221
+ return proxy
222
+ }
223
+ return value
224
+ }
225
+
226
+ /**
227
+ * Given a Proxy object, it will return the original value (baseState) the Proxy
228
+ * was started with. If a clone of that original value is available, it will
229
+ * return that.
230
+ * Given a frozen baseState object, it will return a clone, if available, since that
231
+ * contains the most current state of the object.
232
+ * @param {object} value The potential proxy object or frozen object
233
+ * @return {object} The current state object for this value
234
+ */
235
+ function getRealValue(value) {
236
+ if (types.isProxy(value)) {
237
+ value = values.get(value)
238
+ }
239
+ if (clones.has(value)) {
240
+ value = clones.get(value)
241
+ }
242
+ return value
243
+ }
244
+
245
+ /**
246
+ * This handler automatically returns proxies for all get accesses that result in an object
247
+ * It wraps array functions so that results get proxied, parameters get de-proxied and the
248
+ * actual function is called on the current state of the target object, or for functions that
249
+ * change the actual array, on the clone.
250
+ * @type {Object}
251
+ */
252
+ const updateHandler = {
253
+ get(target, prop, receiver) {
254
+ if (Array.isArray(target.baseState) && target.baseState[prop] instanceof Function) {
255
+ switch(prop) {
256
+ case 'copyWithin':
257
+ case 'fill':
258
+ case 'pop':
259
+ case 'push':
260
+ case 'reverse':
261
+ case 'shift':
262
+ case 'sort':
263
+ case 'splice':
264
+ case 'unshift':
265
+ // these are all functions that alter the array itself, so it
266
+ // needs to be cloned, if not done so already
267
+ //
268
+ return (...args) => {
269
+ args = args.map(arg => getRealValue(arg))
270
+ let clone = getClone(target.baseState)
271
+ let before = shallowClone(clone)
272
+ let result = Array.prototype[prop].apply(clone, args)
273
+ // find differences
274
+ if (before.length>clone.length) {
275
+ for(let i=0,l=before.length-1;i++;i<=l) {
276
+ if (before[i]!==clone[i]) {
277
+ removeReference(clone, i, before[i])
278
+ addReference(clone, i, clone[i])
279
+ }
280
+ }
281
+ } else {
282
+ for(let i=0,l=clone.length;i++;i<=l) {
283
+ if (before[i]!==clone[i]) {
284
+ removeReference(clone, i, before[i])
285
+ addReference(clone, i, clone[i])
286
+ }
287
+ }
288
+ }
289
+ return result
290
+ }
291
+ break
292
+ default:
293
+ return (...args) => {
294
+ args = args.map(arg => getRealValue(arg))
295
+ return Array.prototype[prop].apply(getRealValue(target.baseState), args)
296
+ }
297
+
298
+ break
299
+ }
300
+ } else if (Array.isArray(target.baseState)) {
301
+ switch(prop) {
302
+ case 'length':
303
+ return getRealValue(target.baseState).length
304
+ break
305
+ }
306
+ }
307
+ return getProxyValue(target.baseState[prop])
308
+ },
309
+ set(target, prop, value) {
310
+ let clone = getClone(target.baseState)
311
+ value = getRealValue(value)
312
+ removeReference(clone, prop, clone[prop])
313
+ clone[prop] = value
314
+ addReference(clone, prop, value)
315
+ return true
316
+ },
317
+ deleteProperty(target, prop) {
318
+ let clone = getClone(target.baseState)
319
+ removeReference(clone, prop, clone[prop])
320
+ delete clone[prop]
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Runs the update function on the proxy (draft) of the baseState
326
+ * This function is recursively called to update parent objects that
327
+ * reference the baseState object when a clone is made.
328
+ * @param {object} baseState The base immutable object to alter
329
+ * @param {function} updateFn The update function that alters it
330
+ * @return {object} The nextState, mutable clone
331
+ */
332
+ function innerProduce(baseState, updateFn) {
333
+ let proxy = getProxyValue(baseState)
334
+ updateFn(proxy)
335
+ if (clones.has(baseState)) {
336
+ return clones.get(baseState)
337
+ }
338
+ return baseState // no changes were made
339
+ }
340
+
341
+ if (!references.size) {
342
+ // automatically initialize the references index
343
+ index(baseState)
344
+ }
345
+
346
+ let nextState = innerProduce(baseState, updateFn)
347
+
348
+ clones.forEach(entry => {
349
+ Object.freeze(entry)
350
+ index(entry) // so the references match for the next produce call
351
+ })
352
+
353
+ return nextState
354
+ }