solarite 0.2.2 → 0.2.4

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.
@@ -30,7 +30,7 @@ class WatchExample extends Solarite {
30
30
  ${this.items.map(item => r`
31
31
  <div>${item.name}</div>
32
32
  `)}
33
- ${this.items.length}
33
+ ${() => this.items.length}
34
34
  </watch-example>`;
35
35
  }
36
36
  }
@@ -47,52 +47,143 @@ a.items.push({name: 'Fred'});
47
47
 
48
48
 
49
49
  import Globals from "./Globals.js";
50
- import delve from "../util/delve.js";
51
- //import NodeGroupManager from "./NodeGroupManager.js";
50
+ import Util from "../util/Util.js";
52
51
 
53
52
  let unusedArg = Symbol('unusedArg');
54
53
 
54
+ /**
55
+ * Custom map function triggers the get() Proxy.
56
+ * @param array {Array}
57
+ * @param callback {function}
58
+ * @returns {*[]} */
59
+ function map(array, callback) {
60
+ let result = [];
61
+ for (let i=0; i<array.length; i++)
62
+ result.push(callback(array[i], i, array));
63
+ return result;
64
+ }
65
+
66
+
55
67
  /**
56
68
  *
57
- * @param root {Object}
58
- * @param path {string}
69
+ * @param root {HTMLElement}
70
+ * @param field {string}
59
71
  * @param value {string|Symbol} */
60
- export default function watch3(root, path, value=unusedArg) {
72
+ export default function watch3(root, field, value=unusedArg) {
73
+ // Store internal value used by get/set.
74
+ if (value !== unusedArg)
75
+ root[field] = value;
76
+ else
77
+ value = root[field];
61
78
 
62
- /** @type {ExprPath} The ExprPath that's using this variable.*/
63
- let exprPath;
64
79
 
65
- /** @type {function} Function evaluated by the ExprPath. */
66
- let exprFunction;
80
+ // use a single object for both defineProperty and new Proxy's handler.
81
+ const handler = {
82
+ get(obj, prop, receiver) {
67
83
 
68
- if (value !== unusedArg)
69
- root[path] = value;
84
+ let result = (obj === receiver && field === prop)
85
+ ? value // top-level value.
86
+ : Reflect.get(obj, prop, receiver); // avoid infinite recursion.
70
87
 
71
- // Store internal value used by get/set.
72
- else
73
- value = root[path];
88
+ if (prop === 'map')
89
+
90
+ // Double function so the ExprPath calls it as a function,
91
+ // instead of it being evaluated immediately when the Templat eis created.
92
+ return (callback) => () => {
93
+ let rootNg = Globals.nodeGroups.get(root);
94
+ rootNg.mapCallbacks.set(obj, callback);
95
+ return map(new Proxy(obj, handler), callback);
96
+ }
74
97
 
75
- Object.defineProperty(root, path, {
76
- get() {
77
98
  // Track which ExprPath is using this variable.
78
- if (Globals.currentExprPath)
79
- [exprPath, exprFunction] = Globals.currentExprPath;
99
+ if (Globals.currentExprPath) {
100
+ let [exprPath, exprFunction] = Globals.currentExprPath; // Set in ExprPath.applyExact()
101
+
102
+ let rootNg = Globals.nodeGroups.get(root);
103
+
104
+ // Init for field.
105
+ rootNg.watchedExprPaths[field] = rootNg.watchedExprPaths[field] || new Set();
106
+ rootNg.watchedExprPaths[field].add(exprPath);
107
+ }
108
+
109
+ if (result && typeof result === 'object')
110
+ return new Proxy(result, handler);
80
111
 
81
- // TODO: Return Proxy for non-primitive value, to track path changed.
82
- return value;
112
+ return result;
83
113
  },
84
- set(val) {
85
- value = val;
86
114
 
87
- if (exprFunction) {
88
115
 
89
- // TODO: Will fail for attribute w/ a value having multiple ExprPaths.
90
- // TODO: This won't update a component's expressions.
91
- exprPath.apply(exprFunction);
116
+ // TODO: Will fail for attribute w/ a value having multiple ExprPaths.
117
+ // TODO: This won't update a component's expressions.
118
+ set(obj, prop, val, receiver) {
119
+
120
+ // 1. Set the value.
121
+ if (obj === receiver && field === prop)
122
+ value = val; // top-level value.
123
+ else // avoid infinite recursion.
124
+ Reflect.set(obj, prop, val, receiver);
125
+
126
+ // 2. Add to the list of ExprPaths to re-render.
127
+ let rootNg = Globals.nodeGroups.get(root);
128
+ for (let exprPath of rootNg.watchedExprPaths[field]) {
92
129
 
93
- exprPath.freeNodeGroups(); // TODO: This could be skipped if applyExprs() never marked them as in-use.
130
+ // Update a single NodeGroup created by array.map()
131
+ if (Array.isArray(obj) && parseInt(prop) == prop) {
132
+ let exprsToRender = rootNg.exprsToRender.get(exprPath);
94
133
 
134
+ // If we're not re-rendering the whole thing.
135
+ if (exprsToRender !== true)
136
+ Util.mapAdd(rootNg.exprsToRender, exprPath, [obj, prop, val]);
137
+ }
138
+
139
+ // Reapply the whole expression.
140
+ else
141
+ rootNg.exprsToRender.set(exprPath, true);
95
142
  }
143
+ return true;
96
144
  }
145
+ }
146
+
147
+ Object.defineProperty(root, field, {
148
+ get: () => handler.get(root, field, root),
149
+ set: (val) => handler.set(root, field, val, root)
97
150
  });
98
151
  }
152
+
153
+ /**
154
+ * TODO: Rename so we have watch.add() and watch.render() ?
155
+ * @param root
156
+ * @returns {*[]} */
157
+ export function renderWatched(root) {
158
+ let rootNg = Globals.nodeGroups.get(root);
159
+ let modified = [];
160
+
161
+ for (let [exprPath, params] of rootNg.exprsToRender) {
162
+
163
+ // Reapply the whole expression.
164
+ if (params === true) {
165
+ exprPath.apply(exprPath.watchFunction);
166
+
167
+ // TODO: freeNodeGroups() could be skipped if applyExprs() never marked them as in-use.
168
+ exprPath.freeNodeGroups();
169
+
170
+ modified.push(...exprPath.getNodes());
171
+ }
172
+
173
+ // Update a single NodeGroup created by array.map()
174
+ else {
175
+ for (let row of params) {
176
+ let [obj, prop, value] = row;
177
+ let callback = rootNg.mapCallbacks.get(obj);
178
+ let template = callback(value);
179
+ exprPath.applyLoopItemUpdate(prop, template);
180
+
181
+ modified.push(...exprPath.nodeGroups[prop].getNodes());
182
+ }
183
+ }
184
+ }
185
+
186
+ rootNg.exprsToRender = new Map(); // clear
187
+
188
+ return modified;
189
+ }
@@ -20,7 +20,10 @@ export default class MultiValueMap {
20
20
  return false;
21
21
  }
22
22
 
23
- // Get all values for a key
23
+ /**
24
+ * Get all values for a key.
25
+ * @param key {string}
26
+ * @returns {Set|*[]} */
24
27
  getAll(key) {
25
28
  return this.data[key] || [];
26
29
  }
@@ -29,7 +32,7 @@ export default class MultiValueMap {
29
32
  * Remove one value from a key, and return it.
30
33
  * @param key {string}
31
34
  * @param val If specified, make sure we delete this specific value, if a key exists more than once.
32
- * @returns {*} */
35
+ * @returns {*|undefined} The deleted item. */
33
36
  delete(key, val=undefined) {
34
37
  // if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
35
38
  // debugger;
@@ -65,6 +68,36 @@ export default class MultiValueMap {
65
68
  return result;
66
69
  }
67
70
 
71
+ /**
72
+ * Try to delete an item that matches the key and the isPreferred function.
73
+ * if not the latter, just delete any item that matches the key.
74
+ * @param key {string}
75
+ * @param isPreferred {function}
76
+ * @returns {*|undefined} The deleted item. */
77
+ deletePreferred(key, isPreferred) {
78
+ let result;
79
+ let data = this.data;
80
+ let set = data[key];
81
+ if (!set)
82
+ return undefined;
83
+
84
+ for (let val of set)
85
+ if (isPreferred(val)) {
86
+ set.delete(val);
87
+ result = val;
88
+ break;
89
+ }
90
+ if (!result) {
91
+ [result] = set;
92
+ set.delete(result);
93
+ }
94
+
95
+ if (set.size === 0)
96
+ delete data[key];
97
+
98
+ return result;
99
+ }
100
+
68
101
  hasValue(val) {
69
102
  let data = this.data;
70
103
  let names = [];
package/src/util/Util.js CHANGED
@@ -79,10 +79,35 @@ var Util = {
79
79
 
80
80
  return result;
81
81
  },
82
-
83
82
 
83
+ /**
84
+ * @param map {Map|WeakMap|Object}
85
+ * @param key
86
+ * @param value */
87
+ mapAdd(map, key, value) {
88
+ let isMap = map instanceof Map || map instanceof WeakMap;
89
+ let result = isMap ? map.get(key) : map[key];
90
+ if (!result) {
91
+ result = [value];
92
+ if (isMap)
93
+ map.set(key, result);
94
+ else
95
+ map[key] = result;
96
+ }
97
+ else
98
+ result.push(value);
99
+ },
84
100
 
101
+ weakMemoize(obj, callback) {
102
+ let result = weakMemoizeInputs.get(obj);
103
+ if (!result) {
104
+ result = callback(obj);
105
+ weakMemoizeInputs.set(obj, result);
106
+ }
107
+ return result;
108
+ }
85
109
  };
86
110
 
111
+ let weakMemoizeInputs = new WeakMap();
87
112
 
88
113
  export default Util;
@@ -1,54 +0,0 @@
1
- /**
2
- * partialUpdate test is 20% slower when using this, even before I override any methods!*/
3
- export default class FastLookupArray extends Array {
4
- constructor() {
5
- super(...arguments)
6
- this.indexMap = new Map(); // You can replace with WeakMap() but ensure only objects are added.
7
-
8
- }
9
-
10
- push(item) { // doesn't support pushing multiple
11
- if (!this.indexMap.has(item)) {
12
- this[this.length] = item;
13
- this.indexMap.set(item, this.length - 1);
14
- }
15
- }
16
-
17
- indexOf(item) {
18
- return this.indexMap.get(item) || -1;
19
- }
20
-
21
- remove(item) {
22
- const index = this.indexOf(item);
23
- if (index !== -1) {
24
- this.splice(index, 1);
25
- this.indexMap.delete(item);
26
-
27
- // Adjust the map indices for the items after the removed one
28
- for (let i = index; i < this.length; i++)
29
- this.indexMap.set(this[i], i);
30
- }
31
- }
32
-
33
- insertBefore(referenceItem, newItem) {
34
- const refIndex = this.indexOf(referenceItem);
35
- if (refIndex !== -1) {
36
- this.splice(refIndex, 0, newItem);
37
- this.indexMap.set(newItem, refIndex);
38
-
39
- // Adjust the map indices for the items after the inserted one
40
- for (let i = refIndex + 1; i < this.length; i++)
41
- this.indexMap.set(this[i], i);
42
- }
43
- }
44
-
45
- set(index, item) {
46
- const currentItem = this[index];
47
- if (currentItem !== undefined)
48
- this.indexMap.delete(currentItem);
49
- this[index] = item;
50
- this.indexMap.set(item, index);
51
- }
52
-
53
- // TODO: Override splice?
54
- }
@@ -1,339 +0,0 @@
1
- import {getObjectId} from "../solarite/hash.js";
2
- import Template from "../solarite/Template.js";
3
-
4
- /**
5
- * Convert obj to a non-cyclic form that can be given to JSON.serialize.
6
- * This is useful for creating a string that represents a hash of all the deep values of an object.
7
- *
8
- * One instance of a node always has the same hash, even if its attributes/children have changed.
9
- *
10
- * TODO: Could memoize be faster if given an old version of the same object, and it only finds the differences?
11
- * Or if we inline getObjectId()
12
- *
13
- * @param obj {Object|Array|function|Node}
14
- * @param seenMap Used internally.
15
- * @param idCounter Used internally.
16
- * @returns {{}|*|string} */
17
- export function memoize(obj, seenMap = new Map(), idCounter = {value: 0}) {
18
-
19
- // If obj is a Node, Element, or Window, we treat it specially
20
- if (obj?.nodeType) { // Benchmarking shows this is faster than "instanceof Node"
21
- let nodeId = getObjectId(obj);
22
- return `&N:${nodeId}`;
23
- }
24
-
25
- // TODO: Should I instead use id's to keep track of functions?
26
- let type = typeof obj
27
-
28
- if (obj && type === 'object') {
29
- if (seenMap.has(obj))
30
- return `&O:${seenMap.get(obj)}`;
31
-
32
- else {
33
- seenMap.set(obj, idCounter.value++);
34
- if (typeof obj.memoize === 'function')
35
- return obj.memoize();
36
-
37
- // We iterate through the keys of an object because an object may have changed since the last time we saw it.
38
- const serializedObj = {};
39
- for (let key in obj)
40
- serializedObj[key] = memoize(obj[key], seenMap, idCounter);
41
- return serializedObj;
42
- }
43
- } else if (type === 'function')
44
- //return `&F:${obj}` // String version of function. This version fails the loop.eventBindings test.
45
- return `&F:${getObjectId(obj)}`; // Memory reference to function. this makes it twice as slow!
46
-
47
- else
48
- return obj;
49
- }
50
-
51
-
52
- function fnv1a(...data) {
53
- let hash = 0x811c9dc5; // FNV offset basis
54
- for (let i = 0; i < data.length; i++) {
55
- hash ^= data[i]; // XOR
56
- hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24); // FNV prime multiplication
57
- }
58
- return hash >>> 0; // Convert to 32-bit unsigned integer
59
- }
60
-
61
- const FNV_PRIME_64 = BigInt("1099511628211");
62
- const OFFSET_BASIS_64 = BigInt("14695981039346656037");
63
- function fnv1a_64bit(...data) {
64
- let hash = OFFSET_BASIS_64;
65
- for (let i = 0; i < data.length; i++) {
66
- hash ^= data[i]; // XOR
67
- // hash = BigInt.asUintN(64, hash*FNV_PRIME_64);
68
- }
69
- return hash;
70
- }
71
-
72
-
73
- // Faster than memoize
74
- export function hashObject(obj) {
75
- let combinedData = (0);
76
- if (obj === null)
77
- return combinedData;
78
-
79
- if (Array.isArray(obj)) {
80
- for (let i=(0); i<obj.length; i++) {
81
- let v = hashObject(obj[i])
82
- combinedData = fnv1a(combinedData, i, v);
83
- }
84
- }
85
-
86
- else if (typeof obj === 'object') {
87
- let i = (0);
88
- for (const key in obj) {
89
- let k = getObjectId3(key)
90
- let v = hashObject(obj[key])
91
- combinedData = fnv1a(combinedData, i, k, v);
92
- i++
93
- }
94
- }
95
- else if (typeof obj === 'number')
96
- return (obj)
97
- else if (typeof obj === 'string')
98
- return getObjectId3(obj)
99
- else
100
- return getObjectId2(obj);
101
-
102
- return combinedData;
103
- }
104
-
105
- // TODO: Use this number instead as the high bits for getObjectId2?
106
- let lastObjectId2 = (1_010_101_101); // Big enough to not easily not collide with the space of unhashed numbers.
107
- let objectIds2 = new Map();
108
-
109
- export function getObjectId2(obj) {
110
- let result = objectIds2.get(obj);
111
- if (!result) {
112
- result = (lastObjectId2++); // convert to string, store in result, then add 1 to lastObjectId.
113
- objectIds2.set(obj, result)
114
- }
115
- return result;
116
- }
117
-
118
-
119
- let objectIds3 = {};
120
-
121
- export function getObjectId3(obj) {
122
- // if (typeof obj === 'string') {
123
- // let l = obj.length;
124
- // if (l < 512) { // Faster than getObjectId3. 512 was picked randomly and has not been benchmarked.
125
- // let result = 1;
126
- // for (let i=0; i < l; i++)
127
- // result ^= obj.charCodeAt(i) << (i%16);
128
- // return result
129
- // }
130
- // }
131
-
132
-
133
- let result = objectIds3[obj];
134
- if (!result) {
135
- result = (lastObjectId2++); // convert to string, store in result, then add 1 to lastObjectId.
136
- objectIds3[obj] = result
137
- }
138
- return result;
139
- }
140
-
141
-
142
- let hash64Cache = new WeakMap();
143
- export {hash64Cache};
144
-
145
- // 25% slower than 32-bit version but still faster than memoize+JSON.stringify()
146
- // TODO: hardcoded hash values for true and false?
147
- export function hashObject64(obj) {
148
- let resultLow = 0;
149
- let resultHigh = 0;
150
-
151
- if (obj instanceof Template)
152
- obj = [obj.htmlId, ...obj.exprs]
153
-
154
- if (obj === null)
155
- return [0, 0];
156
-
157
- else if (Array.isArray(obj)) {
158
- let result
159
- // = hash64Cache.get(obj);
160
- // if (!result) {
161
- for (let i = (0); i < obj.length; i++) {
162
- let [vLow, vHigh] = hashObject64(obj[i])
163
- resultLow = fnv1a(resultLow, i, vLow)
164
- resultHigh = fnv1a(resultHigh, vHigh)
165
- }
166
- result = [resultLow, resultHigh]
167
- // hash64Cache.set(obj, result)
168
- //}
169
- return result;
170
- }
171
-
172
- else if (typeof obj === 'object') {
173
- let result
174
- // = hash64Cache.get(obj);
175
- // if (!result) {
176
- let i = (0);
177
- for (const key in obj) {
178
- let k = getObjectId3(key)
179
- let [vLow, vHigh] = hashObject64(obj[key])
180
- resultLow = fnv1a(resultLow, i, vLow)
181
- resultHigh = fnv1a(resultHigh, k, vHigh)
182
- i++
183
- }
184
- result = [resultLow, resultHigh]
185
- // hash64Cache.set(obj, result)
186
- //}
187
-
188
- return result;
189
- }
190
- else if (typeof obj === 'number')
191
- return [0, obj]
192
- else if (typeof obj === 'string')
193
- return [0, getObjectId3(obj)]
194
- else
195
- return [0, getObjectId2(obj)];
196
- }
197
-
198
-
199
-
200
-
201
- // Perforamnce varies greatly.
202
- export function memoize2(obj) {
203
-
204
- if (obj === null)
205
- return 'null';
206
-
207
- switch (obj?.constructor) {
208
- case Function:
209
- case Node:
210
- return getObjectId(obj);
211
- case Array:
212
- return '[' + obj.map(item => memoize2(item)) + ']'
213
- case Object:
214
- let result = '{';
215
- for (let key in obj) {
216
- //key = key.match(/[^A-Z0-9_]/i) ? JSON.stringify(key) : key;
217
- key = (key.includes(':')) ? JSON.stringify(key) : key;
218
- result += `${key}:${memoize2(obj[key])}`
219
- }
220
- return result +'}';
221
- default:
222
- return JSON.stringify(obj);
223
- }
224
- }
225
-
226
- // as fast as memoize2, or faster.
227
- export function memoize3(obj) {
228
-
229
- if (obj === null)
230
- return 'null';
231
-
232
- let type = typeof obj;
233
- if (Array.isArray(obj))
234
- return '[' + obj.map(item => memoize2(item)) + ']'
235
- if (obj instanceof Node || type === 'function')
236
- return getObjectId(obj);
237
- if (type=== 'object') {
238
- let result = '{';
239
- for (let key in obj) {
240
- //key = key.match(/[^A-Z0-9_]/i) ? JSON.stringify(key) : key;
241
- key = (key.includes(':')) ? JSON.stringify(key) : key;
242
- result += `${key}:${memoize2(obj[key])}`
243
- }
244
- return result +'}';
245
- }
246
- return JSON.stringify(obj);
247
- }
248
-
249
-
250
-
251
-
252
- // as fast as memoize2, or faster.
253
- export function memoize4(obj, seenMap=new Map(), idCounter={value: 0}) {
254
- let result;
255
- if (obj instanceof Template) {
256
- if (obj.exprs.length === 1)
257
- return obj.htmlId + '\f' + memoize4(obj.exprs[0]) // \f is the "Form feed" control character, unlikely to be usedin regular text.
258
-
259
- let exprLength = obj.exprs.length;
260
- result = new Array(exprLength + 1);
261
- result[0] = obj.htmlId;
262
- let exprs = obj.exprs;
263
- for (let i = 0; i < exprLength; i++)
264
- result[i + 1] = memoize4(exprs[i])
265
- }
266
-
267
- else if (obj === null)
268
- result = 'null';
269
-
270
- else if (Array.isArray(obj)) {
271
- if (seenMap.has(obj))
272
- result = `&A${seenMap.get(obj)}`;
273
- else {
274
- seenMap.set(obj, idCounter.value++);
275
- result = '[' + obj.map(item => memoize2(item)) + ']'
276
- }
277
- }
278
- else if (obj instanceof Node || typeof obj === 'function')
279
- result = getObjectId(obj);
280
-
281
- else if (typeof obj=== 'object') {
282
- if (seenMap.has(obj))
283
- result = `&O${seenMap.get(obj)}`;
284
-
285
- else {
286
- seenMap.set(obj, idCounter.value++);
287
-
288
- // // String contact approach.
289
- // result = '{';
290
- // for (let key in obj) {
291
- // //key = key.match(/[^A-Z0-9_]/i) ? JSON.stringify(key) : key;
292
- // key = (key.includes(':')) ? JSON.stringify(key) : key;
293
- // result += `${key}:${memoize2(obj[key])}`
294
- // }
295
- // result = result + '}';
296
-
297
- // Buffer approach. Seems slightly faster.
298
- let keys = Object.keys(obj);
299
- let buffer = new Array(keys.length) + 2;
300
- buffer[0] = '{'
301
- buffer[buffer.length-1] = '}'
302
- let idx = 1;
303
- for (let key of keys) {
304
- //key = key.match(/[^A-Z0-9_]/i) ? JSON.stringify(key) : key;
305
- key = (key.includes(':')) ? JSON.stringify(key) : key;
306
- buffer[idx] = `${key}:${memoize2(obj[key])},`
307
- idx++;
308
- }
309
- result = buffer.join('');
310
- }
311
- }
312
- else
313
- result = JSON.stringify(obj);
314
-
315
-
316
- return result;
317
- }
318
-
319
-
320
-
321
-
322
-
323
- // Takes half as long as just calling JSON.serialize!
324
- function containsFunction(obj) {
325
- // Base case: if the object itself is a function
326
- let type = typeof obj;
327
- if (type === 'function') {
328
- return true;
329
- }
330
-
331
- // If the object is an object or array, recursively search its properties
332
- if (type === 'object' && obj !== null)
333
- for (let key in obj)
334
- if (containsFunction(obj[key]))
335
- return true;
336
-
337
- // If none of the properties are functions, return false
338
- return false;
339
- }