solarite 0.2.4 → 0.3.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.
Files changed (38) hide show
  1. package/dist/Solarite-debug.js +1457 -1402
  2. package/dist/Solarite.js +1425 -1272
  3. package/dist/Solarite.min.js +2 -2
  4. package/package.json +5 -6
  5. package/readme.md +2 -4
  6. package/src/{solarite/ExprPath.js → ExprPath.js} +421 -231
  7. package/src/Globals.js +79 -0
  8. package/src/HtmlParser.js +91 -0
  9. package/src/{util/MultiValueMap.js → MultiValueMap.js} +22 -26
  10. package/src/{solarite/NodeGroup.js → NodeGroup.js} +286 -226
  11. package/src/{solarite/Shell.js → Shell.js} +119 -92
  12. package/src/Solarite.d.ts +62 -0
  13. package/src/{solarite/Solarite.js → Solarite.js} +15 -13
  14. package/src/{solarite/Template.js → Template.js} +22 -19
  15. package/src/Util.js +330 -0
  16. package/src/{util/Errors.js → assert.js} +1 -0
  17. package/src/createSolarite.js +154 -0
  18. package/src/{util/delve.js → delve.js} +5 -4
  19. package/src/{solarite/getArg.js → getArg.js} +41 -15
  20. package/src/{solarite/r.js → h.js} +59 -29
  21. package/src/{solarite/hash.js → hash.js} +12 -9
  22. package/src/unused/FastLookupArray.js +54 -0
  23. package/src/unused/Hashes.js +339 -0
  24. package/src/unused/InUse.test.js +92 -0
  25. package/src/unused/InUseMap.js +98 -0
  26. package/src/unused/LinkedList.js +117 -0
  27. package/src/unused/LinkedList.test.js +115 -0
  28. package/src/unused/Misc.js +13 -0
  29. package/src/unused/Perf.js +47 -0
  30. package/src/unused/TrackedArray.js +54 -0
  31. package/src/watch.js +546 -0
  32. package/src/solarite/Globals.js +0 -54
  33. package/src/solarite/Util.js +0 -388
  34. package/src/solarite/createSolarite.js +0 -274
  35. package/src/solarite/watch3.js +0 -189
  36. package/src/util/Util.js +0 -113
  37. /package/src/{solarite/udomdiff.js → udomdiff.js} +0 -0
  38. /package/src/{util → unused}/WeakArray.js +0 -0
@@ -1,11 +1,12 @@
1
1
  import Template from "./Template.js";
2
2
  import Util from "./Util.js";
3
3
  import Globals from "./Globals.js";
4
+ import {assert} from "./assert.js";
4
5
 
5
6
  /**
6
7
  * Convert strings to HTMLNodes.
7
- * Using r as a tag will always create a Template.
8
- * Using r() as a function() will always create a DOM element.
8
+ * Using h`...` as a tag will always create a Template.
9
+ * Using h() as a function() will always create a DOM element.
9
10
  *
10
11
  * Features beyond what standard js tagged template strings do:
11
12
  * 1. r`` sub-expressions
@@ -15,24 +16,27 @@ import Globals from "./Globals.js";
15
16
  * 5. TODO: list more
16
17
  *
17
18
  * Currently supported:
18
- * 1. r(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
19
- * 2. r(el, template, ?options) // Render the Template created by #1 to element.
19
+ * 1. h(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
20
+ * 2. h(el, template, ?options) // Render the Template created by #1 to element.
20
21
  *
21
- * 3. r`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
22
+ * 3. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
22
23
  *
23
- * 4. r('Hello'); // Create single text node.
24
- * 5. r('<b>Hello</b>'); // Create single HTMLElement
25
- * 6. r('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
26
- * 7. r()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
24
+ * 4. h('Hello'); // Create single text node.
25
+ * 5. h('<b>Hello</b>'); // Create single HTMLElement
26
+ * 6. h('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
27
+ * 7. h()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
27
28
  * // includes properly handling nested components and r`` sub-expressions.
28
- * 8. r(template) // Render Template created by #1.
29
- *
30
- * 9. r({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
29
+ * 8. h(template) // Render Template created by #1.
31
30
  *
31
+ * 9. h({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
32
+ * 10. h(string, object, ...) // JSX TODO
32
33
  * @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
33
34
  * @param exprs {*[]|string|Template|Object}
34
35
  * @return {Node|HTMLElement|Template} */
