Skip to content
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
7 changes: 7 additions & 0 deletions ark/schema/__tests__/scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,11 @@ contextualize(() => {
"a.b.a.b must be an object (was a string)"
)
})

it("allows multiple scopes with the same name without collision", () => {
const s1 = schemaScope({ a: { domain: "string" } }, { name: "Array" })
const s2 = schemaScope({ b: { domain: "number" } }, { name: "Array" })
attest(s1.name).equals("Array")
attest(s2.name).equals("Array")
})
})
10 changes: 4 additions & 6 deletions ark/schema/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ export type writeDuplicateAliasError<alias extends string> =

export type AliasDefEntry = [name: string, defValue: unknown]

const scopesByName: Record<string, BaseScope | undefined> = {}
// Fallback counter for anonymous scopes; scopes are identified by instance
// identity rather than names to support HMR and multi-bundle environments.
let anonymousScopeCount = 0

export type GlobalOnlyConfigOptionName = satisfy<
keyof ArkSchemaConfig,
Expand Down Expand Up @@ -274,11 +276,7 @@ export abstract class BaseScope<$ extends {} = {}> {
this.resolvedConfig = mergeConfigs($ark.resolvedConfig, config)

this.name =
this.resolvedConfig.name ??
`anonymousScope${Object.keys(scopesByName).length}`
if (this.name in scopesByName)
throwParseError(`A Scope already named ${this.name} already exists`)
scopesByName[this.name] = this
this.resolvedConfig.name ?? `anonymousScope${anonymousScopeCount++}`
Comment thread
yamcodes marked this conversation as resolved.

const aliasEntries = Object.entries(def).map(entry =>
this.preparseOwnAliasEntry(...entry)
Expand Down
13 changes: 13 additions & 0 deletions ark/type/__tests__/scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,4 +527,17 @@ b.c.c must be an object (was missing)`)
attest(types.foo.json).snap({ domain: "string" })
attest(types.bar.json).snap({ domain: "number" })
})

it("allows multiple scopes with the same name without collision", () => {
const s1 = scope({ a: "string" }, { name: "Array" })
const s2 = scope({ a: "number" }, { name: "Array" })
attest((s1 as any).name).equals("Array")
attest((s2 as any).name).equals("Array")

// Verify parseCache does not cross-pollinate across scopes with the same name
const t1 = s1.type("a")
const t2 = s2.type("a")
attest(t1.expression).equals("string")
attest(t2.expression).equals("number")
})
})
11 changes: 7 additions & 4 deletions ark/type/parser/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
hasArkKind,
type BaseParseContext,
type BaseRoot,
type BaseScope,
type StandardSchemaV1
} from "@ark/schema"
import {
Expand Down Expand Up @@ -52,9 +53,7 @@ import {
type validateTupleLiteral
} from "./tupleLiteral.ts"

const parseCache: {
[cacheId: string]: { [def: string]: InnerParseResult } | undefined
} = {}
const parseCache = new WeakMap<BaseScope, Record<string, InnerParseResult>>()

export const parseInnerDefinition = (
def: unknown,
Expand All @@ -66,7 +65,11 @@ export const parseInnerDefinition = (
// resolutions like "this" or generic args
return parseString(def, ctx)
}
const scopeCache = (parseCache[ctx.$.name] ??= {})
let scopeCache = parseCache.get(ctx.$)
if (!scopeCache) {
scopeCache = {}
parseCache.set(ctx.$, scopeCache)
}
return (scopeCache[def] ??= parseString(def, ctx))
}
return hasDomain(def, "object") ?
Expand Down
Loading