nyaaorick's picture
feat: publish everything-webgpu package, engine source and documentation
1944112 verified
Raw
History Blame Contribute Delete
3.86 kB
/**
* Typed failures.
*
* Every error the engine raised used to be a bare string, so a caller wanting
* to tell "this device has no WebGPU" from "that folder was missing a shard"
* had to match on prose β€” which then silently broke whenever the prose
* improved. A code is the part of an error message that is allowed to be
* depended on.
*
* The codes are deliberately few. Each one exists because a caller does
* something *different* about it, not because it names a different line of
* code:
*
* NO_WEBGPU tell the user to check flags/hardware; retrying is futile
* NO_MODEL nothing registered at all β€” send them to your setup flow
* UNKNOWN_MODEL that id is not resolvable; `listAvailableModels()` says what is
* CACHE_INCOMPLETE a locally-registered model was evicted; re-register the folder
* INVALID_MODEL_FOLDER the folder is not a compiled MLC model; `detail` says what is missing
* BAD_REQUEST the caller's arguments are wrong; a bug in the caller
* ABORTED the caller cancelled it; not a failure, and not to be
* reported to a user as one
* GENERATION_FAILED the model failed mid-generation
* PACKAGE_INCOMPLETE this package is not wired into your build correctly β€”
* a missing `vendor/` bundle, or a decode worker the
* bundler did not emit. Fixed once, in the build, by
* the developer; never branched on at runtime.
*
* `PACKAGE_INCOMPLETE` covers two causes with one code on purpose. They are
* different sentences and `detail.cause` tells them apart, but no caller writes
* a different `catch` branch for them: both mean "your build is wrong, the app
* has not shipped yet", and both are fixed by editing config, not by handling
* an error path. A second code would have grown the table a caller reads
* without giving that caller anything new to do.
*
* `message` stays human-readable and stays the thing you print. `detail`
* carries whatever structured context the site had β€” the missing cache keys,
* the absent field β€” so a caller never has to parse the sentence.
*/
export const ERROR = {
NO_WEBGPU: "NO_WEBGPU",
NO_MODEL: "NO_MODEL",
UNKNOWN_MODEL: "UNKNOWN_MODEL",
CACHE_INCOMPLETE: "CACHE_INCOMPLETE",
INVALID_MODEL_FOLDER: "INVALID_MODEL_FOLDER",
BAD_REQUEST: "BAD_REQUEST",
ABORTED: "ABORTED",
GENERATION_FAILED: "GENERATION_FAILED",
PACKAGE_INCOMPLETE: "PACKAGE_INCOMPLETE",
};
export class EngineError extends Error {
/**
* @param {string} code one of ERROR
* @param {string} message human-readable, safe to show a user
* @param {object} [detail] structured context, so callers need not parse `message`
*/
constructor(code, message, detail) {
super(message);
this.name = "EngineError";
this.code = code;
if (detail !== undefined) this.detail = detail;
}
/** Wire form. `error` stays a plain string so existing callers keep working. */
toJSON() {
return { code: this.code, message: this.message, ...(this.detail ? { detail: this.detail } : {}) };
}
}
/**
* Normalise anything thrown into an EngineError.
*
* Errors from WebLLM, the GPU stack and the structured-clone boundary arrive as
* plain Errors, DOMExceptions or strings, and a caller should not have to care
* which. Anything unrecognised becomes `GENERATION_FAILED` rather than being
* given a more specific code it has not earned.
*/
export function asEngineError(err, fallback = ERROR.GENERATION_FAILED) {
if (err instanceof EngineError) return err;
const message = String(err?.message ?? err);
return new EngineError(fallback, message);
}
export const isEngineError = (err, code) =>
err instanceof EngineError && (code === undefined || err.code === code);