From 36fbe4f7d15012c628005fd0d6badeba95b17b03 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 09:59:17 -0800 Subject: [PATCH 01/18] Use sexps not strings for encoding instances and proxies. Also, handle the case where a string ends up being shared. --- src/loggy-intf/export/LoggedValueEncoder.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/loggy-intf/export/LoggedValueEncoder.js b/src/loggy-intf/export/LoggedValueEncoder.js index 0fde6bd98..5167ff849 100644 --- a/src/loggy-intf/export/LoggedValueEncoder.js +++ b/src/loggy-intf/export/LoggedValueEncoder.js @@ -99,7 +99,10 @@ export class LoggedValueEncoder extends BaseValueVisitor { const visitedArray = this._prot_visitProperties(sexpArray); return new Sexp(...visitedArray); } else { - return this._prot_labelFromValue(node); + const constructor = Reflect.getPrototypeOf(node).constructor; + return constructor + ? new Sexp(this._prot_nameFromValue(constructor), '...') + : new Sexp('Object', this._prot_labelFromValue(node), '...'); } } @@ -110,7 +113,7 @@ export class LoggedValueEncoder extends BaseValueVisitor { /** @override */ _impl_visitProxy(node, isFunction_unused) { - return this._prot_labelFromValue(node); + return new Sexp('Proxy', this._prot_nameFromValue(node)); } /** @override */ @@ -192,6 +195,11 @@ export class LoggedValueEncoder extends BaseValueVisitor { return this.#makeDefIfAppropriate(node, result); } + /** @override */ + _impl_visitString(node) { + return this.#makeDefIfAppropriate(node, node); + } + /** * Wraps a result in a "def" if it is in fact a defining value occurrence. * From 22be88b9d745d3fac2416e89cca22a442190a423 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 10:00:47 -0800 Subject: [PATCH 02/18] Add tests. --- .../tests/LoggedValueEncoder.test.js | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/loggy-intf/tests/LoggedValueEncoder.test.js diff --git a/src/loggy-intf/tests/LoggedValueEncoder.test.js b/src/loggy-intf/tests/LoggedValueEncoder.test.js new file mode 100644 index 000000000..b5e3d0329 --- /dev/null +++ b/src/loggy-intf/tests/LoggedValueEncoder.test.js @@ -0,0 +1,83 @@ +// Copyright 2022-2024 the Lactoserv Authors (Dan Bornstein et alia). +// SPDX-License-Identifier: Apache-2.0 + +import { LoggedValueEncoder } from '@this/loggy-intf'; +import { Sexp } from '@this/sexp'; + +function nullOutObjectPrototypes(value) { + if (!(value && (typeof value === 'object'))) { + return value; + } else if (Array.isArray(value)) { + const result = []; + for (const v of value) { + result.push(nullOutObjectPrototypes(v)); + } + return result; + } else if (Reflect.getPrototypeOf(value) === Object.prototype) { + const result = Object.create(null); + for (const [k, v] of Object.entries(value)) { + result[k] = nullOutObjectPrototypes(v); + } + return result; + } else { + return value; + } +} + +function sexp(type, ...args) { + return new Sexp(type, ...args); +} + +describe('encode()', () => { + // Cases where the result should be equal to the input. + test.each` + value + ${null} + ${false} + ${true} + ${'florp'} + ${123.456} + ${[]} + ${[1, 2, 3, 'four']} + ${[[null], [false], [[55]]]} + `('($#) self-encodes $value', ({ value }) => { + const got = LoggedValueEncoder.encode(value); + expect(got).toStrictEqual(value); + }); + + // Plain objects are expected to get converted to null-prototype objects. + test.each` + value + ${{}} + ${{ a: 10, b: 'twenty' }} + ${{ abc: { x: [{ d: 'efg' }] } }} + `('($#) self-encodes $value except with a `null` object prototypes', ({ value }) => { + const got = LoggedValueEncoder.encode(value); + const expected = nullOutObjectPrototypes(value); + expect(got).toStrictEqual(expected); + }); + + class SomeClass { + // @defaultConstructor + } + + const someFunc = () => null; + + // Stuff that isn't JSON-encodable should end up in the form of a sexp. + test.each` + value | expected + ${undefined} | ${sexp('Undefined')}} + ${[undefined]} | ${[sexp('Undefined')]}} + ${321123n} | ${sexp('BigInt', '321123')}} + ${Symbol('xyz')} | ${sexp('Symbol', 'xyz')}} + ${Symbol.for('blorp')} | ${sexp('Symbol', 'blorp', true)}} + ${new Map()} | ${sexp('Map', '...')} + ${new Proxy({}, {})} | ${sexp('Proxy', '')} + ${new Proxy([], {})} | ${sexp('Proxy', '')} + ${new Proxy(new SomeClass(), {})} | ${sexp('Proxy', '')} + ${new Proxy(someFunc, {})} | ${sexp('Proxy', 'someFunc')} + `('($#) correctly encodes $value', ({ value, expected }) => { + const got = LoggedValueEncoder.encode(value); + expect(got).toStrictEqual(expected); + }); +}); From a3c04328080713743107595bf0e6da9da1ce1ef1 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 10:01:26 -0800 Subject: [PATCH 03/18] Changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f42d49972..d72748618 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ Other notable changes: * general: * Allow node version 23. * `loggy-intf` / `loggy`: - * Minor tweaks to "human" (non-JSON) log rendering. + * Made several improvements to "human" (non-JSON) log rendering. * `structy`: * Started allowing any object (plain or not) to be used as the argument to the `BaseStruct` constructor. From f63662abd294e64b9e16007ba33d0eb0e76f3544 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 10:11:49 -0800 Subject: [PATCH 04/18] Add type check. --- src/valvis/export/VisitRef.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/valvis/export/VisitRef.js b/src/valvis/export/VisitRef.js index d33da5c17..58c2b752d 100644 --- a/src/valvis/export/VisitRef.js +++ b/src/valvis/export/VisitRef.js @@ -1,6 +1,8 @@ // Copyright 2022-2024 the Lactoserv Authors (Dan Bornstein et alia). // SPDX-License-Identifier: Apache-2.0 +import { MustBe } from '@this/typey'; + import { BaseDefRef } from '#x/BaseDefRef'; import { VisitDef } from '#x/VisitDef'; @@ -28,6 +30,8 @@ export class VisitRef extends BaseDefRef { * @param {VisitDef} def The corresponding def. */ constructor(def) { + MustBe.instanceOf(def, VisitDef); + const valueArg = def.isFinished() ? [def.value] : []; super(def.index, ...valueArg); From a91ddc58a85d2863de09ea5e728aa318abd2ab49 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 10:15:46 -0800 Subject: [PATCH 05/18] Add tests. --- .../tests/LoggedValueEncoder.test.js | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/loggy-intf/tests/LoggedValueEncoder.test.js b/src/loggy-intf/tests/LoggedValueEncoder.test.js index b5e3d0329..6fa2f18d8 100644 --- a/src/loggy-intf/tests/LoggedValueEncoder.test.js +++ b/src/loggy-intf/tests/LoggedValueEncoder.test.js @@ -1,22 +1,24 @@ // Copyright 2022-2024 the Lactoserv Authors (Dan Bornstein et alia). // SPDX-License-Identifier: Apache-2.0 +import { Duration } from '@this/quant'; import { LoggedValueEncoder } from '@this/loggy-intf'; import { Sexp } from '@this/sexp'; +import { VisitDef, VisitRef } from '@this/valvis'; -function nullOutObjectPrototypes(value) { +function withNullObjectProtos(value) { if (!(value && (typeof value === 'object'))) { return value; } else if (Array.isArray(value)) { const result = []; for (const v of value) { - result.push(nullOutObjectPrototypes(v)); + result.push(withNullObjectProtos(v)); } return result; } else if (Reflect.getPrototypeOf(value) === Object.prototype) { const result = Object.create(null); for (const [k, v] of Object.entries(value)) { - result[k] = nullOutObjectPrototypes(v); + result[k] = withNullObjectProtos(v); } return result; } else { @@ -53,7 +55,7 @@ describe('encode()', () => { ${{ abc: { x: [{ d: 'efg' }] } }} `('($#) self-encodes $value except with a `null` object prototypes', ({ value }) => { const got = LoggedValueEncoder.encode(value); - const expected = nullOutObjectPrototypes(value); + const expected = withNullObjectProtos(value); expect(got).toStrictEqual(expected); }); @@ -71,6 +73,7 @@ describe('encode()', () => { ${321123n} | ${sexp('BigInt', '321123')}} ${Symbol('xyz')} | ${sexp('Symbol', 'xyz')}} ${Symbol.for('blorp')} | ${sexp('Symbol', 'blorp', true)}} + ${new Duration(12.34)} | ${sexp('Duration', 12.34, '12.340 sec')} ${new Map()} | ${sexp('Map', '...')} ${new Proxy({}, {})} | ${sexp('Proxy', '')} ${new Proxy([], {})} | ${sexp('Proxy', '')} @@ -80,4 +83,37 @@ describe('encode()', () => { const got = LoggedValueEncoder.encode(value); expect(got).toStrictEqual(expected); }); + + test('does not def-ref a small-enough array', () => { + const value = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + const got = LoggedValueEncoder.encode([value, value]); + expect(got).toStrictEqual([value, value]); + }); + + test('does not def-ref a small-enough plain object', () => { + const value = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10 }; + const expected = withNullObjectProtos(value); + const got = LoggedValueEncoder.encode([value, value]); + expect(got).toStrictEqual([expected, expected]); + }); + + test('def-refs a large-enough array', () => { + const value = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; + const def = new VisitDef(0, value); + const expected = [def, new VisitRef(def)]; + const got = LoggedValueEncoder.encode([value, value]); + expect(got).toStrictEqual(expected); + }); + + test('def-refs a large-enough plain object', () => { + const value = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10, k: 11 }; + const def = new VisitDef(0, value); + const expected = [def, new VisitRef(def)]; + const got = LoggedValueEncoder.encode([value, value]); + expect(got).toStrictEqual(expected); + }); + + test('def-refs the sexp from an instance', () => { + + }); }); From be6f7cd475f049702555d6fab08babe3f14025bd Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 10:19:10 -0800 Subject: [PATCH 06/18] Handle the case where an encoded instance is shared. --- src/loggy-intf/export/LoggedValueEncoder.js | 5 +++++ src/loggy-intf/tests/LoggedValueEncoder.test.js | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/loggy-intf/export/LoggedValueEncoder.js b/src/loggy-intf/export/LoggedValueEncoder.js index 5167ff849..f8931c6fe 100644 --- a/src/loggy-intf/export/LoggedValueEncoder.js +++ b/src/loggy-intf/export/LoggedValueEncoder.js @@ -189,6 +189,11 @@ export class LoggedValueEncoder extends BaseValueVisitor { return this.#makeDefIfAppropriate(node, result); } + /** @override */ + _impl_visitInstance(node) { + return this.#makeDefIfAppropriate(node, node); + } + /** @override */ _impl_visitPlainObject(node) { const result = this._prot_visitProperties(node); diff --git a/src/loggy-intf/tests/LoggedValueEncoder.test.js b/src/loggy-intf/tests/LoggedValueEncoder.test.js index 6fa2f18d8..1307bee59 100644 --- a/src/loggy-intf/tests/LoggedValueEncoder.test.js +++ b/src/loggy-intf/tests/LoggedValueEncoder.test.js @@ -114,6 +114,10 @@ describe('encode()', () => { }); test('def-refs the sexp from an instance', () => { - + const value = new Map(); + const def = new VisitDef(0, sexp('Map', '...')); + const expected = [def, new VisitRef(def)]; + const got = LoggedValueEncoder.encode([value, value]); + expect(got).toStrictEqual(expected); }); }); From 493f4bd8d9cd1baf6df7bd33944e774fe50f8ff6 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 10:32:04 -0800 Subject: [PATCH 07/18] Simplify detection of reference cycles. --- CHANGELOG.md | 2 ++ src/valvis/export/BaseValueVisitor.js | 19 ++++--------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d72748618..a5a3c3d7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ Other notable changes: `BaseStruct` constructor. * Added the option to allow undeclared properties to be allowed and dynamically vetted, via two additional `_impl*` methods. +* `valvis`: + * Simplified detection of reference cycles. * `webapp-builtins`: * Simplified naming scheme for preserved log files: Names now always include a `-` suffix after the date. diff --git a/src/valvis/export/BaseValueVisitor.js b/src/valvis/export/BaseValueVisitor.js index 3a7d8998f..b415ea065 100644 --- a/src/valvis/export/BaseValueVisitor.js +++ b/src/valvis/export/BaseValueVisitor.js @@ -68,14 +68,6 @@ export class BaseValueVisitor { */ #allRefs = []; - /** - * The set of visit entries that are actively being visited. This is used to - * detect attempts to visit a value containing a circular reference. - * - * @type {Set} - */ - #activeVisits = new Set(); - /** * Constructs an instance whose purpose in life is to visit the indicated * value. @@ -826,13 +818,13 @@ export class BaseValueVisitor { const already = this.#visitEntries.get(node); if (already) { - let ref = already.ref; + const isCycleHead = !already.isFinished(); + let ref = already.ref; if (ref || already.shouldRef()) { // We either already have a ref, or we are supposed to make a ref. - const isCycleHead = !already.isFinished(); - const result = isCycleHead ? null : already.extractSync(false); + const result = isCycleHead ? null : already.extractSync(false); if (!ref) { already.setDefRef(this.#allRefs.length); @@ -843,7 +835,7 @@ export class BaseValueVisitor { this._impl_revisit(node, result, isCycleHead, ref); return this.#visitNode(ref); - } else if (this.#activeVisits.has(already)) { + } else if (isCycleHead) { // We have encountered the head of a reference cycle that was _not_ // handled by making a "ref" object for the back-reference. @@ -1224,7 +1216,6 @@ export class BaseValueVisitor { // about circular references. this.#promise = (async () => { const visitor = this.#visitor; - visitor.#activeVisits.add(this); try { let result = visitor.#visitNode0(this.#node); @@ -1244,8 +1235,6 @@ export class BaseValueVisitor { this.#finishWithError(e); } - visitor.#activeVisits.delete(this); - return this; })(); } From 39d751dc4937d6bc8cf22f6986d2f90d219f2544 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 10:51:08 -0800 Subject: [PATCH 08/18] Add `isCycleHead` argument to `_impl_shouldRef()`. --- CHANGELOG.md | 5 ++++- src/valvis/export/BaseValueVisitor.js | 23 ++++++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5a3c3d7f..333229c2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,10 @@ Other notable changes: * Added the option to allow undeclared properties to be allowed and dynamically vetted, via two additional `_impl*` methods. * `valvis`: - * Simplified detection of reference cycles. + * `BaseValueVisitor`: + * Simplified detection of reference cycles. + * Added argument `isCycleHead` to `_impl_shouldRef()`, so client code can + choose to be more cycle-aware. * `webapp-builtins`: * Simplified naming scheme for preserved log files: Names now always include a `-` suffix after the date. diff --git a/src/valvis/export/BaseValueVisitor.js b/src/valvis/export/BaseValueVisitor.js index b415ea065..ed221014d 100644 --- a/src/valvis/export/BaseValueVisitor.js +++ b/src/valvis/export/BaseValueVisitor.js @@ -327,10 +327,12 @@ export class BaseValueVisitor { * `false` for everything else. * * @param {*} value The value to check. + * @param {boolean} isCycleHead Was `value` just detected as the head of a + * reference cyle? * @returns {boolean} `true` if `value` should be converted into a reference. * or `false` if not. */ - _impl_shouldRef(value) { // eslint-disable-line no-unused-vars + _impl_shouldRef(value, isCycleHead) { // eslint-disable-line no-unused-vars return false; } @@ -821,7 +823,7 @@ export class BaseValueVisitor { const isCycleHead = !already.isFinished(); let ref = already.ref; - if (ref || already.shouldRef()) { + if (ref || already.shouldRef(isCycleHead)) { // We either already have a ref, or we are supposed to make a ref. const result = isCycleHead ? null : already.extractSync(false); @@ -1173,10 +1175,12 @@ export class BaseValueVisitor { * {@link #_impl_shouldRef} is never called more than once per original * value. * + * @param {boolean} isCycleHead Was this entry's value just detected as the + * head of a reference cyle? * @returns {boolean} `true` iff repeat visits to this instance should * result in a ref to this instance's result. */ - shouldRef() { + shouldRef(isCycleHead) { if (this.#shouldRef === null) { const visitor = this.#visitor; const node = this.#node; @@ -1187,20 +1191,29 @@ export class BaseValueVisitor { case 'function': case 'string': case 'symbol': { - result = visitor._impl_shouldRef(node); // eslint-disable-line no-restricted-syntax + result = visitor._impl_shouldRef(node, isCycleHead); // eslint-disable-line no-restricted-syntax break; } case 'object': { if ((node !== null) && !visitor.#isAssociatedDefOrRef(node)) { - result = visitor._impl_shouldRef(node); // eslint-disable-line no-restricted-syntax + result = visitor._impl_shouldRef(node, isCycleHead); // eslint-disable-line no-restricted-syntax } break; } } this.#shouldRef = result; + } else if (isCycleHead) { + /* c8 ignore start */ + // Shouldn't happen: By construction, the moment a second reference to a + // cycle head is encountered, this method should end up getting called + // with `isCycleHead === true`, and if the `_impl` declined to make a + // ref, the whole visit should end up erroring out with an error along + // the lines of "can't visit circular reference." + throw new Error('Shouldn\'t happen: Cycle head detected too late.'); + /* c8 ignore stop */ } return this.#shouldRef; From 6c34f83d9dd39e21c2373c0e865cce88eebb7fc1 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 10:59:55 -0800 Subject: [PATCH 09/18] Add tests. --- src/valvis/tests/BaseValueVisitor.test.js | 24 ++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/valvis/tests/BaseValueVisitor.test.js b/src/valvis/tests/BaseValueVisitor.test.js index a90210bc9..18b8342b0 100644 --- a/src/valvis/tests/BaseValueVisitor.test.js +++ b/src/valvis/tests/BaseValueVisitor.test.js @@ -67,7 +67,7 @@ class ProxyAwareVisitor extends BaseValueVisitor { * Visitor subclass, which is set up to use refs for duplicate objects. */ class RefMakingVisitor extends BaseValueVisitor { - _impl_shouldRef(value) { + _impl_shouldRef(value, isCycleHead_unused) { return (typeof value === 'object'); } @@ -516,7 +516,7 @@ ${'visitAsyncWrap'} | ${true} | ${false} | ${true} const MSG = 'Not today, Satan!'; class TestVisitor extends BaseValueVisitor { - _impl_shouldRef(node_unused) { + _impl_shouldRef(node_unused, isCycleHead_unused) { return true; } @@ -699,7 +699,7 @@ ${'visitAsyncWrap'} | ${true} | ${false} | ${true} class TestVisitor extends BaseValueVisitor { calledOn = []; - _impl_shouldRef(node) { + _impl_shouldRef(node, isCycleHead_unused) { this.calledOn.push(node); return false; } @@ -747,7 +747,16 @@ ${'visitAsyncWrap'} | ${true} | ${false} | ${true} describe('when `_impl_shouldRef()` can return `true`', () => { class TestVisitor extends RefMakingVisitor { - refs = []; + cycleHeads = []; + refs = []; + + _impl_shouldRef(node, isCycleHead) { + if (isCycleHead) { + this.cycleHeads.push(node); + } + + return super._impl_shouldRef(node, isCycleHead); + } _impl_newRef(ref) { this.refs.push({ ref, wasFinished: ref.isFinished() }); @@ -768,6 +777,8 @@ ${'visitAsyncWrap'} | ${true} | ${false} | ${true} expect(ref.value).toBe(got[1][1]); expect(visitor.getVisitResult(shared)).toBe(ref.value); expect(wasFinished).toBeTrue(); + + expect(visitor.cycleHeads).toBeArrayOfSize(0); } }); }); @@ -789,6 +800,9 @@ ${'visitAsyncWrap'} | ${true} | ${false} | ${true} expect(visitor.getVisitResult(selfRef)).toBe(ref.value); expect(ref).toBe(ref.value[2]); expect(wasFinished).toBeFalse(); + + expect(visitor.cycleHeads).toBeArrayOfSize(1); + expect(visitor.cycleHeads[0]).toBe(selfRef); } }); }); @@ -819,7 +833,7 @@ describe('_impl_revisit()', () => { this.#doRefs = doRefs; } - _impl_shouldRef(node) { + _impl_shouldRef(node, isCycleHead_unused) { return this.#doRefs && (typeof node === 'object'); } From 3e5f4856152710d55fc434eb83fc53ecfb399c74 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 11:17:28 -0800 Subject: [PATCH 10/18] Add tests. --- src/sexp/tests/Sexp.test.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/sexp/tests/Sexp.test.js b/src/sexp/tests/Sexp.test.js index dde7611cf..b52153ebb 100644 --- a/src/sexp/tests/Sexp.test.js +++ b/src/sexp/tests/Sexp.test.js @@ -181,3 +181,27 @@ describe('.toArray()', () => { expect(sexp.toArray()).toStrictEqual(expected); }); }); + +// This validates that it's safe to use `expect(sexp).toStrictEqual(otherSexp)` +// in test cases throughout the system. +describe('validating Jest usage', () => { + test('can use `expect().toStrictEqual()` to check `functor`', () => { + const sexp1a = new Sexp('x', 1, 2, 3); + const sexp1b = new Sexp('x', 1, 2, 3); + const sexp2 = new Sexp('y', 1, 2, 3); + + expect(sexp1a).toStrictEqual(sexp1a); + expect(sexp1a).toStrictEqual(sexp1b); + expect(sexp1a).not.toStrictEqual(sexp2); + }); + + test('can use `expect().toStrictEqual()` to check `args`', () => { + const sexp1a = new Sexp('x', 1, 2, 3); + const sexp1b = new Sexp('x', 1, 2, 3); + const sexp2 = new Sexp('x', 1, 2, 3, 'floop'); + + expect(sexp1a).toStrictEqual(sexp1a); + expect(sexp1a).toStrictEqual(sexp1b); + expect(sexp1a).not.toStrictEqual(sexp2); + }); +}); From aea8b964fd80a896745977a95bb78880ad68171a Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 12:33:52 -0800 Subject: [PATCH 11/18] Add `deconstruct()` to `Visit{Def,Ref}`. --- src/valvis/export/BaseDefRef.js | 5 ++- src/valvis/export/VisitDef.js | 24 ++++++++++++++ src/valvis/export/VisitRef.js | 6 ++++ src/valvis/tests/VisitDef.test.js | 54 +++++++++++++++++++++++++++++++ src/valvis/tests/VisitRef.test.js | 24 ++++++++++++++ 5 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/valvis/export/BaseDefRef.js b/src/valvis/export/BaseDefRef.js index d652ee369..7cc8c9cc3 100644 --- a/src/valvis/export/BaseDefRef.js +++ b/src/valvis/export/BaseDefRef.js @@ -1,6 +1,7 @@ // Copyright 2022-2024 the Lactoserv Authors (Dan Bornstein et alia). // SPDX-License-Identifier: Apache-2.0 +import { IntfDeconstructable } from '@this/sexp'; import { Methods } from '@this/typey'; /** @@ -25,8 +26,9 @@ import { Methods } from '@this/typey'; * implementation details. * * @abstract + * @implements {IntfDeconstructable} */ -export class BaseDefRef { +export class BaseDefRef extends IntfDeconstructable { /** * The reference index number. * @@ -40,6 +42,7 @@ export class BaseDefRef { * @param {number} index The reference index number. */ constructor(index) { + super(); this.#index = index; } diff --git a/src/valvis/export/VisitDef.js b/src/valvis/export/VisitDef.js index ff11404a9..03501e145 100644 --- a/src/valvis/export/VisitDef.js +++ b/src/valvis/export/VisitDef.js @@ -1,6 +1,8 @@ // Copyright 2022-2024 the Lactoserv Authors (Dan Bornstein et alia). // SPDX-License-Identifier: Apache-2.0 +import { Sexp } from '@this/sexp'; + import { BaseDefRef } from '#x/BaseDefRef'; import { VisitRef } from '#x/VisitRef'; @@ -86,6 +88,28 @@ export class VisitDef extends BaseDefRef { } } + /** @override */ + deconstruct(forLogging) { + let valueArgs; + + if (this.#finished) { + if (this.#error) { + if (!forLogging) { + // Can't deconstruct an errored def when _not_ logging, as there's no + // way to construct such a one. + this.value; // This will throw. + } + valueArgs = ['error', this.#error]; + } else { + valueArgs = [this.#value]; + } + } else { + valueArgs = []; + } + + return new Sexp(this.constructor, this.index, ...valueArgs); + } + /** * Indicates that this instance's visit has now finished unsuccessfully with * the given error. It is only ever valid to call this on an unfinished diff --git a/src/valvis/export/VisitRef.js b/src/valvis/export/VisitRef.js index 58c2b752d..65bb787e1 100644 --- a/src/valvis/export/VisitRef.js +++ b/src/valvis/export/VisitRef.js @@ -1,6 +1,7 @@ // Copyright 2022-2024 the Lactoserv Authors (Dan Bornstein et alia). // SPDX-License-Identifier: Apache-2.0 +import { Sexp } from '@this/sexp'; import { MustBe } from '@this/typey'; import { BaseDefRef } from '#x/BaseDefRef'; @@ -53,6 +54,11 @@ export class VisitRef extends BaseDefRef { return this.#def.value; } + /** @override */ + deconstruct(forLogging_unused) { + return new Sexp(this.constructor, this.#def); + } + /** @override */ isFinished() { return this.#def.isFinished(); diff --git a/src/valvis/tests/VisitDef.test.js b/src/valvis/tests/VisitDef.test.js index 50d612de6..046ea6f62 100644 --- a/src/valvis/tests/VisitDef.test.js +++ b/src/valvis/tests/VisitDef.test.js @@ -1,6 +1,7 @@ // Copyright 2022-2024 the Lactoserv Authors (Dan Bornstein et alia). // SPDX-License-Identifier: Apache-2.0 +import { Sexp } from '@this/sexp'; import { VisitDef, VisitRef } from '@this/valvis'; @@ -101,6 +102,59 @@ ${'finishWithValue'} | ${'florp'} }); }); +describe('deconstruct()', () => { + test('works on an unfinished instance', () => { + const def = new VisitDef(1); + const expected = new Sexp(VisitDef, 1); + expect(def.deconstruct()).toStrictEqual(expected); + }); + + test('works on a (non-error) finished instance', () => { + const def = new VisitDef(2, 'beep'); + const expected = new Sexp(VisitDef, 2, 'beep'); + expect(def.deconstruct()).toStrictEqual(expected); + }); + + test('works on an error-finished instance when given `forLogging === true`', () => { + const def = new VisitDef(3); + const error = new Error('eeeek!'); + const expected = new Sexp(VisitDef, 3, 'error', error); + def.finishWithError(error); + expect(def.deconstruct(true)).toStrictEqual(expected); + }); + + test('throws on an error-finished instance when given `forLogging === false`', () => { + const def = new VisitDef(4); + const error = new Error('eeeek!'); + def.finishWithError(error); + expect(() => def.deconstruct()).toThrow(); + }); +}); + +describe('isFinished()', () => { + test('is `false` on an instance constructed without a value', () => { + const def = new VisitDef(901); + expect(def.isFinished()).toBeFalse(); + }); + + test('is `true` on an instance constructed with a value', () => { + const def = new VisitDef(902, 'bloop'); + expect(def.isFinished()).toBeTrue(); + }); + + test('is `true` on an instance which became finished via `finishWithValue()`', () => { + const def = new VisitDef(903); + def.finishWithValue('bleep'); + expect(def.isFinished()).toBeTrue(); + }); + + test('is `true` on an instance which became finished via `finishWithError()`', () => { + const def = new VisitDef(904); + def.finishWithError(new Error('oy!')); + expect(def.isFinished()).toBeTrue(); + }); +}); + describe('.toJSON()', () => { test('returns the expected replacement for a value-bearing instance', () => { const def = new VisitDef(20, 'bongo'); diff --git a/src/valvis/tests/VisitRef.test.js b/src/valvis/tests/VisitRef.test.js index df30a937e..aa40970df 100644 --- a/src/valvis/tests/VisitRef.test.js +++ b/src/valvis/tests/VisitRef.test.js @@ -1,6 +1,7 @@ // Copyright 2022-2024 the Lactoserv Authors (Dan Bornstein et alia). // SPDX-License-Identifier: Apache-2.0 +import { Sexp } from '@this/sexp'; import { VisitDef, VisitRef } from '@this/valvis'; @@ -33,6 +34,29 @@ describe('.ref', () => { }); }); +describe('deconstruct()', () => { + test('returns the `def` from the constructor', () => { + const def = new VisitDef(456, 'floop'); + const ref = new VisitRef(def); + const expected = new Sexp(VisitRef, def); + expect(ref.deconstruct()).toStrictEqual(expected); + }); +}); + +describe('isFinished()', () => { + test('is `false` when the associated `def` is unfinished', () => { + const def = new VisitDef(901); + const ref = new VisitRef(def); + expect(ref.isFinished()).toBeFalse(); + }); + + test('is `true` when the associated `def` is finished', () => { + const def = new VisitDef(902, 'bloop'); + const ref = new VisitRef(def); + expect(ref.isFinished()).toBeTrue(); + }); +}); + describe('.toJSON()', () => { test('returns the expected replacement', () => { const ref = new VisitRef(new VisitDef(2, null)); From b0f169c1199635e4b5f71dc3ae1fa5ea04c23692 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 12:40:43 -0800 Subject: [PATCH 12/18] Fix test. --- src/loggy-intf/tests/LoggedValueEncoder.test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/loggy-intf/tests/LoggedValueEncoder.test.js b/src/loggy-intf/tests/LoggedValueEncoder.test.js index 1307bee59..6ea000c8a 100644 --- a/src/loggy-intf/tests/LoggedValueEncoder.test.js +++ b/src/loggy-intf/tests/LoggedValueEncoder.test.js @@ -107,9 +107,13 @@ describe('encode()', () => { test('def-refs a large-enough plain object', () => { const value = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10, k: 11 }; - const def = new VisitDef(0, value); + const def = new VisitDef(0, withNullObjectProtos(value)); const expected = [def, new VisitRef(def)]; const got = LoggedValueEncoder.encode([value, value]); + + expect(got).toBeArrayOfSize(2); + expect(got[0]).toStrictEqual(expected[0]); + expect(got[1]).toStrictEqual(expected[1]); expect(got).toStrictEqual(expected); }); From e240a6415decc568ac86472472b5ed767e0bce56 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 13:06:39 -0800 Subject: [PATCH 13/18] Add custom equality testing for tests in this project. --- src/main-tester/index.js | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/main-tester/index.js b/src/main-tester/index.js index b6d287bc0..691bf70d7 100644 --- a/src/main-tester/index.js +++ b/src/main-tester/index.js @@ -1,6 +1,8 @@ // Copyright 2022-2024 the Lactoserv Authors (Dan Bornstein et alia). // SPDX-License-Identifier: Apache-2.0 +import { inspect } from 'node:util'; + process.on('warning', (warning) => { if (warning.name === 'ExperimentalWarning') { if (/VM Modules/.test(warning.message)) { @@ -13,3 +15,51 @@ process.on('warning', (warning) => { // eslint-disable-next-line no-console console.log('%s: %s\n', warning.name, warning.message); }); + +// This extends Jest's equality checks to deal reasonably with a few more cases +// than it does by default, by leaning on common patterns in this codebase. + +function lactoservEquals(a, b, customTesters) { + // Note: `return undefined` means, "The comparison is not handled by this + // tester." + + if ((typeof a !== 'object') || (typeof b !== 'object')) { + return undefined; + } else if ((a === null) || (b === null)) { + return undefined; + } else if (Array.isArray(a) || Array.isArray(b)) { + return undefined; + } + + const aProto = Reflect.getPrototypeOf(a); + const bProto = Reflect.getPrototypeOf(b); + + if ((aProto === null) || (aProto !== bProto)) { + return undefined; + } + + // At this point, we're looking at two non-array objects of the same class + // (that is, with the same prototype). + + if (typeof aProto.deconstruct === 'function') { + // They (effectively) implement `IntfDeconstructable`. Note: There is no + // need to check the `functor` of the deconstructed result, since it's + // going to be `aProto` (which is also `bProto`). + const aDecon = a.deconstruct(true).args; + const bDecon = b.deconstruct(true).args; + return this.equals(aDecon, bDecon, customTesters); + } else if (typeof aProto[inspect.custom] === 'function') { + // They have a custom inspector. + return this.equals(inspect(a), inspect(b)); + } else if (typeof aProto.toJSON === 'function') { + // They have a custom JSON encoder. This is the least-preferable option + // because it's the most likely to end up losing information. + return this.equals(a.toJSON(), b.toJSON(), customTesters); + } + + return undefined; +} + +// The linter doesn't realize this file is loaded in the context of testing, so +// it would complain that `expect` is undefined without the disable directive. +expect.addEqualityTesters([lactoservEquals]); // eslint-disable-line no-undef From 45e1c0b6ea7f56ae4889e285d671b8e678efe9c9 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 13:07:11 -0800 Subject: [PATCH 14/18] Add tests to confirm that the custom equality testing works. --- src/valvis/tests/VisitDef.test.js | 43 +++++++++++++++++++++++++++++++ src/valvis/tests/VisitRef.test.js | 30 +++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/valvis/tests/VisitDef.test.js b/src/valvis/tests/VisitDef.test.js index 046ea6f62..9a89a78df 100644 --- a/src/valvis/tests/VisitDef.test.js +++ b/src/valvis/tests/VisitDef.test.js @@ -173,3 +173,46 @@ describe('.toJSON()', () => { expect(def.toJSON()).toStrictEqual({ '@def': [22, null, 'Eek!'] }); }); }); + +// This validates that it's safe to use `expect(def).toStrictEqual(def)` +// in test cases throughout the system. +describe('validating Jest usage', () => { + test('can use `expect().toStrictEqual()` to check `index`es', () => { + const def1a = new VisitDef(1, 'boop'); + const def1b = new VisitDef(1, 'boop'); + const def2 = new VisitDef(2, 'boop'); + + expect(def1a).toStrictEqual(def1a); + expect(def1a).toStrictEqual(def1b); + expect(def1a).not.toStrictEqual(def2); + }); + + test('can use `expect().toStrictEqual()` to check finished `value`s', () => { + const def1a = new VisitDef(1, 'boop'); + const def1b = new VisitDef(1, 'boop'); + const def2 = new VisitDef(1, 'zonkers'); + const def3 = new VisitDef(1); + + expect(def1a).toStrictEqual(def1a); + expect(def1a).toStrictEqual(def1b); + expect(def1a).not.toStrictEqual(def2); + expect(def1a).not.toStrictEqual(def3); + }); + + test('can use `expect().toStrictEqual()` to check finished `error`s', () => { + const def1a = new VisitDef(1); + const def1b = new VisitDef(1); + const def2 = new VisitDef(1); + const def3 = new VisitDef(1, 'good'); + + const error1 = new Error('oy 1'); + def1a.finishWithError(error1); + def1b.finishWithError(error1); + def2.finishWithError(new Error('oy 2')); + + expect(def1a).toStrictEqual(def1a); + expect(def1a).toStrictEqual(def1b); + expect(def1a).not.toStrictEqual(def2); + expect(def1a).not.toStrictEqual(def3); + }); +}); diff --git a/src/valvis/tests/VisitRef.test.js b/src/valvis/tests/VisitRef.test.js index aa40970df..b1d31423d 100644 --- a/src/valvis/tests/VisitRef.test.js +++ b/src/valvis/tests/VisitRef.test.js @@ -63,3 +63,33 @@ describe('.toJSON()', () => { expect(ref.toJSON()).toStrictEqual({ '@ref': [2] }); }); }); + +// This validates that it's safe to use `expect(ref).toStrictEqual(ref)` +// in test cases throughout the system. +describe('validating Jest usage', () => { + test('can use `expect().toStrictEqual()` to check the defs\' `index`', () => { + const def1a = new VisitDef(1, 'boop'); + const def1b = new VisitDef(1, 'boop'); + const def2 = new VisitDef(2, 'boop'); + const ref1a = new VisitRef(def1a); + const ref1b = new VisitRef(def1b); + const ref2 = new VisitRef(def2); + + expect(ref1a).toStrictEqual(ref1a); + expect(ref1a).toStrictEqual(ref1b); + expect(ref1a).not.toStrictEqual(ref2); + }); + + test('can use `expect().toStrictEqual()` to check the defs\' `value`', () => { + const def1a = new VisitDef(1, 'boop'); + const def1b = new VisitDef(1, 'boop'); + const def2 = new VisitDef(1, 'zonkers'); + const ref1a = new VisitRef(def1a); + const ref1b = new VisitRef(def1b); + const ref2 = new VisitRef(def2); + + expect(ref1a).toStrictEqual(ref1a); + expect(ref1a).toStrictEqual(ref1b); + expect(ref1a).not.toStrictEqual(ref2); + }); +}); From b2a6616f85529231a01013d99e07d578c92b97ac Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 13:22:48 -0800 Subject: [PATCH 15/18] Handle cycles in logged data. --- src/loggy-intf/export/LoggedValueEncoder.js | 28 +++++++++++++------ .../tests/LoggedValueEncoder.test.js | 21 ++++++++++++++ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/loggy-intf/export/LoggedValueEncoder.js b/src/loggy-intf/export/LoggedValueEncoder.js index f8931c6fe..2d91a0dcd 100644 --- a/src/loggy-intf/export/LoggedValueEncoder.js +++ b/src/loggy-intf/export/LoggedValueEncoder.js @@ -33,17 +33,27 @@ export class LoggedValueEncoder extends BaseValueVisitor { } /** @override */ - _impl_shouldRef(value) { - if (typeof value === 'object') { - if (Array.isArray(value)) { - return (value.length > 10); - } else if (AskIf.plainObject(value)) { - return (Object.entries(value).length > 10); - } else { + _impl_shouldRef(value, isCycleHead) { + switch (typeof value) { + case 'function': { return true; } - } else { - return false; + + case 'object': { + if (isCycleHead) { + return true; + } else if (Array.isArray(value)) { + return (value.length > 10); + } else if (AskIf.plainObject(value)) { + return (Object.entries(value).length > 10); + } else { + return true; + } + } + + default: { + return false; + } } } diff --git a/src/loggy-intf/tests/LoggedValueEncoder.test.js b/src/loggy-intf/tests/LoggedValueEncoder.test.js index 6ea000c8a..df5c1035b 100644 --- a/src/loggy-intf/tests/LoggedValueEncoder.test.js +++ b/src/loggy-intf/tests/LoggedValueEncoder.test.js @@ -1,6 +1,8 @@ // Copyright 2022-2024 the Lactoserv Authors (Dan Bornstein et alia). // SPDX-License-Identifier: Apache-2.0 +import { inspect } from 'node:util'; + import { Duration } from '@this/quant'; import { LoggedValueEncoder } from '@this/loggy-intf'; import { Sexp } from '@this/sexp'; @@ -124,4 +126,23 @@ describe('encode()', () => { const got = LoggedValueEncoder.encode([value, value]); expect(got).toStrictEqual(expected); }); + + test('def-refs an array with a self-reference', () => { + const value = [123]; + value.push(value); + + const got = LoggedValueEncoder.encode(value); + + // Jest can't compare self-referential structures (it recurses forever), so + // we have to do it "manually." + + expect(got).toBeInstanceOf(VisitDef); + expect(got.index).toBe(0); + + const gotValue = got.value; + expect(gotValue).toBeArrayOfSize(2); + expect(gotValue[0]).toBe(123); + expect(gotValue[1]).toBeInstanceOf(VisitRef); + expect(gotValue[1].def).toBe(got); + }); }); From 9e6deb264869057b7719b8fe472eea5b1d26daf1 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 13:23:53 -0800 Subject: [PATCH 16/18] Changelog. --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 333229c2a..1accb1eb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,8 @@ Other notable changes: * general: * Allow node version 23. * `loggy-intf` / `loggy`: - * Made several improvements to "human" (non-JSON) log rendering. + * Made several improvements to "human" (non-JSON) log rendering, including + fixing it to be able to log values with reference cycles. * `structy`: * Started allowing any object (plain or not) to be used as the argument to the `BaseStruct` constructor. From 2997dda175697718ff658b2a5cc41e330dd0abb6 Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 13:24:17 -0800 Subject: [PATCH 17/18] Remove unneeded `import`. --- src/loggy-intf/tests/LoggedValueEncoder.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/loggy-intf/tests/LoggedValueEncoder.test.js b/src/loggy-intf/tests/LoggedValueEncoder.test.js index df5c1035b..2138e0c18 100644 --- a/src/loggy-intf/tests/LoggedValueEncoder.test.js +++ b/src/loggy-intf/tests/LoggedValueEncoder.test.js @@ -1,8 +1,6 @@ // Copyright 2022-2024 the Lactoserv Authors (Dan Bornstein et alia). // SPDX-License-Identifier: Apache-2.0 -import { inspect } from 'node:util'; - import { Duration } from '@this/quant'; import { LoggedValueEncoder } from '@this/loggy-intf'; import { Sexp } from '@this/sexp'; From c62150b9b31cd44168acb415de4dbdde3cb51bdd Mon Sep 17 00:00:00 2001 From: Dan Bornstein Date: Thu, 21 Nov 2024 13:24:48 -0800 Subject: [PATCH 18/18] Sort imports. --- src/loggy-intf/export/LogPayload.js | 2 +- src/loggy-intf/tests/LogPayload.test.js | 2 +- src/loggy-intf/tests/LogTag.test.js | 2 +- src/loggy-intf/tests/LoggedValueEncoder.test.js | 3 ++- src/main-tester/index.js | 1 + src/net-util/export/DispatchInfo.js | 2 +- src/valvis/export/BaseDefRef.js | 1 + 7 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/loggy-intf/export/LogPayload.js b/src/loggy-intf/export/LogPayload.js index 421f3a34d..e829ab867 100644 --- a/src/loggy-intf/export/LogPayload.js +++ b/src/loggy-intf/export/LogPayload.js @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import { EventPayload, EventSource } from '@this/async'; -import { IntfDeconstructable, Sexp } from '@this/sexp'; import { Moment } from '@this/quant'; +import { IntfDeconstructable, Sexp } from '@this/sexp'; import { MustBe } from '@this/typey'; import { StackTrace } from '@this/valvis'; diff --git a/src/loggy-intf/tests/LogPayload.test.js b/src/loggy-intf/tests/LogPayload.test.js index b4147ffd7..7a77a6468 100644 --- a/src/loggy-intf/tests/LogPayload.test.js +++ b/src/loggy-intf/tests/LogPayload.test.js @@ -3,9 +3,9 @@ import stripAnsi from 'strip-ansi'; -import { Sexp } from '@this/sexp'; import { LogPayload, LogTag } from '@this/loggy-intf'; import { Moment } from '@this/quant'; +import { Sexp } from '@this/sexp'; import { StackTrace } from '@this/valvis'; diff --git a/src/loggy-intf/tests/LogTag.test.js b/src/loggy-intf/tests/LogTag.test.js index 73231d42b..8c62224d6 100644 --- a/src/loggy-intf/tests/LogTag.test.js +++ b/src/loggy-intf/tests/LogTag.test.js @@ -3,8 +3,8 @@ import stripAnsi from 'strip-ansi'; -import { Sexp } from '@this/sexp'; import { LogTag } from '@this/loggy-intf'; +import { Sexp } from '@this/sexp'; import { StyledText } from '@this/texty'; diff --git a/src/loggy-intf/tests/LoggedValueEncoder.test.js b/src/loggy-intf/tests/LoggedValueEncoder.test.js index 2138e0c18..6b0a05b45 100644 --- a/src/loggy-intf/tests/LoggedValueEncoder.test.js +++ b/src/loggy-intf/tests/LoggedValueEncoder.test.js @@ -1,11 +1,12 @@ // Copyright 2022-2024 the Lactoserv Authors (Dan Bornstein et alia). // SPDX-License-Identifier: Apache-2.0 -import { Duration } from '@this/quant'; import { LoggedValueEncoder } from '@this/loggy-intf'; +import { Duration } from '@this/quant'; import { Sexp } from '@this/sexp'; import { VisitDef, VisitRef } from '@this/valvis'; + function withNullObjectProtos(value) { if (!(value && (typeof value === 'object'))) { return value; diff --git a/src/main-tester/index.js b/src/main-tester/index.js index 691bf70d7..6ee8ed723 100644 --- a/src/main-tester/index.js +++ b/src/main-tester/index.js @@ -3,6 +3,7 @@ import { inspect } from 'node:util'; + process.on('warning', (warning) => { if (warning.name === 'ExperimentalWarning') { if (/VM Modules/.test(warning.message)) { diff --git a/src/net-util/export/DispatchInfo.js b/src/net-util/export/DispatchInfo.js index 8dfc037ce..b2c79fc6c 100644 --- a/src/net-util/export/DispatchInfo.js +++ b/src/net-util/export/DispatchInfo.js @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import { PathKey } from '@this/collections'; -import { IntfDeconstructable, Sexp } from '@this/sexp'; import { IntfLogger } from '@this/loggy-intf'; +import { IntfDeconstructable, Sexp } from '@this/sexp'; import { MustBe } from '@this/typey'; import { IncomingRequest } from '#x/IncomingRequest'; diff --git a/src/valvis/export/BaseDefRef.js b/src/valvis/export/BaseDefRef.js index 7cc8c9cc3..231885041 100644 --- a/src/valvis/export/BaseDefRef.js +++ b/src/valvis/export/BaseDefRef.js @@ -4,6 +4,7 @@ import { IntfDeconstructable } from '@this/sexp'; import { Methods } from '@this/typey'; + /** * Forward declaration of this class, because `import`ing it would cause a * circular dependency while loading.