Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
24dc40d
Prepare to rewrite in modern TypeScript
novemberborn Feb 20, 2025
037d59e
Reimplement deep comparison and (de)serialization
novemberborn Feb 21, 2025
b219c6d
Test BoxedPrimiteRepresentation
novemberborn Apr 13, 2025
3df9a40
Pack annotations to enable compression optimizations
novemberborn Apr 13, 2025
fc2094c
fixup! Prepare to rewrite in modern TypeScript
novemberborn Apr 15, 2025
d945880
Test DateRepresentation
novemberborn Apr 15, 2025
27699be
Test ErrorRepresentation
novemberborn Apr 15, 2025
7f54a24
Test FunctionRepresentation
novemberborn Apr 15, 2025
8765028
Test MapRepresentation
novemberborn May 2, 2025
fa277d4
Fix and test ModuleNamespaceObjectRepresentation
novemberborn May 2, 2025
cc5d9ca
fixup! Reimplement deep comparison and (de)serialization
novemberborn May 2, 2025
47b432c
Test PromiseRepresentation
novemberborn May 3, 2025
bb2ad5f
More efficient serialization of string tags that are the same as the …
novemberborn May 3, 2025
88872d3
Test RegExpRepresentation
novemberborn May 3, 2025
a0142ed
fixup! Reimplement deep comparison and (de)serialization
novemberborn May 3, 2025
e200dff
Test SetRepresentation
novemberborn May 3, 2025
e114ae5
Test WeakMapRepresentation
novemberborn May 3, 2025
c3a8e17
Test WeakSetRepresentation
novemberborn May 3, 2025
bd46a0e
Test ExternalRepresentation; improve deserialized comparisons
novemberborn May 5, 2025
aae0641
Further optimize serialization of string tags that are the same as co…
novemberborn May 5, 2025
7380d51
Test CryptoKeyRepresentation
novemberborn May 5, 2025
375af03
Test BytesAccessor
novemberborn May 5, 2025
9e29018
Test ElementAccessor and SparseValueRepresentation
novemberborn May 5, 2025
f2afba0
Test IteratorValueAccessor
novemberborn May 5, 2025
bd9d358
Test MapEntryAccessor
novemberborn May 5, 2025
79fc20e
Test property accessors
novemberborn May 6, 2025
1887afd
Refactor compare() tests focusing on algorithmic accuracy not behavio…
novemberborn May 6, 2025
025055e
Test describe()
novemberborn May 6, 2025
2a65532
Test serialization types
novemberborn May 7, 2025
7f75486
Test serialization
novemberborn May 7, 2025
0a7cdd9
Test deserialization
novemberborn May 16, 2025
78437fb
c8: Exclude *.d.ts files from report
novemberborn May 18, 2025
8d15d93
Split DescriptionContext and isPrimitive/representPrimitive into sepa…
novemberborn May 18, 2025
1377318
Split Encoder into its own file
novemberborn May 19, 2025
45b521d
Use cbor2 directly
novemberborn May 19, 2025
9fee1b3
Target Node.js 24 and update related dependencies & configuration
novemberborn May 19, 2025
8c88547
Support integers larger than int64
novemberborn May 19, 2025
9cf3312
Encode / decode non-wellformed strings
novemberborn May 19, 2025
0fe1c0a
Implement flags to control behavior
novemberborn May 19, 2025
1c444a5
Remove exports test file
novemberborn May 19, 2025
90d0471
Make comparing objects with null prototypes to regular object prototy…
novemberborn May 21, 2025
6526af5
Stop using interfaces
novemberborn May 21, 2025
e07ae2d
Add missing tests for flags getter in context implementations
novemberborn Jun 3, 2025
aa3c0c8
Treat empty constructor names as such
novemberborn May 24, 2025
6f25269
Refactor so property groups are not value representations
novemberborn May 31, 2025
bfb271a
Change BytesAccessor to be shallow
novemberborn May 29, 2025
a1e5fc2
Implement formatting for accessors and values
novemberborn May 21, 2025
d5e2c79
Upgrade XO
novemberborn Jun 9, 2025
fc40ba8
Update dependencies
novemberborn Jun 9, 2025
0d1d49d
Rename concepts
novemberborn Jun 9, 2025
1b2dc8a
Add method to fully (recursively) deserialize a representation
novemberborn Jun 9, 2025
aa46eb4
Change stack iteration to return undefined when done
novemberborn Jun 10, 2025
41ab11d
Simplify symbol property serialization
novemberborn Jun 10, 2025
a569ba5
Move iterator out of stack entry into internal state
novemberborn Jun 15, 2025
f206ec5
Implement Stack#peekNext() to access the next iterated value
novemberborn Jun 15, 2025
a6445fc
Implement Stack#takeWhile() to iterate over values while a condition …
novemberborn Jun 15, 2025
9a97460
Group accessors depending on applied algorithm, not in the context
novemberborn Jun 15, 2025
f2d0c84
Improve comparison logic for Error, RegExp, WeakMap, WeakSet and Cryp…
novemberborn Jun 16, 2025
19d124d
Exclude Error#stack from the iterated properties
novemberborn Jun 18, 2025
6f85dc6
Always include Error#code
novemberborn Jun 18, 2025
94aa64d
Implement fuzzy comparison mode
novemberborn Jun 16, 2025
79570f6
Refactor array-like handling
novemberborn Aug 17, 2025
00e5da1
Clean up unnecessary static is() from mocks
novemberborn Aug 17, 2025
05c8cc5
Give RHS control over being compared
novemberborn Aug 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Implement Stack#takeWhile() to iterate over values while a condition …
…holds

