solarite 0.5.2 → 0.7.0

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/dist/Solarite-debug.js +2823 -1856
  3. package/dist/Solarite.js +2779 -1824
  4. package/dist/Solarite.min.js +2 -4
  5. package/package.json +16 -3
  6. package/readme.md +58 -11
  7. package/src/Globals.js +54 -72
  8. package/src/HtmlParser.js +90 -90
  9. package/src/MultiValueMap.js +57 -105
  10. package/src/NodeGroup.js +624 -470
  11. package/src/Path.js +224 -211
  12. package/src/PathToAttribValue.js +401 -261
  13. package/src/PathToAttribs.js +113 -80
  14. package/src/PathToComment.js +7 -7
  15. package/src/PathToComponent.js +183 -188
  16. package/src/PathToEvent.js +76 -64
  17. package/src/PathToKey.js +19 -0
  18. package/src/PathToNodes.js +1053 -566
  19. package/src/RootNodeGroup.js +120 -8
  20. package/src/Shell.js +570 -348
  21. package/src/Solarite.d.ts +134 -113
  22. package/src/Solarite.js +243 -286
  23. package/src/Template.js +195 -274
  24. package/src/Util.js +353 -351
  25. package/src/assert.js +10 -10
  26. package/src/assignAttributes.js +63 -0
  27. package/src/delve.js +55 -43
  28. package/src/h.js +220 -138
  29. package/src/jsx-dev-runtime.d.ts +1 -0
  30. package/src/jsx-dev-runtime.js +5 -0
  31. package/src/jsx-runtime.d.ts +21 -0
  32. package/src/jsx-runtime.js +84 -0
  33. package/src/jsx.js +194 -0
  34. package/src/toEl.js +77 -82
  35. package/dist/udomdiff-license.txt +0 -18
  36. package/src/getArg.js +0 -137
  37. package/src/hash.js +0 -89
  38. package/src/udomdiff.js +0 -176
  39. package/src/unused/FastLookupArray.js +0 -54
  40. package/src/unused/Hashes.js +0 -339
  41. package/src/unused/InUse.test.js +0 -92
  42. package/src/unused/InUseMap.js +0 -98
  43. package/src/unused/LinkedList.js +0 -117
  44. package/src/unused/LinkedList.test.js +0 -115
  45. package/src/unused/Misc.js +0 -13
  46. package/src/unused/Perf.js +0 -47
  47. package/src/unused/TrackedArray.js +0 -54
  48. package/src/unused/WeakArray.js +0 -33
  49. package/src/watch.js +0 -543
