Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: optimize traversals with a very large number of errors #1172

Merged
merged 5 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion ark/schema/shared/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export class ArkErrors extends ReadonlyArray<ArkError> {
this.ctx = ctx
}

byPath: Record<string, ArkError> = {}
byPath: Record<string, ArkError> = Object.create(null)
count = 0
private mutable: ArkError[] = this as never

Expand Down
12 changes: 9 additions & 3 deletions ark/schema/shared/traversal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
type ArkErrorContextInput,
type ArkErrorInput
} from "./errors.ts"
import { isNode, pathToPropString, type TraversalPath } from "./utils.ts"
import { appendPropToPathString, isNode, type TraversalPath } from "./utils.ts"

export type MorphsAtPath = {
path: TraversalPath
Expand Down Expand Up @@ -149,8 +149,14 @@ export class TraversalContext {
pathHasError(path: TraversalPath): boolean {
if (!this.hasError()) return false

const propString = pathToPropString(path)
return this.errors.some(e => propString.startsWith(e.propString))
let partialPropString: string = ""
// this.errors.byPath is null prototyped so indexing by string is safe
if (this.errors.byPath[partialPropString]) return true
for (let i = 0; i < path.length; i++) {
partialPropString = appendPropToPathString(partialPropString, path[i])
if (this.errors.byPath[partialPropString]) return true
}
return false
}

get failFast(): boolean {
Expand Down
58 changes: 40 additions & 18 deletions ark/schema/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,27 +61,49 @@ export type PathToPropStringFn = <stringifiable>(
: NoInfer<[opts: PathToPropStringOptions<stringifiable>]>
) => string

export const pathToPropString: PathToPropStringFn = (path, ...[opts]) => {
export type AppendPropToPathStringFn = <stringifiable>(
path: string,
prop: stringifiable,
...[opts]: [stringifiable] extends [PropertyKey] ?
[opts?: PathToPropStringOptions]
: NoInfer<[opts: PathToPropStringOptions<stringifiable>]>
) => string

export const appendPropToPathString: AppendPropToPathStringFn = (
path,
prop,
...[opts]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this just be opts without TS complaining? The signature there is really just a TS trick to force opts to be passed in some situations.

Copy link
Contributor Author

@Dimava Dimava Oct 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not without anys
It's possible to make it a function with overloads and loose implementation though

Copy link
Contributor Author

@Dimava Dimava Oct 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I can make it required here, as it's internal anyways

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No that didn't work

) => {
const stringifySymbol = opts?.stringifySymbol ?? printable
const propAccessChain = path.reduce<string>((s, k) => {
switch (typeof k) {
case "string":
return isDotAccessible(k) ? `${s}.${k}` : `${s}[${JSON.stringify(k)}]`
case "number":
return `${s}[${k}]`
case "symbol":
return `${s}[${stringifySymbol(k)}]`
default:
if (opts?.stringifyNonKey)
return `${s}[${opts.stringifyNonKey(k as never)}]`
throwParseError(
`${printable(k)} must be a PropertyKey or stringifyNonKey must be passed to options`
)
}
}, "")
return propAccessChain[0] === "." ? propAccessChain.slice(1) : propAccessChain
let propAccessChain: string = path
switch (typeof prop) {
case "string":
propAccessChain =
isDotAccessible(prop) ?
path === "" ?
prop
: `${path}.${prop}`
: `${path}[${JSON.stringify(prop)}]`
break
case "number":
propAccessChain = `${path}[${prop}]`
break
case "symbol":
propAccessChain = `${path}[${stringifySymbol(prop)}]`
break
default:
if (opts?.stringifyNonKey)
propAccessChain = `${path}[${opts.stringifyNonKey(prop as never)}]`
throwParseError(
`${printable(prop)} must be a PropertyKey or stringifyNonKey must be passed to options`
)
}
return propAccessChain
}

export const pathToPropString: PathToPropStringFn = (path, ...opts) =>
path.reduce<string>((s, k) => appendPropToPathString(s, k, ...opts), "")

export type arkKind = typeof arkKind

export const arkKind = noSuggest("arkKind")
Expand Down