(And the stack isn't modified.)
  • Loading branch information
novemberborn committed Jun 24, 2025
commit a6445fc1215a9e42d4d8dab655b5531ad2d21bf1
22 changes: 22 additions & 0 deletions src/stack.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from 'node:assert'
import never from 'never'
import type { ValueRepresentation } from './value.d.ts'
import { fullyDeserialize } from './deserialize.ts'

type OptionalFields = Partial<Record<string, unknown>>

Expand Down Expand Up @@ -95,4 +96,25 @@ export class Stack<Fields extends OptionalFields = OptionalFields> {
internal.nextValueRepresentation = next.value
return next.value
}

*#takeWhile<T extends ValueRepresentation>(
expectedTop: StackEntry<Fields>,
condition: (value: ValueRepresentation) => value is T,
): IterableIterator<T> {
while (this.top === expectedTop) {
const nextValue = this.peekNext()
if (nextValue === undefined || !condition(nextValue)) return
this.iterateNext()
yield fullyDeserialize(nextValue)
}
}

get takeWhile(): TakeWhile {
const { top: expectedTop = never('Stack is empty') } = this
return this.#takeWhile.bind(this, expectedTop) as TakeWhile
}
}

export type TakeWhile = <T extends ValueRepresentation>(
condition: (value: ValueRepresentation) => value is T,
) => IterableIterator<T>
141 changes: 141 additions & 0 deletions src/test/stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import test from 'ava'
import { Stack } from '../stack.ts'
import { ObjectRepresentation } from '../values/objects/object.ts'
import { RealValueContext } from '../real-value-context.ts'
import type { ValueRepresentation } from '../value.d.ts'
import { Encoder } from '../encoder.ts'
import { staticTypeTable } from '../serialization-types.ts'
import { Decoder } from '../decoder.ts'
import { DeserializationContext } from '../deserialization-context.ts'
import { NamedPropertyAccessor } from '../accessors/property.ts'

const representation = new ObjectRepresentation(new RealValueContext(), {})

Expand Down Expand Up @@ -142,3 +148,138 @@ test('peekNext and iterateNext interaction', (t) => {
t.is(stack.iterateNext(), undefined)
t.is(stack.peekNext(), undefined)
})