package/src/jsx.js ADDED
@@ -0,0 +1,194 @@
1
+ import Template from "./Template.js";
2
+ import Util from "./Util.js";
3
+
4
+ /**
5
+ * JSX support for Solarite. Two tiers share this one module:
6
+ *
7
+ * Tier 1 (precompile): a build step (Deno's `jsx:"precompile"` or a Solarite build plugin) hoists
8
+ * each element's static HTML into a module-level array and emits
9
+ * `jsxTemplate(statics, jsxAttr(name, value), jsxEscape(child), ...)`. Stable array identity maps
10
+ * straight onto Template => Shell cache, closeKey, and NodeGroup reuse all work; perf equals tagged
11
+ * templates. jsxTemplate/jsxAttr/jsxEscape live in jsx-runtime.js; the JsxAttr class below is the
12
+ * whole-attribute hole they produce.
13
+ *
14
+ * Tier 2 (classic/automatic factory): tsc/esbuild/Vite emit `h(tag, props, ...children)` or
15
+ * `jsx(tag, props, key)` with no hoisting. jsxToTemplate() interns the static HTML per
16
+ * (tag, prop-names, child-count) shape so identity is stable per call site even though a new
17
+ * Template is built each render. Every prop value and child is an expression hole, never diffed
18
+ * as static. */
19
+
20
+ // Fragment for <>...</> in the classic/automatic factories. Tier 1 fragments need no import.
21
+ export const Fragment = Symbol('Fragment');
22
+
23
+ /**
24
+ * A whole-attribute hole from Tier 1, e.g. `<a ` + jsxAttr("href", v) + `>`. It lands at a
25
+ * PathToAttribs hole (a bare ${} between attributes); PathToAttribs detects this class and routes
26
+ * the value through the normal attribute/event/property application. */
27
+ export class JsxAttr {
28
+ constructor(name, value) {
29
+ this.name = name;
30
+ this.value = value;
31
+ }
32
+ }
33
+
34
+ const selfClosingTags = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']);
35
+
36
+ // Tier 2 shape cache: `tag\0name1\0name2\0#childCount` => htmlStrings array (stable identity).
37
+ const shapeCache = new Map();
38
+
39
+ /**
40
+ * Serialize a style object to css text. {color:'red', fontSize:'1px'} => 'color:red;font-size:1px'.
41
+ * A string passes through unchanged.
42
+ * @param value {Object|string}
43
+ * @return {string} */
44
+ export function styleToCss(value) {
45
+ if (value === null || typeof value !== 'object')
46
+ return value;
47
+ let css = '';
48
+ for (let k in value) {
49
+ let v = value[k];
50
+ if (v === null || v === undefined || v === false)
51
+ continue;
52
+ css += (css ? ';' : '') + Util.camelToDashes(k) + ':' + v;
53
+ }
54
+ return css;
55
+ }
56
+
57
+ /** Escape a static attribute value for inlining into the html string. */
58
+ function escapeAttr(value) {
59
+ return ('' + value).replace(/&/g, '&amp;').replace(/"/g, '&quot;');
60
+ }
61
+
62
+ /**
63
+ * Build the interned htmlStrings for a Tier 2 element shape, matching how a tagged template would
64
+ * split `<tag openStatic name1=${} name2=${}>${child0}${child1}</tag>`.
65
+ * @param tag {string}
66
+ * @param openStatic {string} Static attributes inlined into the opening tag (e.g. ` id="x"`).
67
+ * @param names {string[]} Dynamic attribute names in order.
68
+ * @param childCount {int}
69
+ * @param isVoid {boolean}
70
+ * @return {string[]} */
71
+ function buildShapeHtml(tag, openStatic, names, childCount, isVoid) {
72
+ let strings = [];
73
+ let cur = '<' + tag + openStatic;
74
+ for (let name of names) {
75
+ strings.push(cur + ' ' + name + '=');
76
+ cur = '';
77
+ }
78
+ cur += '>';
79
+ if (isVoid) {
80
+ strings.push(cur);
81
+ return strings;
82
+ }
83
+ if (childCount === 0)
84
+ strings.push(cur + '</' + tag + '>');
85
+ else {
86
+ strings.push(cur);
87
+ for (let i = 1; i < childCount; i++)
88
+ strings.push('');
89
+ strings.push('</' + tag + '>');
90
+ }
91
+ return strings;
92
+ }
93
+
94
+ /**
95
+ * Build a Template for a Tier 2 (classic/automatic) JSX element.
96
+ * @param tag {string|Function|symbol} Intrinsic tag name, component class/function, or Fragment.
97
+ * @param props {?Object} Attributes/props (children and key are pulled out by the caller for the
98
+ * automatic runtime; for the classic factory they may still be present and are stripped here).
99
+ * @param children {any[]} One hole per child.
100
+ * @param key {*} Optional list key (automatic runtime passes it separately).
101
+ * @return {Template} */
102
+ export function jsxToTemplate(tag, props, children=[], key=undefined) {
103
+ props = props || {};
104
+
105
+ // 1. Fragment: only child holes, no element wrapper.
106
+ if (tag === Fragment) {
107
+ let html = [''];
108
+ for (let i = 0; i < children.length; i++)
109
+ html.push('');
110
+ return new Template(html, children);
111
+ }
112
+
113
+ // 2. Component (class or function).
114
+ if (typeof tag === 'function') {
115
+
116
+ // 2a. Custom element class => emit <tag-name ...props>children</tag-name>; PathToComponent
117
+ // instantiates it exactly like a tagged-template component.
118
+ if (tag.prototype instanceof HTMLElement) {
119
+ Util.defineClass(tag);
120
+ let tagName = customElements.getName ? customElements.getName(tag) : Util.camelToDashes(tag.name);
121
+ if (tagName && !tagName.includes('-'))
122
+ tagName += '-element';
123
+ return buildIntrinsic(tagName, props, children, key);
124
+ }
125
+
126
+ // 2b. Plain function component: call it with props (+ children) and expect a Template back.
127
+ let p = {};
128
+ for (let name in props)
129
+ if (name !== 'key')
130
+ p[name] = name === 'style' ? styleToCss(props[name]) : props[name];
131
+ if (!('children' in p) && children.length)
132
+ p.children = children.length === 1 ? children[0] : children;
133
+ let t = tag(p);
134
+ if (key === undefined)
135
+ key = props.key;
136
+ if (key !== undefined && t instanceof Template)
137
+ t.key = key;
138
+ return t;
139
+ }
140
+
141
+ // 3. Intrinsic element.
142
+ return buildIntrinsic(tag, props, children, key);
143
+ }
144
+
145
+ /**
146
+ * @param tag {string}
147
+ * @param props {Object}
148
+ * @param children {any[]}
149
+ * @param key {*}
150
+ * @return {Template} */
151
+ function buildIntrinsic(tag, props, children, key) {
152
+ let isVoid = selfClosingTags.has(tag.toLowerCase());
153
+ let names = [];
154
+ let values = [];
155
+ let openStatic = ''; // Static id/data-id inlined so their embeds (this.x references) resolve.
156
+
157
+ for (let name in props) {
158
+ if (name === 'children') // The automatic runtime stashes children here; they're passed separately.
159
+ continue;
160
+ if (name === 'key') {
161
+ if (key === undefined)
162
+ key = props[name];
163
+ continue;
164
+ }
165
+ let value = props[name];
166
+
167
+ // id/data-id reference embeds only work when the attribute is static in the html, so inline
168
+ // string-valued ones (the usual case) instead of making them holes. Tier 1 transforms
169
+ // already inline static attributes; this keeps the Tier 2 classic factory on par.
170
+ if ((name === 'id' || name === 'data-id') && typeof value === 'string') {
171
+ openStatic += ` ${name}="${escapeAttr(value)}"`;
172
+ continue;
173
+ }
174
+
175
+ if (name === 'style')
176
+ value = styleToCss(value);
177
+ names.push(name);
178
+ values.push(value);
179
+ }
180
+
181
+ let childCount = isVoid ? 0 : children.length;
182
+ let shapeKey = tag + openStatic + '\0' + names.join('\0') + '\0#' + childCount;
183
+ let html = shapeCache.get(shapeKey);
184
+ if (!html) {
185
+ html = buildShapeHtml(tag, openStatic, names, childCount, isVoid);
186
+ shapeCache.set(shapeKey, html);
187
+ }
188
+
189
+ let exprs = isVoid ? values : values.concat(children);
190
+ let t = new Template(html, exprs);
191
+ if (key !== undefined)
192
+ t.key = key;
193
+ return t;
194
+ }
package/src/toEl.js CHANGED
@@ -1,83 +1,78 @@
1
- import Globals from "./Globals.js";
2
- import Template from "./Template.js";
3
- import Util from "./Util.js";
4
-
5
- /**
6
- * Convert a template, string, or object into a DOM Node or Element
7
- *
8
- * 1. toEl('Hello'); // Create single text node.
9
- * 2. toEl('<b>Hello</b>'); // Create single HTMLElement
10
- * 3. toEl('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
11
- * 4. toEl(template) // Render Template created by h`<html>` or h();
12
- * 5. toEl({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
13
- * @param arg {string|Template|{render:()=>void}}
14
- * @returns {Node|DocumentFragment|HTMLElement} */
15
- export default function toEl(arg) {
16
-
17
- if (typeof arg === 'string') {
18
- let html = arg;
19
-
20
- // If it's an element with whitespace before or after it, trim both ends.
21
- if (html.match(/^\s^<\S+/) || html.match(/\S+>\s+$/))
22
- html = html.trim();
23
-
24
- // We create a new one each time because otherwise
25
- // the returned fragment will have its content replaced by a subsequent call.
26
- let templateEl = Globals.doc.createElement('template');
27
- templateEl.innerHTML = html;
28
-
29
- // 1+2. Return Node if there's one child.
30
- let relevantNodes = Util.trimEmptyNodes(templateEl.content.childNodes);
31
- if (relevantNodes.length === 1)
32
- return relevantNodes[0];
33
-
34
- // 3. Otherwise return DocumentFragment.
35
- return templateEl.content;
36
- }
37
-
38
- // 4.
39
- if (arg instanceof Template) {
40
- return arg.render();
41
- }
42
-
43
- // 5. Create dynamic element from an object with a render() function.
44
- // TODO: This path doesn't handle embeds like data-id="..."
45
- else if (arg && typeof arg === 'object') {
46
- let obj = arg;
47
-
48
- if (obj.constructor.name !== 'Object')
49
- throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
50
-
51
- // Normal path
52
- if (!Globals.objToEl.has(obj)) {
53
- Globals.objToEl.set(obj, null);
54
- obj[renderF](); // Calls the Special rebound render path above, when the render function calls h(this)
55
- let el = Globals.objToEl.get(obj);
56
- Globals.objToEl.delete(obj);
57
-
58
- for (let name in obj)
59
- if (typeof obj[name] === 'function')
60
- el[name] = obj[name].bind(el); // Make the "this" of functions be el.
61
- // TODO: But this doesn't work for passing an object with functions as a constructor arg via an attribute:
62
- // <my-element arg=${{myFunc() { return this }}}
63
- else
64
- el[name] = obj[name];
65
-
66
- // Bind id's
67
- // This doesn't work for id's referenced by attributes.
68
- // for (let idEl of el.querySelectorAll('[id],[data-id]')) {
69
- // Util.bindId(el, idEl);
70
- // Util.bindId(obj, idEl);
71
- // }
72
- // TODO: Bind styles
73
-
74
- return el;
75
- }
76
- }
77
-
78
- throw new Error('toEl() does not support argument of type: ' + (arg ? typeof arg : arg));
79
- }
80
-
81
-
82
- // Trick to prevent minifier from renaming this function.
1
+ import Globals from "./Globals.js";
2
+ import Template from "./Template.js";
3
+ import Util from "./Util.js";
4
+
5
+ /**
6
+ * Convert a template, string, or object into a DOM Node or Element
7
+ *
8
+ * 1. toEl('Hello'); // Create single text node.
9
+ * 2. toEl('<b>Hello</b>'); // Create single HTMLElement
10
+ * 3. toEl('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
11
+ * 4. toEl(template) // Render Template created by h`<html>` or h();
12
+ * 5. toEl({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
13
+ * @param arg {string|Template|{render:()=>void}}
14
+ * @returns {Node|DocumentFragment|HTMLElement} */
15
+ export default function toEl(arg) {
16
+
17
+ if (typeof arg === 'string') {
18
+
19
+ // We create a new one each time because otherwise
20
+ // the returned fragment will have its content replaced by a subsequent call.
21
+ let templateEl = Globals.doc.createElement('template');
22
+ templateEl.innerHTML = arg;
23
+
24
+ // 1+2. Return Node if there's one child.
25
+ let relevantNodes = Util.trimEmptyNodes(templateEl.content.childNodes);
26
+ if (relevantNodes.length === 1)
27
+ return relevantNodes[0];
28
+
29
+ // 3. Otherwise return DocumentFragment.
30
+ return templateEl.content;
31
+ }
32
+
33
+ // 4.
34
+ if (arg instanceof Template) {
35
+ return arg.render();
36
+ }
37
+
38
+ // 5. Create dynamic element from an object with a render() function.
39
+ // TODO: This path doesn't handle embeds like data-id="..."
40
+ else if (arg && typeof arg === 'object') {
41
+ let obj = arg;
42
+
43
+ if (obj.constructor.name !== 'Object')
44
+ throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
45
+
46
+ // Normal path
47
+ if (!Globals.objToEl.has(obj)) {
48
+ Globals.objToEl.set(obj, null);
49
+ obj[renderF](); // Calls the Special rebound render path above, when the render function calls h(this)
50
+ let el = Globals.objToEl.get(obj);
51
+ Globals.objToEl.delete(obj);
52
+
53
+ for (let name in obj)
54
+ if (typeof obj[name] === 'function')
55
+ el[name] = obj[name].bind(el); // Make the "this" of functions be el.
56
+ // TODO: But this doesn't work for passing an object with functions as a constructor arg via an attribute:
57
+ // <my-element arg=${{myFunc() { return this }}}
58
+ else
59
+ el[name] = obj[name];
60
+
61
+ // Bind id's
62
+ // This doesn't work for id's referenced by attributes.
63
+ // for (let idEl of el.querySelectorAll('[id],[data-id]')) {
64
+ // Util.bindId(el, idEl);
65
+ // Util.bindId(obj, idEl);
66
+ // }
67
+ // TODO: Bind styles
68
+
69
+ return el;
70
+ }
71
+ }
72
+
73
+ throw new Error('toEl() does not support argument of type: ' + (arg ? typeof arg : arg));
74
+ }
75
+
76
+
77
+ // Trick to prevent minifier from renaming this function.
83
78
  let renderF = 'render';
@@ -1,18 +0,0 @@
1
- The below license is for the udomdiff library, used by Solarite.js.
2
- https://github.com/WebReflection/udomdiff/tree/main
3
- -------------
4
- ISC License
5
-
6
- Copyright (c) 2020, Andrea Giammarchi, @WebReflection
7
-
8
- Permission to use, copy, modify, and/or distribute this software for any
9
- purpose with or without fee is hereby granted, provided that the above
10
- copyright notice and this permission notice appear in all copies.
11
-
12
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
13
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
14
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
15
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
16
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
17
- OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
18
- PERFORMANCE OF THIS SOFTWARE.
package/src/getArg.js DELETED
@@ -1,137 +0,0 @@
1
- import Util from "./Util.js";
2
-
3
-
4
- /**
5
- * @deprecated Inherit from Solarite and pass arribs to super() instead.
6
- * There are three ways to create an instance of a Solarite Component:
7
- * 1. new ComponentName(3); // direct class instantiation
8
- * 2. h(this)`<div><component-name user-id=${3}></component-name></div>; // as a child of another Component.
9
- * 3. <body><component-name user-id="3"></component-name></body> // in the Document html.
10
- *
11
- * When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
12
- * sure we get the correct value via all three paths, we write our constructors according to the following
13
- * example. Note that constructor args are embedded in an object, and must be all lower-case because
14
- * Browsers make all html attribute names lowercase.
15
- *
16
- * @example
17
- * constructor({name, userId=1}={}) {
18
- * super();
19
- *
20
- * // Get value from "name" attriute if persent, otherwise from name constructor arg.
21
- * this.name = getArg(this, 'name', name);
22
- *
23
- * // Optionally convert the value to an integer.
24
- * this.userId = getArg(this, 'user-id', userId, ArgType.Int);
25
- * }
26
- *
27
- * @param el {HTMLElement}
28
- * @param attributeName {string} Attribute name. Not case-sensitive.
29
- * @param defaultValue {*} Default value to use if attribute doesn't exist. Typically the argument from the constructor.
30
- * @param type {ArgType|function|Class|*[]}
31
- * If an array, use the value if it's in the array, otherwise return undefined.
32
- * If it's a function, pass the value to the function and return the result.
33
- * @return {*} Undefined if attribute isn't set and there's no defaultValue, or if the value couldn't be parsed as the type. */
34
- export function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String) {
35
- let val = defaultValue;
36
- let attrVal = el.getAttribute(attributeName) || el.getAttribute(Util.camelToDashes(attributeName));
37
- if (attrVal !== null) // If attribute doesn't exist.
38
- val = attrVal;
39
-
40
- if (Array.isArray(type))
41
- return type.includes(val) ? val : undefined;
42
-
43
- if (typeof type === 'function') {
44
- return type.constructor
45
- ? new type(val) // arg type is custom Class
46
- : type(val); // arg type is custom function
47
- }
48
-
49
- // If bool, it's true as long as it exists and its value isn't falsey.
50
- if (type===ArgType.Bool) {
51
- let lAttrVal = typeof val === 'string' ? val.toLowerCase() : val;
52
- if (['false', '0', false, 0, null, undefined, NaN].includes(lAttrVal))
53
- return false;
54
- if (['true', true].includes(lAttrVal) || parseFloat(lAttrVal) !== 0)
55
- return true;
56
- return undefined;
57
- }
58
-
59
- // Attribute doesn't exist
60
- switch (type) {
61
- case ArgType.Int:
62
- return parseInt(val);
63
- case ArgType.Float:
64
- return parseFloat(val);
65
- case ArgType.String:
66
- return [undefined, null, false].includes(val) ? '' : (val+'');
67
- case ArgType.Json:
68
- case ArgType.Eval:
69
- if (typeof val === 'string' && val.length)
70
- try {
71
- if (type === ArgType.Json)
72
- return JSON.parse(val);
73
- else
74
- return eval(`(${val})`);
75
- } catch (e) {
76
- return val;
77
- }
78
- else return val;
79
-
80
- // type not provided
81
- default:
82
- return val;
83
- }
84
- }
85
-
86
-
87
- /**
88
- * @deprecated for Solarite.getAttribs()
89
- * Experimental. Set multiple arguments/attributes all at once.
90
- * @param el {HTMLElement}
91
- * @param args {Record<string, any>}
92
- * @param types {Record<string, ArgType|function|Class>}
93
- *
94
- * @example
95
- * constructor({user, path}={}) {
96
- * setArgs(this, arguments[0], {user: User, path: ArgType.String});
97
- *
98
- * // Equivalent to:
99
- * this.user = getArg(this, user, 'user', User); // or new User(user);
100
- * this.path = getArg(this, path, 'path', ArgType.String);
101
- * }
102
- */
103
- export function setArgs(el, args, types) {
104
- for (let name in args)
105
- this[name] = getArg(el, name, args[name], types[name] || ArgType.String);
106
- }
107
-
108
-
109
- /**
110
- * @deprecated
111
- * @enum */
112
- var ArgType = {
113
-
114
- /**
115
- * false, 0, null, undefined, '0', and 'false' (case-insensitive) become false.
116
- * Anything else, including empty string becomes true.
117
- * Empty string is true because attributes with no value should be evaulated as true. */
118
- Bool: 'Bool',
119
-
120
- Int: 'Int',
121
- Float: 'Float',
122
- String: 'String',
123
-
124
- /** @deprecated for Json */
125
- JSON: 'Json',
126
-
127
- /**
128
- * Parse the string value as JSON.
129
- * If it's not parsable, return the value as a string. */
130
- Json: 'Json',
131
-
132
- /**
133
- * Evaluate the string as JavaScript using the eval() function.
134
- * If it can't be evaluated, return the original string. */
135
- Eval: 'Eval'
136
- }
137
- export {ArgType};
package/src/hash.js DELETED
@@ -1,89 +0,0 @@
1
- let lastObjectId = 1>>>0; // Is a 32-bit int faster to increment than JavaScript's Number, which is a 64-bit float?
2
- let objectIds = new WeakMap();
3
-
4
- /**
5
- * @param obj {Object|string|Node}
6
- * @returns {string} */
7
- export function getObjectId(obj) {
8
- // if (typeof obj === 'function')
9
- // return obj.toString(); // This fails to detect when a function's bound variables changes.
10
-
11
- let result = objectIds.get(obj);
12
- if (result===undefined) { // convert to string, store in result, then add 1 to lastObjectId.
13
- result = '~@' + (lastObjectId++); // We use a unique, 2-byte prefix to ensure it doesn't collide w/ strings not from getObjectId()
14
- objectIds.set(obj, result)
15
- }
16
- return result;
17
- }
18
-
19
- /**
20
- * Control how JSON.stringify() handles Nodes and Functions.
21
- * Normally, we'd pass a replacer() function argument to JSON.stringify() to handle Nodes and Functions.
22
- * But that makes JSON.stringify() take twice as long to run.
23
- * Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty.
24
- * TODO: This needs to be benchmarked again after the json rewrite in Chrome 138. */
25
- let isHashing = true;
26
- function toJSON() {
27
- return isHashing ? getObjectId(this) : this
28
- }
29
-
30
-
31
- // Node.prototype.toJSON = toJSON;
32
- // Function.prototype.toJSON = toJSON;
33
-
34
-
35
- /**
36
- * Get a string that uniquely maps to the values of the given object.
37
- * If a value in obj changes, calling getObjectHash(obj) will then return a different hash.
38
- * This is used by NodeGroupManager to create a hash that represents the current values of a NodeGroup.
39
- *
40
- * Relies on the Node and Function prototypes being overridden above.
41
- *
42
- * Note that passing an integer may collide with the number we get from hashing an object.
43
- * But we don't handle that case because we need max performance and Solarite never passes integers to this function.
44
- *
45
- * @param obj {*}
46
- * @returns {string} */
47
- export function getObjectHash(obj) {
48
-
49
- // Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
50
- // The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
51
- // So we check the assignments on every run of getObjectHash()
52
- // TODO: Cache references to Node.prototype and Function.prototype:
53
- if (Node.prototype.toJSON !== toJSON) {
54
- Node.prototype.toJSON = toJSON;
55
- if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
56
- Function.prototype.toJSON = toJSON;
57
- }
58
-
59
- isHashing = true;
60
- try {
61
- return JSON.stringify(obj);
62
- }
63
- catch(e) {
64
- return getObjectHashCircular(obj);
65
- }
66
- finally {
67
- isHashing = false;
68
- }
69
- }
70
-
71
- /**
72
- * Slower hashing method that supports circular references.
73
- * @param obj
74
- * @returns {string} */
75
- function getObjectHashCircular(obj) {
76
-
77
- //console.log('circular')
78
- // Slower version that handles circular references.
79
- // Just adding any callback at all, even one that just returns the value, makes JSON.stringify() twice as slow.
80
- const seen = new Set();
81
- return JSON.stringify(obj, (key, value) => {
82
- if (typeof value === 'object' && value !== null) {
83
- if (seen.has(value))
84
- return getObjectId(value);
85
- seen.add(value);
86
- }
87
- return value;
88
- });
89
- }