@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~ +8 -0
- package/package.json +7 -4
- package/package.json~ +32 -0
- package/src/commands.mjs +7 -0
- package/src/produce.mjs +354 -0
- package/src/server.mjs +152 -147
- package/src/share.mjs +159 -0
- package/src/util.mjs +46 -0
- package/src/worker-command.js +49 -0
- package/src/worker-query.js +148 -0
- package/test/produce.mjs +79 -0
- package/test/share.mjs +18 -0
- package/test/test.jsontag +20 -0
- package/www/codemirror/keymap/vim.js +3 -3
- package/design/access-management.md +0 -25
- package/design/acid.md +0 -31
- package/design/commands.md +0 -25
- package/design/identity.md +0 -71
- package/design/immutability.md +0 -26
- package/design/jsontag-selector.md +0 -365
- package/design/multitasking.md +0 -13
- package/design/thoughts.md +0 -32
- package/docs/docker.md +0 -129
- package/www/assets/css/page.css +0 -34
- package/www/help/index.html +0 -94
package/.npmignore~
ADDED
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muze-nl/simplystore",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
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
|
+
}
|
package/src/commands.mjs
ADDED
package/src/produce.mjs
ADDED
|
@@ -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
|
+
}
|