test('takeWhile throws when stack is empty', (t) => {
const stack = new Stack()
t.throws(
() => {
stack.takeWhile((value): value is ObjectRepresentation => value instanceof ObjectRepresentation)
},
{ message: 'Stack is empty' },
)
})

test('takeWhile yields values while condition is true', (t) => {
const stack = new Stack()

// Create an object with multiple properties to iterate over
const testObject = { a: 1, b: 2, c: 3 }
const objectRepresentation = new ObjectRepresentation(new RealValueContext(), testObject)
stack.push(objectRepresentation)

// Take the first 2 values (should be property representations)
let count = 0
const condition = (_value: ValueRepresentation): _value is ValueRepresentation => {
count++
return count <= 2
}

const iterator = stack.takeWhile(condition)
const results = [...iterator]

t.is(results.length, 2)
t.truthy(results[0])
t.truthy(results[1])

// There should be one more value available
const remaining = stack.iterateNext()
t.truthy(remaining)

// And then it should be exhausted
t.is(stack.iterateNext(), undefined)
})

test('takeWhile returns empty iterator when condition is false immediately', (t) => {
const stack = new Stack()

const testObject = { a: 1, b: 2 }
const objectRepresentation = new ObjectRepresentation(new RealValueContext(), testObject)
stack.push(objectRepresentation)

// Condition that never matches
const condition = (_value: ValueRepresentation): _value is never => false
const iterator = stack.takeWhile(condition)

const results = [...iterator]
t.is(results.length, 0)

// The first value should still be available
const first = stack.peekNext()
t.truthy(first)
})

test('takeWhile stops when stack top changes', (t) => {
const stack = new Stack()

const testObject = { a: 1, b: 2 }
const objectRepresentation = new ObjectRepresentation(new RealValueContext(), testObject)
stack.push(objectRepresentation)

const condition = (_value: ValueRepresentation): _value is ValueRepresentation => true
const iterator = stack.takeWhile(condition)

// Get first value
const firstResult = iterator.next()
t.false(firstResult.done)
t.truthy(firstResult.value)

// Change the stack by popping
stack.pop()

// Iterator should now be done even though condition would match
const secondResult = iterator.next()
t.true(secondResult.done)
})

test('takeWhile fully deserializes yielded values', (t) => {
const encoder = new Encoder()

// First value: Complex nested object that requires full deserialization
encoder
.staticType(staticTypeTable.object)
.annotations({ p: 1 })
// Named property: 'foo'
.staticType(staticTypeTable.namedPropertyAspect)
.string('foo')
// Value: Simple string
.staticType(staticTypeTable.string)
.string('bar')
// Named property: 'nested'
.staticType(staticTypeTable.namedPropertyAspect)
.string('nested')
// Value: Another object
.staticType(staticTypeTable.object)
.annotations({ p: 2 })
// Named property: 'items'
.staticType(staticTypeTable.namedPropertyAspect)
.string('items')
// Value: Array with multiple elements
.staticType(staticTypeTable.array)
.annotations({ p: 3 })
// Element 0
.staticType(staticTypeTable.elementAspect)
.staticType(staticTypeTable.string)
.string('first')
// Element 1
.staticType(staticTypeTable.elementAspect)
.staticType(staticTypeTable.string)
.string('second')
.staticType(staticTypeTable.terminator) // End array
.staticType(staticTypeTable.terminator) // End nested object
.staticType(staticTypeTable.terminator) // End root object

const decoder = new Decoder(encoder.bytes)
const context = new DeserializationContext(decoder)

const representation = context.next()!
const stack = new Stack()
stack.push(representation)
const condition = (value: ValueRepresentation): value is NamedPropertyAccessor => {
return NamedPropertyAccessor.is(value)
}

// Expect only two properties if takeWhile() fully deserialized each yielded value. If not, we should see three,
// since the 'items' property would be attributed to the root object instead of the nested object.
const properties = [...stack.takeWhile(condition)]
t.is(properties.length, 2) // 'foo' and 'nested'
})