Files
headroom/frontend/node_modules/@ark/schema/out/shared/traversal.js
Santhosh Janardhanan de2d83092e feat: Reinitialize frontend with SvelteKit and TypeScript
- Delete old Vite+Svelte frontend
- Initialize new SvelteKit project with TypeScript
- Configure Tailwind CSS v4 + DaisyUI
- Implement JWT authentication with auto-refresh
- Create login page with form validation (Zod)
- Add protected route guards
- Update Docker configuration for single-stage build
- Add E2E tests with Playwright (6/11 passing)
- Fix Svelte 5 reactivity with $state() runes

Known issues:
- 5 E2E tests failing (timing/async issues)
- Token refresh implementation needs debugging
- Validation error display timing
2026-02-17 16:19:59 -05:00

227 lines
7.3 KiB
JavaScript

import { ReadonlyPath, stringifyPath } from "@ark/util";
import { ArkError, ArkErrors } from "./errors.js";
import { isNode } from "./utils.js";
export class Traversal {
/**
* #### the path being validated or morphed
*
* ✅ array indices represented as numbers
* ⚠️ mutated during traversal - use `path.slice(0)` to snapshot
* 🔗 use {@link propString} for a stringified version
*/
path = [];
/**
* #### {@link ArkErrors} that will be part of this traversal's finalized result
*
* ✅ will always be an empty array for a valid traversal
*/
errors = new ArkErrors(this);
/**
* #### the original value being traversed
*/
root;
/**
* #### configuration for this traversal
*
* ✅ options can affect traversal results and error messages
* ✅ defaults < global config < scope config
* ✅ does not include options configured on individual types
*/
config;
queuedMorphs = [];
branches = [];
seen = {};
constructor(root, config) {
this.root = root;
this.config = config;
}
/**
* #### the data being validated or morphed
*
* ✅ extracted from {@link root} at {@link path}
*/
get data() {
let result = this.root;
for (const segment of this.path)
result = result?.[segment];
return result;
}
/**
* #### a string representing {@link path}
*
* @propString
*/
get propString() {
return stringifyPath(this.path);
}
/**
* #### add an {@link ArkError} and return `false`
*
* ✅ useful for predicates like `.narrow`
*/
reject(input) {
this.error(input);
return false;
}
/**
* #### add an {@link ArkError} from a description and return `false`
*
* ✅ useful for predicates like `.narrow`
* 🔗 equivalent to {@link reject}({ expected })
*/
mustBe(expected) {
this.error(expected);
return false;
}
error(input) {
const errCtx = typeof input === "object" ?
input.code ?
input
: { ...input, code: "predicate" }
: { code: "predicate", expected: input };
return this.errorFromContext(errCtx);
}
/**
* #### whether {@link currentBranch} (or the traversal root, outside a union) has one or more errors
*/
hasError() {
return this.currentErrorCount !== 0;
}
get currentBranch() {
return this.branches[this.branches.length - 1];
}
queueMorphs(morphs) {
const input = {
path: new ReadonlyPath(...this.path),
morphs
};
if (this.currentBranch)
this.currentBranch.queuedMorphs.push(input);
else
this.queuedMorphs.push(input);
}
finalize(onFail) {
if (this.queuedMorphs.length) {
if (typeof this.root === "object" &&
this.root !== null &&
this.config.clone)
this.root = this.config.clone(this.root);
this.applyQueuedMorphs();
}
if (this.hasError())
return onFail ? onFail(this.errors) : this.errors;
return this.root;
}
get currentErrorCount() {
return (this.currentBranch ?
this.currentBranch.error ?
1
: 0
: this.errors.count);
}
get failFast() {
return this.branches.length !== 0;
}
pushBranch() {
this.branches.push({
error: undefined,
queuedMorphs: []
});
}
popBranch() {
return this.branches.pop();
}
/**
* @internal
* Convenience for casting from InternalTraversal to Traversal
* for cases where the extra methods on the external type are expected, e.g.
* a morph or predicate.
*/
get external() {
return this;
}
errorFromNodeContext(input) {
return this.errorFromContext(input);
}
errorFromContext(errCtx) {
const error = new ArkError(errCtx, this);
if (this.currentBranch)
this.currentBranch.error = error;
else
this.errors.add(error);
return error;
}
applyQueuedMorphs() {
// invoking morphs that are Nodes will reuse this context, potentially
// adding additional morphs, so we have to continue looping until
// queuedMorphs is empty rather than iterating over the list once
while (this.queuedMorphs.length) {
const queuedMorphs = this.queuedMorphs;
this.queuedMorphs = [];
for (const { path, morphs } of queuedMorphs) {
// even if we already have an error, apply morphs that are not at a path
// with errors to capture potential validation errors
if (this.errors.affectsPath(path))
continue;
this.applyMorphsAtPath(path, morphs);
}
}
}
applyMorphsAtPath(path, morphs) {
const key = path[path.length - 1];
let parent;
if (key !== undefined) {
// find the object on which the key to be morphed exists
parent = this.root;
for (let pathIndex = 0; pathIndex < path.length - 1; pathIndex++)
parent = parent[path[pathIndex]];
}
for (const morph of morphs) {
// ensure morphs are applied relative to the correct path
// in case previous operations modified this.path
this.path = [...path];
const morphIsNode = isNode(morph);
const result = morph((parent === undefined ? this.root : parent[key]), this);
if (result instanceof ArkError) {
// if an ArkError was returned, ensure it has been added to errors
// (it may have already been added via ctx.error() within the morph)
// Only add if it's not already in the errors collection
if (!this.errors.includes(result))
this.errors.add(result);
// skip any remaining morphs at the current path
break;
}
if (result instanceof ArkErrors) {
// if the morph was a direct reference to another node,
// errors will have been added directly via this piped context
if (!morphIsNode) {
// otherwise, we have to ensure each error has been added
this.errors.merge(result);
}
// skip any remaining morphs at the current path
this.queuedMorphs = [];
break;
}
// if the morph was successful, assign the result to the
// corresponding property, or to root if path is empty
if (parent === undefined)
this.root = result;
else
parent[key] = result;
// if the current morph queued additional morphs,
// applying them before subsequent morphs
this.applyQueuedMorphs();
}
}
}
export const traverseKey = (key, fn,
// ctx will be undefined if this node isn't context-dependent
ctx) => {
if (!ctx)
return fn();
ctx.path.push(key);
const result = fn();
ctx.path.pop();
return result;
};