35
- export default function r(htmlStrings=undefined, ...exprs) {
36
+ export default function h(htmlStrings=undefined, ...exprs) {
37
+
38
+ if (htmlStrings === undefined && !exprs.length && arguments.length)
39
+ throw new Error('h() cannot be called with undefined.');
36
40
 
37
41
  // TODO: Make this a more flat if/else and call other functions for the logic.
38
42
  if (htmlStrings instanceof Node) {
@@ -55,7 +59,7 @@ export default function r(htmlStrings=undefined, ...exprs) {
55
59
  }
56
60
 
57
61
  // 2. Render template created by #4 to element.
58
- else if (exprs[0] instanceof Template) {
62
+ else { // instanceof Template
59
63
  let options = exprs[1];
60
64
  template.render(parent, options);
61
65
 
@@ -66,16 +70,6 @@ export default function r(htmlStrings=undefined, ...exprs) {
66
70
  parent.append(this.rootNg.getParentNode());
67
71
  }
68
72
  }
69
-
70
-
71
-
72
- // null for expr[0], remove whole element.
73
- // This path never happens?
74
- else {
75
- throw new Error('unsupported');
76
- //let ngm = NodeGroupManager.get(parent);
77
- //ngm.render(null, exprs[1])
78
- }
79
73
  }
80
74
 
81
75
  // 3. Path if used as a template tag.
@@ -84,6 +78,23 @@ export default function r(htmlStrings=undefined, ...exprs) {
84
78
  }
85
79
 
86
80
  else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
81
+ // 10. JSX
82
+ if (typeof exprs[0] === 'object') {
83
+ let tag = htmlStrings;
84
+ let props = exprs[0] || {};
85
+ let children = exprs.slice(1);
86
+
87
+ let templateHtmlStrings = [];
88
+ let templateExprs = [];
89
+
90
+ // TODO How to know which children are static html and which are expression placeholders?
91
+ // Perhaps we have to treat every text child as a string?
92
+
93
+ assert(templateHtmlStrings.length === templateExprs.length+1);
94
+ return new Template(templateHtmlStrings, templateExprs);
95
+ }
96
+
97
+
87
98
  // If it starts with a string, trim both ends.
88
99
  // TODO: Also trim if it ends with whitespace?
89
100
  if (htmlStrings.match(/^\s^</))
@@ -107,7 +118,7 @@ export default function r(htmlStrings=undefined, ...exprs) {
107
118
  else if (htmlStrings === undefined) {
108
119
  return (htmlStrings, ...exprs) => {
109
120
  //Globals.rendered.add(parent)
110
- let template = r(htmlStrings, ...exprs);
121
+ let template = h(htmlStrings, ...exprs);
111
122
  return template.render();
112
123
  }
113
124
  }
@@ -119,13 +130,19 @@ export default function r(htmlStrings=undefined, ...exprs) {
119
130
 
120
131
 
121
132
  // 9. Create dynamic element with render() function.
133
+ // TODO: This path doesn't handle embeds like data-id="..."
122
134
  else if (typeof htmlStrings === 'object') {
123
135
  let obj = htmlStrings;
124
136
 
137
+ if (obj.constructor.name !== 'Object')
138
+ throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
139
+
140
+
125
141
  // Special rebound render path, called by normal path.
142
+ // Intercepts the main r`...` function call inside render().
126
143
  if (Globals.objToEl.has(obj)) {
127
144
  return function(...args) {
128
- let template = r(...args);
145
+ let template = h(...args);
129
146
  let el = template.render();
130
147
  Globals.objToEl.set(obj, el);
131
148
  }.bind(obj);
@@ -134,20 +151,33 @@ export default function r(htmlStrings=undefined, ...exprs) {
134
151
  // Normal path
135
152
  else {
136
153
  Globals.objToEl.set(obj, null);
137
- obj.render(); // Calls the Special rebound render path above, when the render function calls r(this)
154
+ obj[renderF](); // Calls the Special rebound render path above, when the render function calls r(this)
138
155
  let el = Globals.objToEl.get(obj);
139
156
  Globals.objToEl.delete(obj);
140
157
 
141
158
  for (let name in obj)
142
159
  if (typeof obj[name] === 'function')
143
- el[name] = obj[name].bind(el);
160
+ el[name] = obj[name].bind(el); // Make the "this" of functions be el.
161
+ // TODO: But this doesn't work for passing an object with functions as a constructor arg via an attribute:
162
+ // <my-element arg=${{myFunc() { return this }}}
144
163
  else
145
164
  el[name] = obj[name];
146
165
 
166
+ // Bind id's
167
+ // This doesn't work for id's referenced by attributes.
168
+ // for (let idEl of el.querySelectorAll('[id],[data-id]')) {
169
+ // Util.bindId(el, idEl);
170
+ // Util.bindId(obj, idEl);
171
+ // }
172
+ // TODO: Bind styles
173
+
147
174
  return el;
148
175
  }
149
176
  }
150
177
 
151
178
  else
152
179
  throw new Error('Unsupported arguments.')
153
- }
180
+ }
181
+
182
+ // Trick to prevent minifier from renaming this function.
183
+ let renderF = 'render';
@@ -10,7 +10,7 @@ export function getObjectId(obj) {
10
10
 
11
11
  let result = objectIds.get(obj);
12
12
  if (!result) { // convert to string, store in result, then add 1 to lastObjectId.
13
- result = (lastObjectId++); // We use a unique prefix to ensure it doesn't collide w/ strings not from getObjectId()
13
+ result = '~\f' + (lastObjectId++); // We use a unique, 2-byte prefix to ensure it doesn't collide w/ strings not from getObjectId()
14
14
  objectIds.set(obj, result)
15
15
  }
16
16
  return result;
@@ -29,14 +29,7 @@ function toJSON() {
29
29
 
30
30
  // Node.prototype.toJSON = toJSON;
31
31
  // Function.prototype.toJSON = toJSON;
32
- // Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
33
- // The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
34
- // So we check the assignments on every run of getObjectHash()
35
- if (Node.prototype.toJSON !== toJSON) {
36
- Node.prototype.toJSON = toJSON;
37
- if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
38
- Function.prototype.toJSON = toJSON;
39
- }
32
+
40
33
 
41
34
  /**
42
35
  * Get a string that uniquely maps to the values of the given object.
@@ -51,6 +44,16 @@ if (Node.prototype.toJSON !== toJSON) {
51
44
  * @param obj {*}
52
45
  * @returns {string} */
53
46
  export function getObjectHash(obj) {
47
+
48
+ // Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
49
+ // The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
50
+ // So we check the assignments on every run of getObjectHash()
51
+ if (Node.prototype.toJSON !== toJSON) {
52
+ Node.prototype.toJSON = toJSON;
53
+ if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
54
+ Function.prototype.toJSON = toJSON;
55
+ }
56
+
54
57
  let result;
55
58
  isHashing = true;
56
59
  try {
@@ -0,0 +1,54 @@
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
+ }
@@ -0,0 +1,339 @@
1
+ import {getObjectId} from "../hash.js";
2
+ import Template from "../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
+ }
@@ -0,0 +1,92 @@
1
+ import {InUseArray, InUseMap} from "./InUseMap.js";
2
+ import Testimony, {assert} from "../../tests/Testimony.js";
3
+
4
+ Testimony.test('InUseArray.constructor', () => {
5
+ const arr = new InUseArray();
6
+ assert.eq(arr.count, 0, 'Initial count should be 0');
7
+ assert.eq(arr.length, 0, 'Initial length should be 0');
8
+ });
9
+
10
+ Testimony.test('InUseArray.use - with no value', () => {
11
+ const arr = new InUseArray(1, 2, 3);
12
+ const val = arr.use();
13
+ assert.eq(val, 1, 'Should return the first value');
14
+ assert.eq(arr.count, 1, 'Count should be incremented');
15
+ });
16
+
17
+ Testimony.test('InUseArray.use - with specific value', () => {
18
+ const arr = new InUseArray(1, 2, 3);
19
+ arr.count = 1; // Mark the first element as in-use
20
+ const val = arr.use(3);
21
+ assert.eq(val, 3, 'Should return the specified value');
22
+ assert.eq(arr.count, 2, 'Count should be incremented');
23
+ assert.eq(arr[1], 3, 'Value should be moved to the in-use section');
24
+ assert.eq(arr[2], 2, 'Remaining value should be in the available section');
25
+ });
26
+
27
+ Testimony.test('InUseArray.use', () => {
28
+ const arr = new InUseArray(1, 2, 3);
29
+ let val = arr.use(4);
30
+ assert.eq(val, undefined,'Should return undefined');
31
+ assert.eq(arr.use(), 1);
32
+ assert.eq(arr.use(3), 3);
33
+ assert.eq(arr.use(), 2);
34
+ assert.eq(arr.use(), undefined);
35
+ });
36
+
37
+ Testimony.test('InUseArray.use - all in use', () => {
38
+ const arr = new InUseArray(1, 2, 3);
39
+ arr.count = 3;
40
+ const val = arr.use();
41
+ assert.eq(val, undefined, 'Should return undefined');
42
+ });
43
+
44
+ Testimony.test('InUseArray.free', () => {
45
+ const arr = new InUseArray(1, 2, 3);
46
+ arr.count = 3;
47
+ arr.free();
48
+ assert.eq(arr.count, 0, 'Count should be reset to 0');
49
+ });
50
+
51
+ Testimony.test('InUseArray.reset', () => {
52
+ const arr = new InUseArray(1, 2, 3);
53
+ arr.reset();
54
+ assert.eq(arr.length, 0, 'Array should be empty');
55
+ });
56
+
57
+
58
+
59
+
60
+ Testimony.test('InUseMap.add', () => {
61
+ const map = new InUseMap();
62
+ map.add('key', 'value');
63
+ assert.eq(map.data['key'][0], 'value', 'Value should be added to the map');
64
+ });
65
+
66
+ Testimony.test('InUseMap.getAll', () => {
67
+ const map = new InUseMap();
68
+ map.add('key', 'value');
69
+ const values = map.getAll('key');
70
+ assert.eq(values.length, 1, 'Should return an array with one value');
71
+ assert.eq(values[0], 'value', 'Should return the correct value');
72
+ });
73
+
74
+ Testimony.test('InUseMap.use', () => {
75
+ const map = new InUseMap();
76
+ map.add('key', 'value');
77
+ let value = map.use('key');
78
+ assert.eq(value, 'value', 'Should return and mark the value as in-use');
79
+ value = map.use('key');
80
+ assert.eq(value, undefined, 'Should return and mark the value as in-use');
81
+ });
82
+
83
+ Testimony.test('InUseMap.hasValue', () => {
84
+ const map = new InUseMap();
85
+ map.add('key1', 'value1');
86
+ map.add('key2', 'value2');
87
+ const keys = map.hasValue('value1');
88
+ assert.eq(keys.length, 1, 'Should return an array with one key');
89
+ assert.eq(keys[0], 'key1', 'Should return the correct key');
90
+ });
91
+
92
+