tempest-react-sdk 0.81.0 → 0.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -10
- package/bin/create-tempest-app.e2e.test.mjs +31 -0
- package/bin/create-tempest-app.mjs +11 -1
- package/bin/lib/doctor/doctor.e2e.test.mjs +29 -0
- package/bin/lib/doctor/vitest.mjs +120 -0
- package/bin/lib/doctor/vitest.test.mjs +139 -0
- package/bin/lib/fs/contain.mjs +7 -0
- package/bin/lib/openapi/escape.mjs +68 -4
- package/bin/lib/openapi/generate.hostile.test.mjs +228 -0
- package/bin/lib/openapi/generate.mjs +119 -15
- package/bin/lib/openapi/pattern-risk.mjs +134 -0
- package/bin/lib/openapi/pattern-risk.test.mjs +45 -0
- package/bin/tempest-gen.test.mjs +100 -0
- package/bin/tempest.mjs +24 -8
- package/dist/access/use-can.cjs +1 -1
- package/dist/access/use-can.cjs.map +1 -1
- package/dist/access/use-can.js +25 -28
- package/dist/access/use-can.js.map +1 -1
- package/dist/auth/create-tempest-auth.cjs +1 -1
- package/dist/auth/create-tempest-auth.cjs.map +1 -1
- package/dist/auth/create-tempest-auth.js +189 -143
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/auth/session-identity.cjs +2 -0
- package/dist/auth/session-identity.cjs.map +1 -0
- package/dist/auth/session-identity.js +12 -0
- package/dist/auth/session-identity.js.map +1 -0
- package/dist/br/boleto.cjs.map +1 -1
- package/dist/br/boleto.js.map +1 -1
- package/dist/components/AuthForm/AuthLinks.cjs +1 -1
- package/dist/components/AuthForm/AuthLinks.cjs.map +1 -1
- package/dist/components/AuthForm/AuthLinks.js +18 -15
- package/dist/components/AuthForm/AuthLinks.js.map +1 -1
- package/dist/components/AuthPage/AuthPage.cjs +1 -1
- package/dist/components/AuthPage/AuthPage.cjs.map +1 -1
- package/dist/components/AuthPage/AuthPage.js +58 -52
- package/dist/components/AuthPage/AuthPage.js.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.cjs +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.cjs.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.js +22 -21
- package/dist/components/Breadcrumbs/Breadcrumbs.js.map +1 -1
- package/dist/components/Button/Button.cjs +1 -1
- package/dist/components/Button/Button.cjs.map +1 -1
- package/dist/components/Button/Button.js +41 -40
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Chat/ChatAttachments.cjs +1 -1
- package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
- package/dist/components/Chat/ChatAttachments.js +38 -38
- package/dist/components/Form/Form.cjs +1 -1
- package/dist/components/Form/Form.cjs.map +1 -1
- package/dist/components/Form/Form.js +30 -32
- package/dist/components/Form/Form.js.map +1 -1
- package/dist/components/Layout/Layout.cjs +1 -1
- package/dist/components/Layout/Layout.cjs.map +1 -1
- package/dist/components/Layout/Layout.js +25 -24
- package/dist/components/Layout/Layout.js.map +1 -1
- package/dist/components/Layout/gap.cjs +2 -0
- package/dist/components/Layout/gap.cjs.map +1 -0
- package/dist/components/Layout/gap.js +15 -0
- package/dist/components/Layout/gap.js.map +1 -0
- package/dist/components/NavigationMenu/NavigationMenu.cjs +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.js +71 -70
- package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.js.map +1 -1
- package/dist/components/Sidebar/SidebarEntry.cjs +1 -1
- package/dist/components/Sidebar/SidebarEntry.cjs.map +1 -1
- package/dist/components/Sidebar/SidebarEntry.js +30 -29
- package/dist/components/Sidebar/SidebarEntry.js.map +1 -1
- package/dist/data/create-data-provider.cjs +1 -1
- package/dist/data/create-data-provider.cjs.map +1 -1
- package/dist/data/create-data-provider.js +1 -2
- package/dist/data/create-data-provider.js.map +1 -1
- package/dist/forms/validate-form.cjs +1 -1
- package/dist/forms/validate-form.cjs.map +1 -1
- package/dist/forms/validate-form.js +6 -1
- package/dist/forms/validate-form.js.map +1 -1
- package/dist/forms/zod-resolver.cjs +1 -1
- package/dist/forms/zod-resolver.cjs.map +1 -1
- package/dist/forms/zod-resolver.js +8 -3
- package/dist/forms/zod-resolver.js.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +90 -72
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/credential-scope.cjs +1 -1
- package/dist/http/credential-scope.cjs.map +1 -1
- package/dist/http/credential-scope.js +33 -9
- package/dist/http/credential-scope.js.map +1 -1
- package/dist/http/presigned-post.cjs.map +1 -1
- package/dist/http/presigned-post.js.map +1 -1
- package/dist/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js +167 -112
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/upload-with-progress.cjs +1 -1
- package/dist/http/upload-with-progress.cjs.map +1 -1
- package/dist/http/upload-with-progress.js +100 -44
- package/dist/http/upload-with-progress.js.map +1 -1
- package/dist/oauth/GoogleSignIn.cjs +1 -1
- package/dist/oauth/GoogleSignIn.cjs.map +1 -1
- package/dist/oauth/GoogleSignIn.js +5 -4
- package/dist/oauth/GoogleSignIn.js.map +1 -1
- package/dist/offline/create-offline-sync.cjs +1 -1
- package/dist/offline/create-offline-sync.cjs.map +1 -1
- package/dist/offline/create-offline-sync.js +87 -69
- package/dist/offline/create-offline-sync.js.map +1 -1
- package/dist/query/offline-persistence.cjs +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/offline-persistence.js +67 -44
- package/dist/query/offline-persistence.js.map +1 -1
- package/dist/sse/create-event-stream.cjs +1 -1
- package/dist/sse/create-event-stream.cjs.map +1 -1
- package/dist/sse/create-event-stream.js +48 -41
- package/dist/sse/create-event-stream.js.map +1 -1
- package/dist/sse/ticket-event-stream.cjs +1 -1
- package/dist/sse/ticket-event-stream.cjs.map +1 -1
- package/dist/sse/ticket-event-stream.js +48 -19
- package/dist/sse/ticket-event-stream.js.map +1 -1
- package/dist/sse/use-event-stream.cjs +1 -1
- package/dist/sse/use-event-stream.cjs.map +1 -1
- package/dist/sse/use-event-stream.js +19 -14
- package/dist/sse/use-event-stream.js.map +1 -1
- package/dist/sw/background-sync.cjs +1 -1
- package/dist/sw/background-sync.cjs.map +1 -1
- package/dist/sw/background-sync.js +10 -8
- package/dist/sw/background-sync.js.map +1 -1
- package/dist/sw/cache.cjs +1 -1
- package/dist/sw/cache.cjs.map +1 -1
- package/dist/sw/cache.js +111 -55
- package/dist/sw/cache.js.map +1 -1
- package/dist/sw/create-push-handler.cjs +1 -1
- package/dist/sw/create-push-handler.cjs.map +1 -1
- package/dist/sw/create-push-handler.js +20 -10
- package/dist/sw/create-push-handler.js.map +1 -1
- package/dist/sw.d.ts +65 -14
- package/dist/tabular/compact.cjs +1 -1
- package/dist/tabular/compact.cjs.map +1 -1
- package/dist/tabular/compact.js +35 -16
- package/dist/tabular/compact.js.map +1 -1
- package/dist/tabular/manifest.cjs +1 -1
- package/dist/tabular/manifest.cjs.map +1 -1
- package/dist/tabular/manifest.js +33 -15
- package/dist/tabular/manifest.js.map +1 -1
- package/dist/tabular.d.ts +11 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +543 -70
- package/dist/tempest-react-sdk.js +224 -224
- package/dist/theme/initial-theme.cjs +4 -4
- package/dist/theme/initial-theme.cjs.map +1 -1
- package/dist/theme/initial-theme.js +8 -5
- package/dist/theme/initial-theme.js.map +1 -1
- package/dist/utils/compressed-storage.cjs +1 -1
- package/dist/utils/compressed-storage.cjs.map +1 -1
- package/dist/utils/compressed-storage.js +13 -7
- package/dist/utils/compressed-storage.js.map +1 -1
- package/dist/utils/objects.cjs +1 -1
- package/dist/utils/objects.cjs.map +1 -1
- package/dist/utils/objects.js +13 -7
- package/dist/utils/objects.js.map +1 -1
- package/dist/utils/safe-url.cjs +1 -1
- package/dist/utils/safe-url.cjs.map +1 -1
- package/dist/utils/safe-url.js +4 -1
- package/dist/utils/safe-url.js.map +1 -1
- package/dist/vite/base-url.cjs +1 -1
- package/dist/vite/base-url.cjs.map +1 -1
- package/dist/vite/base-url.js +5 -1
- package/dist/vite/base-url.js.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.js +96 -31
- package/dist/vite/tempest-pwa-dev-sw.js.map +1 -1
- package/dist/vite/tempest-pwa-manifest.cjs +2 -2
- package/dist/vite/tempest-pwa-manifest.cjs.map +1 -1
- package/dist/vite/tempest-pwa-manifest.js +32 -16
- package/dist/vite/tempest-pwa-manifest.js.map +1 -1
- package/dist/vite.cjs +1 -1
- package/dist/vite.d.ts +63 -12
- package/dist/vite.js +4 -4
- package/dist/webrtc/peer-mesh.cjs +1 -1
- package/dist/webrtc/peer-mesh.cjs.map +1 -1
- package/dist/webrtc/peer-mesh.js +7 -1
- package/dist/webrtc/peer-mesh.js.map +1 -1
- package/dist/ws/create-web-socket.cjs +1 -1
- package/dist/ws/create-web-socket.cjs.map +1 -1
- package/dist/ws/create-web-socket.js +77 -61
- package/dist/ws/create-web-socket.js.map +1 -1
- package/dist/ws/use-web-socket.cjs +1 -1
- package/dist/ws/use-web-socket.cjs.map +1 -1
- package/dist/ws/use-web-socket.js +4 -3
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +1 -1
- package/template-pwa/src/pwa.tsx +42 -0
- package/template-pwa/src/sw.ts +5 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-push-handler.js","names":["getSwScope","FORWARDED_OPTIONS","buildNotificationOptions","options","payload","defaultIcon","defaultBadge","key","data","actionUrls","action","url","installPushHandler","sw","event","raw","defaultTitle","transform","title","notification","defaultResolveUrl","record","actionUrl","clientShowsTarget","target","clientUrl","installNotificationClickHandler","resolveUrl","clients","client","installSkipWaitingListener"],"sources":["../../src/sw/create-push-handler.ts"],"sourcesContent":["/**\n * Service-worker context helpers for handling `push` and `notificationclick`\n * events. Import these inside your own `sw.ts` — they expect to run in the\n * service-worker global scope, not in the main thread.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installPushHandler, installNotificationClickHandler } from \"tempest-react-sdk\";\n *\n * installPushHandler({ defaultIcon: \"/icons/Logo.png\" });\n * installNotificationClickHandler();\n */\n\ninterface SwGlobal {\n registration: {\n showNotification(title: string, options?: NotificationOptions): Promise<void>;\n };\n clients: {\n matchAll(options: { type: \"window\"; includeUncontrolled?: boolean }): Promise<\n {\n url: string;\n focused: boolean;\n focus(): Promise<unknown>;\n navigate(url: string): Promise<unknown>;\n }[]\n >;\n openWindow(url: string): Promise<unknown>;\n };\n addEventListener(\n type: \"push\",\n listener: (event: {\n data: { json(): unknown; text(): string } | null;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n addEventListener(\n type: \"notificationclick\",\n listener: (event: {\n notification: { close(): void; data?: unknown };\n action?: string;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n skipWaiting(): Promise<void>;\n}\n\nfunction getSwScope(): SwGlobal {\n return globalThis as unknown as SwGlobal;\n}\n\n/**\n * A notification button as the push payload describes it.\n *\n * `action`, `title` and `icon` are forwarded to the browser as a\n * `NotificationAction`. `url` is not part of that type — the browser would drop\n * it — so {@link installPushHandler} moves it into `data.actionUrls`, where\n * {@link installNotificationClickHandler} reads it back when that button is\n * clicked.\n */\nexport interface PushNotificationAction {\n /** Identifier delivered as `event.action` when the button is clicked. */\n action: string;\n /** Button label. */\n title: string;\n /** Button icon URL. */\n icon?: string;\n /** URL opened when this button is clicked. Falls back to the top-level `url`. */\n url?: string;\n}\n\nexport interface PushPayload {\n title?: string;\n body?: string;\n icon?: string;\n badge?: string;\n image?: string;\n tag?: string;\n /** URL opened when the notification body is clicked. Default: `\"/\"`. */\n url?: string;\n /** Buttons rendered on the notification (browsers show up to `Notification.maxActions`). */\n actions?: PushNotificationAction[];\n /** Keep the notification on screen until the user interacts with it. */\n requireInteraction?: boolean;\n /** Vibration pattern in milliseconds (mobile). */\n vibrate?: number | number[];\n /** Show the notification without sound or vibration. */\n silent?: boolean;\n /** Alert again when a notification with the same `tag` replaces an older one. */\n renotify?: boolean;\n /** Event time in epoch milliseconds, shown by some platforms. */\n timestamp?: number;\n /** Text direction of the title and body. */\n dir?: NotificationDirection;\n /** BCP 47 language tag of the title and body. */\n lang?: string;\n /** Arbitrary extra data forwarded to `event.notification.data`. */\n data?: Record<string, unknown>;\n}\n\n/**\n * `NotificationOptions` plus the members TypeScript's DOM lib leaves out\n * because not every engine implements them. Chromium honours all of them;\n * engines without support ignore the unknown keys.\n */\ntype ExtendedNotificationOptions = NotificationOptions & {\n image?: string;\n actions?: Omit<PushNotificationAction, \"url\">[];\n vibrate?: number | number[];\n renotify?: boolean;\n timestamp?: number;\n};\n\n/**\n * Payload keys copied verbatim onto the notification options when present.\n */\nconst FORWARDED_OPTIONS = [\n \"body\",\n \"image\",\n \"tag\",\n \"requireInteraction\",\n \"vibrate\",\n \"silent\",\n \"renotify\",\n \"timestamp\",\n \"dir\",\n \"lang\",\n] as const satisfies readonly (keyof PushPayload & keyof ExtendedNotificationOptions)[];\n\n/**\n * Build the `showNotification` options from a push payload.\n *\n * Keys the payload omits stay absent instead of being set to `undefined`, so\n * the browser applies its own defaults. Per-action `url`s are collected into\n * `data.actionUrls` (only when at least one action carries one) and stripped\n * from the actions handed to the browser.\n *\n * @param payload - The parsed (and transformed) push payload.\n * @param defaultIcon - Icon used when the payload omits one.\n * @param defaultBadge - Badge used when the payload omits one.\n * @returns The options object for `registration.showNotification`.\n */\nfunction buildNotificationOptions(\n payload: PushPayload,\n defaultIcon: string | undefined,\n defaultBadge: string | undefined,\n): ExtendedNotificationOptions {\n const options: Record<string, unknown> = {\n icon: payload.icon ?? defaultIcon,\n badge: payload.badge ?? defaultBadge,\n };\n for (const key of FORWARDED_OPTIONS) {\n if (payload[key] !== undefined) options[key] = payload[key];\n }\n\n const data: Record<string, unknown> = { url: payload.url ?? \"/\", ...(payload.data ?? {}) };\n if (payload.actions) {\n const actionUrls: Record<string, string> = {};\n options.actions = payload.actions.map(({ url, ...action }) => {\n if (url !== undefined) actionUrls[action.action] = url;\n return action;\n });\n if (Object.keys(actionUrls).length > 0) data.actionUrls = actionUrls;\n }\n options.data = data;\n return options as ExtendedNotificationOptions;\n}\n\nexport interface InstallPushHandlerOptions {\n /** Title used when the payload omits one. */\n defaultTitle?: string;\n /** Icon used when the payload omits one. */\n defaultIcon?: string;\n /** Badge image (mobile). */\n defaultBadge?: string;\n /**\n * Transform the raw payload before showing the notification. Return `null`\n * to suppress the notification entirely (e.g. silent pings).\n */\n transform?: (payload: PushPayload) => PushPayload | null;\n}\n\n/**\n * Install a `push` event listener that parses the payload as JSON (with a\n * plain-text fallback) and shows a notification.\n *\n * Every `NotificationOptions` member the payload carries is forwarded —\n * `actions`, `requireInteraction`, `vibrate`, `silent`, `renotify`,\n * `timestamp`, `dir` and `lang` alongside `body`, `icon`, `badge`, `image` and\n * `tag`. `data` receives the top-level `url` (default `\"/\"`), the payload's\n * own `data`, and `actionUrls` mapping each action id to its `url`.\n */\nexport function installPushHandler(options: InstallPushHandlerOptions = {}): void {\n const sw = getSwScope();\n const { defaultTitle = \"Notificação\", defaultIcon, defaultBadge, transform } = options;\n\n sw.addEventListener(\"push\", (event) => {\n if (!event.data) return;\n\n let raw: PushPayload;\n try {\n raw = event.data.json() as PushPayload;\n } catch {\n raw = { title: defaultTitle, body: event.data.text() };\n }\n\n const payload = transform ? transform(raw) : raw;\n if (!payload) return;\n\n const title = payload.title ?? defaultTitle;\n const notification = buildNotificationOptions(payload, defaultIcon, defaultBadge);\n\n event.waitUntil(sw.registration.showNotification(title, notification));\n });\n}\n\nexport interface InstallNotificationClickHandlerOptions {\n /**\n * Resolve the destination URL from the notification data and the clicked\n * action id (`\"\"` or `undefined` for a click on the body). Default: the\n * action's entry in `data.actionUrls`, else `data.url`, else `\"/\"`.\n */\n resolveUrl?: (data: unknown, action: string | undefined) => string;\n}\n\n/**\n * Default URL resolution for a notification click.\n *\n * @param data - `event.notification.data`, as {@link installPushHandler} wrote it.\n * @param action - `event.action`; empty or `undefined` for a click on the body.\n * @returns The action's URL when it has one, else `data.url`, else `\"/\"`.\n */\nfunction defaultResolveUrl(data: unknown, action: string | undefined): string {\n if (typeof data === \"string\") return data;\n if (!data || typeof data !== \"object\") return \"/\";\n const record = data as Record<string, unknown>;\n if (action && record.actionUrls && typeof record.actionUrls === \"object\") {\n const actionUrl = (record.actionUrls as Record<string, unknown>)[action];\n if (typeof actionUrl === \"string\") return actionUrl;\n }\n return typeof record.url === \"string\" ? record.url : \"/\";\n}\n\n/**\n * Whether an open window client is already showing `target`.\n *\n * `target` is resolved against the client's own URL, so a relative path\n * compares on the client's origin, and the comparison is on the full href.\n * A substring test would let `/events/1` match a client on `/events/10`, and\n * `/` match every client.\n *\n * @param clientUrl - The window client's absolute URL.\n * @param target - The URL the click resolved to, absolute or relative.\n * @returns `true` when both point at the same document.\n */\nfunction clientShowsTarget(clientUrl: string, target: string): boolean {\n try {\n return new URL(target, clientUrl).href === new URL(clientUrl).href;\n } catch {\n return false;\n }\n}\n\n/**\n * Install a `notificationclick` handler that focuses an existing client when\n * possible and falls back to opening a new window.\n *\n * A click on an action button opens that action's URL (see\n * {@link PushNotificationAction.url}); a click on the body, or on an action\n * without one, opens the top-level `url`.\n */\nexport function installNotificationClickHandler(\n options: InstallNotificationClickHandlerOptions = {},\n): void {\n const sw = getSwScope();\n const resolveUrl = options.resolveUrl ?? defaultResolveUrl;\n\n sw.addEventListener(\"notificationclick\", (event) => {\n event.notification.close();\n const target = resolveUrl(event.notification.data, event.action);\n\n event.waitUntil(\n (async () => {\n const clients = await sw.clients.matchAll({\n type: \"window\",\n includeUncontrolled: true,\n });\n for (const client of clients) {\n if (clientShowsTarget(client.url, target)) {\n return client.focus();\n }\n }\n return sw.clients.openWindow(target);\n })(),\n );\n });\n}\n\n/**\n * Install a `message` listener that activates a waiting worker when the host\n * app sends `{ type: \"SKIP_WAITING\" }`.\n */\nexport function installSkipWaitingListener(): void {\n const sw = getSwScope() as SwGlobal & {\n addEventListener(\n type: \"message\",\n listener: (event: { data?: { type?: string } }) => void,\n ): void;\n };\n sw.addEventListener(\"message\", (event) => {\n if (event.data?.type === \"SKIP_WAITING\") {\n void sw.skipWaiting();\n }\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"create-push-handler.js","names":["getSwScope","FORWARDED_OPTIONS","buildNotificationOptions","options","payload","defaultIcon","defaultBadge","key","data","actionUrls","action","url","installPushHandler","sw","event","raw","defaultTitle","transform","title","notification","defaultResolveUrl","record","actionUrl","allowedTarget","origin","target","sameOriginOnly","clientShowsTarget","clientUrl","installNotificationClickHandler","resolveUrl","clients","client","installSkipWaitingListener"],"sources":["../../src/sw/create-push-handler.ts"],"sourcesContent":["/**\n * Service-worker context helpers for handling `push` and `notificationclick`\n * events. Import these inside your own `sw.ts` — they expect to run in the\n * service-worker global scope, not in the main thread.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installPushHandler, installNotificationClickHandler } from \"tempest-react-sdk\";\n *\n * installPushHandler({ defaultIcon: \"/icons/Logo.png\" });\n * installNotificationClickHandler();\n */\n\ninterface SwGlobal {\n registration: {\n showNotification(title: string, options?: NotificationOptions): Promise<void>;\n };\n clients: {\n matchAll(options: { type: \"window\"; includeUncontrolled?: boolean }): Promise<\n {\n url: string;\n focused: boolean;\n focus(): Promise<unknown>;\n navigate(url: string): Promise<unknown>;\n }[]\n >;\n openWindow(url: string): Promise<unknown>;\n };\n addEventListener(\n type: \"push\",\n listener: (event: {\n data: { json(): unknown; text(): string } | null;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n addEventListener(\n type: \"notificationclick\",\n listener: (event: {\n notification: { close(): void; data?: unknown };\n action?: string;\n waitUntil(promise: Promise<unknown>): void;\n }) => void,\n ): void;\n skipWaiting(): Promise<void>;\n location?: { origin: string };\n}\n\nfunction getSwScope(): SwGlobal {\n return globalThis as unknown as SwGlobal;\n}\n\n/**\n * A notification button as the push payload describes it.\n *\n * `action`, `title` and `icon` are forwarded to the browser as a\n * `NotificationAction`. `url` is not part of that type — the browser would drop\n * it — so {@link installPushHandler} moves it into `data.actionUrls`, where\n * {@link installNotificationClickHandler} reads it back when that button is\n * clicked.\n */\nexport interface PushNotificationAction {\n /** Identifier delivered as `event.action` when the button is clicked. */\n action: string;\n /** Button label. */\n title: string;\n /** Button icon URL. */\n icon?: string;\n /** URL opened when this button is clicked. Falls back to the top-level `url`. */\n url?: string;\n}\n\nexport interface PushPayload {\n title?: string;\n body?: string;\n icon?: string;\n badge?: string;\n image?: string;\n tag?: string;\n /** URL opened when the notification body is clicked. Default: `\"/\"`. */\n url?: string;\n /** Buttons rendered on the notification (browsers show up to `Notification.maxActions`). */\n actions?: PushNotificationAction[];\n /** Keep the notification on screen until the user interacts with it. */\n requireInteraction?: boolean;\n /** Vibration pattern in milliseconds (mobile). */\n vibrate?: number | number[];\n /** Show the notification without sound or vibration. */\n silent?: boolean;\n /** Alert again when a notification with the same `tag` replaces an older one. */\n renotify?: boolean;\n /** Event time in epoch milliseconds, shown by some platforms. */\n timestamp?: number;\n /** Text direction of the title and body. */\n dir?: NotificationDirection;\n /** BCP 47 language tag of the title and body. */\n lang?: string;\n /**\n * Arbitrary extra data forwarded to `event.notification.data`.\n *\n * Its `url` and `actionUrls` keys are overwritten by the top-level `url` and\n * the actions' `url`s: those two keys decide where a click goes, and a\n * free-form bag — often filled from the record that triggered the push — must\n * not be able to redirect it.\n */\n data?: Record<string, unknown>;\n}\n\n/**\n * `NotificationOptions` plus the members TypeScript's DOM lib leaves out\n * because not every engine implements them. Chromium honours all of them;\n * engines without support ignore the unknown keys.\n */\ntype ExtendedNotificationOptions = NotificationOptions & {\n image?: string;\n actions?: Omit<PushNotificationAction, \"url\">[];\n vibrate?: number | number[];\n renotify?: boolean;\n timestamp?: number;\n};\n\n/**\n * Payload keys copied verbatim onto the notification options when present.\n */\nconst FORWARDED_OPTIONS = [\n \"body\",\n \"image\",\n \"tag\",\n \"requireInteraction\",\n \"vibrate\",\n \"silent\",\n \"renotify\",\n \"timestamp\",\n \"dir\",\n \"lang\",\n] as const satisfies readonly (keyof PushPayload & keyof ExtendedNotificationOptions)[];\n\n/**\n * Build the `showNotification` options from a push payload.\n *\n * Keys the payload omits stay absent instead of being set to `undefined`, so\n * the browser applies its own defaults. Per-action `url`s are collected into\n * `data.actionUrls` (only when at least one action carries one) and stripped\n * from the actions handed to the browser.\n *\n * `payload.data` is spread **first**, so its own `url` or `actionUrls` can never\n * replace the destinations the payload declares: until 0.81.0 it was spread last,\n * and `{ url: \"/orders/9\", data: { url: \"https://elsewhere\" } }` opened\n * `https://elsewhere` on click.\n *\n * @param payload - The parsed (and transformed) push payload.\n * @param defaultIcon - Icon used when the payload omits one.\n * @param defaultBadge - Badge used when the payload omits one.\n * @returns The options object for `registration.showNotification`.\n */\nfunction buildNotificationOptions(\n payload: PushPayload,\n defaultIcon: string | undefined,\n defaultBadge: string | undefined,\n): ExtendedNotificationOptions {\n const options: Record<string, unknown> = {\n icon: payload.icon ?? defaultIcon,\n badge: payload.badge ?? defaultBadge,\n };\n for (const key of FORWARDED_OPTIONS) {\n if (payload[key] !== undefined) options[key] = payload[key];\n }\n\n const data: Record<string, unknown> = { ...(payload.data ?? {}), url: payload.url ?? \"/\" };\n delete data.actionUrls;\n if (payload.actions) {\n const actionUrls: Record<string, string> = {};\n options.actions = payload.actions.map(({ url, ...action }) => {\n if (url !== undefined) actionUrls[action.action] = url;\n return action;\n });\n if (Object.keys(actionUrls).length > 0) data.actionUrls = actionUrls;\n }\n options.data = data;\n return options as ExtendedNotificationOptions;\n}\n\nexport interface InstallPushHandlerOptions {\n /** Title used when the payload omits one. */\n defaultTitle?: string;\n /** Icon used when the payload omits one. */\n defaultIcon?: string;\n /** Badge image (mobile). */\n defaultBadge?: string;\n /**\n * Transform the raw payload before showing the notification. Return `null`\n * to suppress the notification entirely (e.g. silent pings).\n */\n transform?: (payload: PushPayload) => PushPayload | null;\n}\n\n/**\n * Install a `push` event listener that parses the payload as JSON (with a\n * plain-text fallback) and shows a notification.\n *\n * Every `NotificationOptions` member the payload carries is forwarded —\n * `actions`, `requireInteraction`, `vibrate`, `silent`, `renotify`,\n * `timestamp`, `dir` and `lang` alongside `body`, `icon`, `badge`, `image` and\n * `tag`. `data` receives the payload's own `data`, then the top-level `url`\n * (default `\"/\"`) and `actionUrls` mapping each action id to its `url` — in that\n * order, so `payload.data` cannot override where a click goes.\n */\nexport function installPushHandler(options: InstallPushHandlerOptions = {}): void {\n const sw = getSwScope();\n const { defaultTitle = \"Notificação\", defaultIcon, defaultBadge, transform } = options;\n\n sw.addEventListener(\"push\", (event) => {\n if (!event.data) return;\n\n let raw: PushPayload;\n try {\n raw = event.data.json() as PushPayload;\n } catch {\n raw = { title: defaultTitle, body: event.data.text() };\n }\n\n const payload = transform ? transform(raw) : raw;\n if (!payload) return;\n\n const title = payload.title ?? defaultTitle;\n const notification = buildNotificationOptions(payload, defaultIcon, defaultBadge);\n\n event.waitUntil(sw.registration.showNotification(title, notification));\n });\n}\n\nexport interface InstallNotificationClickHandlerOptions {\n /**\n * Resolve the destination URL from the notification data and the clicked\n * action id (`\"\"` or `undefined` for a click on the body). Default: the\n * action's entry in `data.actionUrls`, else `data.url`, else `\"/\"`.\n *\n * The default only ever opens a page of the worker's **own origin**: a URL on\n * another origin, or with a scheme other than `http:`/`https:`, becomes `\"/\"`.\n * A resolver of your own is how an app opts into another origin — what it\n * returns may point anywhere `http:`/`https:`, and any other scheme still\n * becomes `\"/\"`.\n */\n resolveUrl?: (data: unknown, action: string | undefined) => string;\n}\n\n/**\n * Default URL resolution for a notification click.\n *\n * @param data - `event.notification.data`, as {@link installPushHandler} wrote it.\n * @param action - `event.action`; empty or `undefined` for a click on the body.\n * @returns The action's URL when it has one, else `data.url`, else `\"/\"`.\n */\nfunction defaultResolveUrl(data: unknown, action: string | undefined): string {\n if (typeof data === \"string\") return data;\n if (!data || typeof data !== \"object\") return \"/\";\n const record = data as Record<string, unknown>;\n if (action && record.actionUrls && typeof record.actionUrls === \"object\") {\n const actionUrl = (record.actionUrls as Record<string, unknown>)[action];\n if (typeof actionUrl === \"string\") return actionUrl;\n }\n return typeof record.url === \"string\" ? record.url : \"/\";\n}\n\n/**\n * The URL a click may open, given the worker's origin.\n *\n * The push payload is written by the app's server, but its `url` is often\n * copied from the record that triggered the push — a chat message, a ticket —\n * and `openWindow` opens it with the app's notification as the only context the\n * user sees. So the default resolver keeps to the worker's own origin, the same\n * line `safeLinkUrl` draws for rendered links, and a custom resolver is the\n * explicit opt-in to leave it. Neither may open a non-`http(s)` scheme.\n *\n * The URL comes back as given, not resolved, so a relative path stays relative —\n * `openWindow` resolves it against the worker's location exactly like this check\n * did.\n *\n * @param target - What the resolver returned.\n * @param origin - The worker's origin; `undefined` outside a worker, where\n * nothing can be judged and only `\"/\"` is opened.\n * @param sameOriginOnly - Whether another origin is refused too.\n * @returns `target` when it may be opened, `\"/\"` otherwise.\n */\nfunction allowedTarget(\n target: string,\n origin: string | undefined,\n sameOriginOnly: boolean,\n): string {\n if (origin === undefined) return \"/\";\n let url: URL;\n try {\n url = new URL(target, origin);\n } catch {\n return \"/\";\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return \"/\";\n if (sameOriginOnly && url.origin !== origin) return \"/\";\n return target;\n}\n\n/**\n * Whether an open window client is already showing `target`.\n *\n * `target` is resolved against the client's own URL, so a relative path\n * compares on the client's origin, and the comparison is on the full href.\n * A substring test would let `/events/1` match a client on `/events/10`, and\n * `/` match every client.\n *\n * @param clientUrl - The window client's absolute URL.\n * @param target - The URL the click resolved to, absolute or relative.\n * @returns `true` when both point at the same document.\n */\nfunction clientShowsTarget(clientUrl: string, target: string): boolean {\n try {\n return new URL(target, clientUrl).href === new URL(clientUrl).href;\n } catch {\n return false;\n }\n}\n\n/**\n * Install a `notificationclick` handler that focuses an existing client when\n * possible and falls back to opening a new window.\n *\n * A click on an action button opens that action's URL (see\n * {@link PushNotificationAction.url}); a click on the body, or on an action\n * without one, opens the top-level `url`. Only a same-origin `http(s)` URL is\n * opened by default; see {@link InstallNotificationClickHandlerOptions.resolveUrl}\n * for opting into another origin.\n */\nexport function installNotificationClickHandler(\n options: InstallNotificationClickHandlerOptions = {},\n): void {\n const sw = getSwScope();\n const resolveUrl = options.resolveUrl ?? defaultResolveUrl;\n const sameOriginOnly = options.resolveUrl === undefined;\n\n sw.addEventListener(\"notificationclick\", (event) => {\n event.notification.close();\n const target = allowedTarget(\n resolveUrl(event.notification.data, event.action),\n sw.location?.origin,\n sameOriginOnly,\n );\n\n event.waitUntil(\n (async () => {\n const clients = await sw.clients.matchAll({\n type: \"window\",\n includeUncontrolled: true,\n });\n for (const client of clients) {\n if (clientShowsTarget(client.url, target)) {\n return client.focus();\n }\n }\n return sw.clients.openWindow(target);\n })(),\n );\n });\n}\n\n/**\n * Install a `message` listener that activates a waiting worker when the host\n * app sends `{ type: \"SKIP_WAITING\" }`.\n */\nexport function installSkipWaitingListener(): void {\n const sw = getSwScope() as SwGlobal & {\n addEventListener(\n type: \"message\",\n listener: (event: { data?: { type?: string } }) => void,\n ): void;\n };\n sw.addEventListener(\"message\", (event) => {\n if (event.data?.type === \"SKIP_WAITING\") {\n void sw.skipWaiting();\n }\n });\n}\n"],"mappings":"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"}
|
package/dist/sw.d.ts
CHANGED
|
@@ -336,7 +336,9 @@ export declare interface InstallBackgroundSyncOptions {
|
|
|
336
336
|
*
|
|
337
337
|
* A click on an action button opens that action's URL (see
|
|
338
338
|
* {@link PushNotificationAction.url}); a click on the body, or on an action
|
|
339
|
-
* without one, opens the top-level `url`.
|
|
339
|
+
* without one, opens the top-level `url`. Only a same-origin `http(s)` URL is
|
|
340
|
+
* opened by default; see {@link InstallNotificationClickHandlerOptions.resolveUrl}
|
|
341
|
+
* for opting into another origin.
|
|
340
342
|
*/
|
|
341
343
|
export declare function installNotificationClickHandler(options?: InstallNotificationClickHandlerOptions): void;
|
|
342
344
|
|
|
@@ -345,6 +347,12 @@ export declare interface InstallNotificationClickHandlerOptions {
|
|
|
345
347
|
* Resolve the destination URL from the notification data and the clicked
|
|
346
348
|
* action id (`""` or `undefined` for a click on the body). Default: the
|
|
347
349
|
* action's entry in `data.actionUrls`, else `data.url`, else `"/"`.
|
|
350
|
+
*
|
|
351
|
+
* The default only ever opens a page of the worker's **own origin**: a URL on
|
|
352
|
+
* another origin, or with a scheme other than `http:`/`https:`, becomes `"/"`.
|
|
353
|
+
* A resolver of your own is how an app opts into another origin — what it
|
|
354
|
+
* returns may point anywhere `http:`/`https:`, and any other scheme still
|
|
355
|
+
* becomes `"/"`.
|
|
348
356
|
*/
|
|
349
357
|
resolveUrl?: (data: unknown, action: string | undefined) => string;
|
|
350
358
|
}
|
|
@@ -357,7 +365,27 @@ export declare interface InstallNotificationClickHandlerOptions {
|
|
|
357
365
|
* - on `fetch`, serves precached assets cache-first and falls back to the
|
|
358
366
|
* `navigateFallback` document for offline navigations (SPA routing).
|
|
359
367
|
*
|
|
360
|
-
*
|
|
368
|
+
* Survives the browser stopping and restarting the worker, which it does
|
|
369
|
+
* whenever the worker sits idle and which never repeats `install`. Nothing the
|
|
370
|
+
* fetch handler needs lives only in memory: `install` and `activate` record, in
|
|
371
|
+
* an entry inside the versioned cache, when that version was installed and
|
|
372
|
+
* activated, and a restarted worker reads back the activated version and its
|
|
373
|
+
* pathnames from Cache Storage. Before 0.83.0 the cache name and the precached
|
|
374
|
+
* paths were module variables filled only by `install`, so after the first
|
|
375
|
+
* restart offline navigations failed with "offline and no cached app shell",
|
|
376
|
+
* precached assets went to the network, and an `activate` in a run without
|
|
377
|
+
* `install` deleted every precache, the current one included (#572).
|
|
378
|
+
*
|
|
379
|
+
* `activate` deletes the other `${cacheName}-*` caches only once it knows which
|
|
380
|
+
* version it is activating; when no version recorded an install it deletes
|
|
381
|
+
* nothing. In the first request after a restart, while the worker is still
|
|
382
|
+
* reading which paths are precached, a same-origin `GET` that a runtime route
|
|
383
|
+
* did not answer is answered with `fetch(request)` when it is not precached —
|
|
384
|
+
* the decision to call `respondWith` cannot wait for Cache Storage. From then on
|
|
385
|
+
* a non-precached request is left to the browser, as before.
|
|
386
|
+
*
|
|
387
|
+
* Same-origin only. Register this LAST, after any {@link installRuntimeCache}:
|
|
388
|
+
* a request a runtime route answered is never answered again here.
|
|
361
389
|
*/
|
|
362
390
|
export declare function installPrecache(options?: InstallPrecacheOptions): void;
|
|
363
391
|
|
|
@@ -389,8 +417,9 @@ export declare interface InstallPrecacheOptions {
|
|
|
389
417
|
* Every `NotificationOptions` member the payload carries is forwarded —
|
|
390
418
|
* `actions`, `requireInteraction`, `vibrate`, `silent`, `renotify`,
|
|
391
419
|
* `timestamp`, `dir` and `lang` alongside `body`, `icon`, `badge`, `image` and
|
|
392
|
-
* `tag`. `data` receives the top-level `url`
|
|
393
|
-
*
|
|
420
|
+
* `tag`. `data` receives the payload's own `data`, then the top-level `url`
|
|
421
|
+
* (default `"/"`) and `actionUrls` mapping each action id to its `url` — in that
|
|
422
|
+
* order, so `payload.data` cannot override where a click goes.
|
|
394
423
|
*/
|
|
395
424
|
export declare function installPushHandler(options?: InstallPushHandlerOptions): void;
|
|
396
425
|
|
|
@@ -414,9 +443,10 @@ export declare interface InstallPushHandlerOptions {
|
|
|
414
443
|
* `respondWith`), so a later {@link installPrecache} can handle them.
|
|
415
444
|
*
|
|
416
445
|
* A request carrying a credential header (`Authorization`, `X-Api-Key`, … — the
|
|
417
|
-
* route's `credentialHeaders`)
|
|
446
|
+
* route's `credentialHeaders`), or a CORS `fetch` sent with
|
|
447
|
+
* `credentials: "include"`, is also left untouched unless its route sets
|
|
418
448
|
* `cacheAuthenticated`, and a response with `Cache-Control: no-store`
|
|
419
|
-
* (or `private`, without that opt-in) is served but never stored. `maxAgeSeconds`
|
|
449
|
+
* (or `private`, without that opt-in) or `Vary: *` is served but never stored. `maxAgeSeconds`
|
|
420
450
|
* applies to every read, including the offline fallback: an expired entry is a
|
|
421
451
|
* miss, never a last resort. Under `rangeRequests`, the full resource is cached
|
|
422
452
|
* once (the `Range` header stripped) and each range is sliced from it.
|
|
@@ -457,7 +487,13 @@ export declare function installSkipWaitingListener(): void;
|
|
|
457
487
|
*
|
|
458
488
|
* Pass `getPrincipal` to bind each entry to whoever queued it: the worker asks
|
|
459
489
|
* the issuing tab right after queueing, asks every tab again before replaying,
|
|
460
|
-
* and drops the entry when no signed-in tab is its owner.
|
|
490
|
+
* and drops the entry when no signed-in tab is its owner. Without it every
|
|
491
|
+
* entry is `"unbound"` and replays with whichever session answers — the next
|
|
492
|
+
* account's bearer included, unless `clearSessionData()` ran on logout — so a
|
|
493
|
+
* development build warns once per call (#531). Treating an unbound
|
|
494
|
+
* credentialed entry as `"unknown"` instead would change nothing: an
|
|
495
|
+
* `"unknown"` entry replays when no answering tab reports a principal, which is
|
|
496
|
+
* exactly the page that omits `getPrincipal`.
|
|
461
497
|
*
|
|
462
498
|
* @param getHeaders - Resolves the headers for one queued request at replay.
|
|
463
499
|
* @param options - `getPrincipal` binds entries to their owner.
|
|
@@ -548,7 +584,14 @@ export declare interface PushPayload {
|
|
|
548
584
|
dir?: NotificationDirection;
|
|
549
585
|
/** BCP 47 language tag of the title and body. */
|
|
550
586
|
lang?: string;
|
|
551
|
-
/**
|
|
587
|
+
/**
|
|
588
|
+
* Arbitrary extra data forwarded to `event.notification.data`.
|
|
589
|
+
*
|
|
590
|
+
* Its `url` and `actionUrls` keys are overwritten by the top-level `url` and
|
|
591
|
+
* the actions' `url`s: those two keys decide where a click goes, and a
|
|
592
|
+
* free-form bag — often filled from the record that triggered the push — must
|
|
593
|
+
* not be able to redirect it.
|
|
594
|
+
*/
|
|
552
595
|
data?: Record<string, unknown>;
|
|
553
596
|
}
|
|
554
597
|
|
|
@@ -679,8 +722,9 @@ export declare interface RuntimeRoute {
|
|
|
679
722
|
*/
|
|
680
723
|
rangeRequests?: boolean;
|
|
681
724
|
/**
|
|
682
|
-
* Opt in to caching requests that carry a credential
|
|
683
|
-
* `credentialHeaders`)
|
|
725
|
+
* Opt in to caching credentialed requests — those that carry a credential
|
|
726
|
+
* header (see `credentialHeaders`) or are a CORS-mode `fetch` sent with
|
|
727
|
+
* `credentials: "include"` — and responses marked `Cache-Control: private`.
|
|
684
728
|
* Default `false`.
|
|
685
729
|
*
|
|
686
730
|
* Cache Storage is keyed by URL and outlives the session: without this
|
|
@@ -697,11 +741,18 @@ export declare interface RuntimeRoute {
|
|
|
697
741
|
* `clearSessionData()` on logout, which removes the entries themselves.
|
|
698
742
|
* `Cache-Control: no-store` is never cached, opt-in or not.
|
|
699
743
|
*
|
|
700
|
-
* A cookie session is
|
|
701
|
-
*
|
|
702
|
-
*
|
|
744
|
+
* A cookie session is only partly visible here: the worker cannot see
|
|
745
|
+
* `Cookie`. What it can see is a `fetch` that asked for cookies explicitly —
|
|
746
|
+
* `mode: "cors"` with `credentials: "include"`, which is what `ApiClient`
|
|
747
|
+
* sends under `withCredentials` — and since 0.82.0 that request is treated
|
|
748
|
+
* as credentialed too (#531). `<img>`, `<audio>` and `<video>` also send
|
|
749
|
+
* `credentials: "include"`, but in `no-cors` mode, so they keep caching. A
|
|
750
|
+
* plain same-origin `fetch` (`credentials: "same-origin"`, the default)
|
|
751
|
+
* carries the cookie without saying so; such an API stays private only
|
|
703
752
|
* through `Cache-Control: private`/`no-store` on its responses and
|
|
704
|
-
* `clearSessionData()` on logout.
|
|
753
|
+
* `clearSessionData()` on logout. Opting a cookie request in caches it by
|
|
754
|
+
* URL — there is no credential header to vary on — so `clearSessionData()`
|
|
755
|
+
* is then its only isolation.
|
|
705
756
|
*/
|
|
706
757
|
cacheAuthenticated?: boolean;
|
|
707
758
|
/**
|
package/dist/tabular/compact.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./exceptions.cjs");var t=`TMC1`,n=class n{header;arrays;info;constructor(e,t,n){this.header=e,this.arrays=t,this.info=n}static async create(
|
|
1
|
+
const e=require("./exceptions.cjs");var t=`TMC1`,n=class n{header;arrays;info;constructor(e,t,n){this.header=e,this.arrays=t,this.info=n}static async create(a,o){let s=await r(a,o),c=new Uint8Array(s);if(String.fromCharCode(...c.subarray(0,4))!==t)throw new e.CompactFormatError(`This is not a compact model file: it does not start with "${t}". A .onnx file goes through TabularPredictor instead.`);let l=new DataView(s).getUint32(4,!0),u=JSON.parse(new TextDecoder().decode(c.subarray(8,8+l)));if(u.schema_version>1)throw new e.CompactFormatError(`This file uses compact layout ${u.schema_version}, and this SDK understands 1. Upgrade tempest-react-sdk before serving it.`);let d={},f=8+l;for(let e of u.sections)d[e.name]=e.dtype===`float32`?new Float32Array(s,f,e.length):new Int32Array(s,f,e.length),f+=e.length*4;return u.kind===`tree_ensemble`&&i(u,d),new n(u,d,{kind:u.kind,classes:u.classes,numFeatures:u.n_features,featureNames:u.feature_names,numTrees:u.n_trees??0,isClassifier:u.task===`classification`,estimator:u.estimator})}async predict(e){let t=a(e,this.header.n_features),n=performance.now(),r=[];for(let n of e){let e=this.preprocess(n,t);r.push(this.header.kind===`linear`?this.linearScores(e):this.treeScores(e))}let{labels:i,probabilities:o}=this.finish(r);return{labels:i,probabilities:o,numRows:e.length,ms:performance.now()-n}}async dispose(){}preprocess(e,t){let n=this.header.preprocess;if(n===null)return e;let r=Array(t);for(let i=0;i<t;i+=1)r[i]=(e[i]-n.offset[i])/n.scale[i];return r}linearScores(e){let t=this.arrays.coef,n=this.arrays.intercept,r=this.header.n_features,i=this.header.n_outputs,a=Array(i);for(let o=0;o<i;o+=1){let i=n[o],s=o*r;for(let n=0;n<r;n+=1)i+=t[s+n]*e[n];a[o]=i}return a}treeScores(e){let t=this.arrays.node_feature,n=this.arrays.node_threshold,r=this.arrays.node_left,i=this.arrays.node_right,a=this.arrays.leaf_value,o=this.arrays.tree_offset,s=this.header.n_outputs,c=o.length-1,l=Array(s).fill(0);for(let u=0;u<c;u+=1){let c=o[u],d=t[c];for(;d>=0;)c=Math.fround(e[d])<=n[c]?r[c]:i[c],d=t[c];let f=(-1-d)*s;for(let e=0;e<s;e+=1)l[e]+=a[f+e]}for(let e=0;e<s;e+=1)l[e]=l[e]/c;return l}finish(e){let t=this.header.link,n=this.header.classes;if(t===`identity`)return{labels:e.map(e=>e[0]),probabilities:[]};let r=e.map(e=>{if(t===`sigmoid`){let t=1/(1+Math.exp(-e[0]));return[1-t,t]}if(t===`softmax`){let t=Math.max(...e),n=e.map(e=>Math.exp(e-t)),r=n.reduce((e,t)=>e+t,0);return n.map(e=>e/r)}let n=e.reduce((e,t)=>e+t,0);return n===0?[...e]:e.map(e=>e/n)}),i=this.header.class_type!==`str`;return{labels:r.map(e=>{let t=0;for(let n=1;n<e.length;n+=1)e[n]>e[t]&&(t=n);let r=n[t];return r===void 0?t:i?Number(r):r}),probabilities:r}}};async function r(t,n){if(typeof t==`string`){let r;try{r=await fetch(t,n)}catch(n){throw new e.ModelFetchError(`Could not download the model: ${t}`,{cause:n})}if(!r.ok)throw new e.ModelFetchError(`Could not download the model: ${r.status} ${r.statusText}`);return await r.arrayBuffer()}return t instanceof Uint8Array?t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength):t}function i(t,n){let r=n.node_feature,i=n.node_left,a=n.node_right,o=n.leaf_value,s=n.tree_offset;if(!r||!i||!a||!o||!s||s.length<2)throw new e.CompactFormatError(`This tree model is missing its node or offset sections.`);let c=r.length,l=t.n_outputs,u=t=>new e.CompactFormatError(`This tree model is malformed: ${t}. Re-export it.`);if(i.length!==c||a.length!==c||s[0]!==0)throw u(`the node sections disagree on the node count`);if(s[s.length-1]!==c)throw u(`the last tree does not end at the last node`);for(let e=0;e<s.length-1;e+=1){let n=s[e],c=s[e+1];if(c<=n)throw u(`tree ${e} has no nodes`);for(let e=n;e<c;e+=1){let n=r[e];if(n>=0){let r=i[e],o=a[e];if(n>=t.n_features)throw u(`node ${e} splits on feature ${n}`);if(r<=e||o<=e||r>=c||o>=c)throw u(`node ${e} points outside its tree or back up it`)}else if((-1-n)*l+l>o.length)throw u(`leaf ${e} reads past the leaf values`)}}}function a(t,n){if(!Array.isArray(t)||t.length===0)throw new e.FeatureShapeError(`predict() needs at least one row, shaped [[f1, f2, ...]].`);let r=t[0]?.length??0,i=t.findIndex(e=>e.length!==r);if(i!==-1)throw new e.FeatureShapeError(`All rows must have the same width; row ${i} has ${t[i]?.length} values, expected ${r}.`);if(r!==n)throw new e.FeatureShapeError(`The model expects ${n} features per row, got ${r}.`);return r}exports.CompactPredictor=n;
|
|
2
2
|
//# sourceMappingURL=compact.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compact.cjs","names":["CompactFormatError","ModelFetchError","FeatureShapeError","require_exceptions","MAGIC","CompactPredictor","header","arrays","info","buffer","toBuffer","source","requestInit","bytes","headerLength","cursor","section","width","validateRows","rows","started","scores","row","prepared","preprocess","scaled","index","coef","intercept","features","outputs","output","total","base","feature","threshold","left","right","leaf","offsets","trees","totals","tree","node","column","slot","link","classes","probabilities","positive","highest","exponentiated","value","sum","numeric","best","label","response","error","ragged","expected"],"sources":["../../src/tabular/compact.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the compact model format: the header, the column\n * dictionary, the quantised weights and the decoder that reads them back. Writer and\n * reader in one file because a format whose two halves live apart drifts.\n */\n/**\n * Running a model with no inference runtime at all.\n *\n * ONNX in the browser costs a **25.6 MB WebAssembly runtime** (6.0 MB\n * gzipped) before the first prediction, while the model itself is a few\n * hundred kilobytes. For an app whose only model is tabular, that runtime\n * *is* the download — so this reader replaces it with 1.49 KB of\n * arithmetic (measured, brotli).\n *\n * A linear model is a dot product. A tree is a chain of comparisons. That\n * is the whole implementation; there is nothing here that a WebAssembly\n * kernel would do better at this size.\n *\n * The file it reads (`.tmc`, magic `TMC1`) is written by\n * `tempest-fastapi-sdk`'s `export_sklearn_to_compact`, which verifies the\n * bytes against scikit-learn's own predictions and refuses to write a file\n * that disagrees. **It is data, not code**: no `eval`, no generated\n * JavaScript, nothing a strict CSP forbids.\n *\n * The trade against {@link TabularPredictor}: ONNX covers every estimator,\n * this covers linear models and tree ensembles. Pick by what your app\n * already ships — a page that loads ONNX for vision pays nothing extra for\n * tabular ONNX.\n */\n\nimport { CompactFormatError, FeatureShapeError, ModelFetchError } from \"./exceptions\";\nimport type { FeatureRow, PredictedLabel, TabularPrediction } from \"./types\";\n\n/** Magic bytes opening every compact file. */\nconst MAGIC = \"TMC1\";\n\n/** Layout version this reader understands. */\nexport const SUPPORTED_COMPACT_SCHEMA = 1;\n\n/** What the file holds. */\nexport type CompactKind = \"linear\" | \"tree_ensemble\";\n\n/** How raw scores become probabilities. */\ntype CompactLink = \"softmax\" | \"sigmoid\" | \"normalize\" | \"identity\";\n\n/** One array stored after the header. */\ninterface CompactSection {\n readonly name: string;\n readonly dtype: \"float32\" | \"int32\";\n readonly length: number;\n}\n\n/** The parsed header. */\ninterface CompactHeader {\n readonly schema_version: number;\n readonly kind: CompactKind;\n readonly task: \"classification\" | \"regression\";\n readonly link: CompactLink;\n readonly classes: readonly string[];\n /** How scikit-learn typed those labels: ``int``, ``float`` or ``str``. */\n readonly class_type?: \"int\" | \"float\" | \"str\";\n readonly n_features: number;\n readonly n_outputs: number;\n readonly n_trees?: number;\n readonly estimator: string;\n readonly feature_names: readonly string[];\n readonly preprocess: { readonly offset: number[]; readonly scale: number[] } | null;\n readonly sections: readonly CompactSection[];\n}\n\n/** What a loaded compact model is. */\nexport interface CompactPredictorInfo {\n /** Which reader path the file uses. */\n readonly kind: CompactKind;\n /** Class labels in score-column order; empty for a regressor. */\n readonly classes: readonly string[];\n /** Values expected per row. */\n readonly numFeatures: number;\n /** Column order recorded at training time, when the export had one. */\n readonly featureNames: readonly string[];\n /** Trees in the ensemble; `0` for a linear model. */\n readonly numTrees: number;\n /** Whether the model produces class scores. */\n readonly isClassifier: boolean;\n /** Class name of the exported estimator. */\n readonly estimator: string;\n}\n\n/**\n * A compact model, loaded and ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await CompactPredictor.create(\"/models/risk.tmc\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class CompactPredictor {\n private constructor(\n private readonly header: CompactHeader,\n private readonly arrays: Record<string, Float32Array | Int32Array>,\n /** What is loaded. */\n public readonly info: CompactPredictorInfo,\n ) {}\n\n /**\n * Load a `.tmc` file.\n *\n * @param source A URL, or the bytes when the app already has them.\n * @param requestInit `fetch` options, when `source` is a URL.\n * @returns The loaded predictor.\n * @throws {@link ModelFetchError} when a URL cannot be read.\n * @throws {@link CompactFormatError} when the bytes are not a compact\n * model, or use a layout newer than this reader.\n */\n static async create(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n ): Promise<CompactPredictor> {\n const buffer = await toBuffer(source, requestInit);\n const bytes = new Uint8Array(buffer);\n\n if (String.fromCharCode(...bytes.subarray(0, 4)) !== MAGIC) {\n throw new CompactFormatError(\n \"This is not a compact model file: it does not start with \" +\n `\"${MAGIC}\". A .onnx file goes through TabularPredictor instead.`,\n );\n }\n\n const view = new DataView(buffer);\n const headerLength = view.getUint32(4, true);\n const header = JSON.parse(\n new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)),\n ) as CompactHeader;\n\n if (header.schema_version > SUPPORTED_COMPACT_SCHEMA) {\n throw new CompactFormatError(\n `This file uses compact layout ${header.schema_version}, and this ` +\n `SDK understands ${SUPPORTED_COMPACT_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving it.\",\n );\n }\n\n const arrays: Record<string, Float32Array | Int32Array> = {};\n let cursor = 8 + headerLength;\n for (const section of header.sections) {\n arrays[section.name] =\n section.dtype === \"float32\"\n ? new Float32Array(buffer, cursor, section.length)\n : new Int32Array(buffer, cursor, section.length);\n cursor += section.length * 4;\n }\n\n return new CompactPredictor(header, arrays, {\n kind: header.kind,\n classes: header.classes,\n numFeatures: header.n_features,\n featureNames: header.feature_names,\n numTrees: header.n_trees ?? 0,\n isClassifier: header.task === \"classification\",\n estimator: header.estimator,\n });\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model is a classifier, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n const width = validateRows(rows, this.header.n_features);\n const started = performance.now();\n\n const scores: number[][] = [];\n for (const row of rows) {\n const prepared = this.preprocess(row, width);\n scores.push(\n this.header.kind === \"linear\"\n ? this.linearScores(prepared)\n : this.treeScores(prepared),\n );\n }\n\n const { labels, probabilities } = this.finish(scores);\n return {\n labels,\n probabilities,\n numRows: rows.length,\n ms: performance.now() - started,\n };\n }\n\n /** Releasing nothing, so callers can swap predictors without branching. */\n async dispose(): Promise<void> {\n /* no runtime, nothing to release */\n }\n\n /**\n * Apply the folded scaler, when the export had one.\n *\n * @param row The raw feature values.\n * @param width How many there are.\n * @returns The values the model was trained on.\n */\n private preprocess(row: FeatureRow, width: number): number[] {\n const preprocess = this.header.preprocess;\n if (preprocess === null) return row as number[];\n const scaled = new Array<number>(width);\n for (let index = 0; index < width; index += 1) {\n scaled[index] =\n ((row[index] as number) - (preprocess.offset[index] as number)) /\n (preprocess.scale[index] as number);\n }\n return scaled;\n }\n\n /**\n * Score one row against the coefficient matrix.\n *\n * @param row The prepared feature values.\n * @returns One raw score per output.\n */\n private linearScores(row: readonly number[]): number[] {\n const coef = this.arrays.coef as Float32Array;\n const intercept = this.arrays.intercept as Float32Array;\n const features = this.header.n_features;\n const outputs = this.header.n_outputs;\n\n const scores = new Array<number>(outputs);\n for (let output = 0; output < outputs; output += 1) {\n let total = intercept[output] as number;\n const base = output * features;\n for (let index = 0; index < features; index += 1) {\n total += (coef[base + index] as number) * (row[index] as number);\n }\n scores[output] = total;\n }\n return scores;\n }\n\n /**\n * Walk every tree and average what the leaves hold.\n *\n * A leaf is marked by a negative `feature` entry, which also carries\n * its slot in the value array — so the walk needs no second lookup and\n * the file stores values only for leaves.\n *\n * **The comparison runs in float32** (`Math.fround`), because that is\n * what scikit-learn does: it casts its input to float32 before\n * traversing, so a threshold like 5.099999904632568 — a float32 value\n * widened for storage — and an input of 5.1 compare *equal* and go\n * left. Comparing in float64 sends that row right instead, which on an\n * iris forest changed one tree's vote and moved a probability by 0.05.\n *\n * @param row The prepared feature values.\n * @returns One averaged score per output.\n */\n private treeScores(row: readonly number[]): number[] {\n const feature = this.arrays.node_feature as Int32Array;\n const threshold = this.arrays.node_threshold as Float32Array;\n const left = this.arrays.node_left as Int32Array;\n const right = this.arrays.node_right as Int32Array;\n const leaf = this.arrays.leaf_value as Float32Array;\n const offsets = this.arrays.tree_offset as Int32Array;\n const outputs = this.header.n_outputs;\n const trees = offsets.length - 1;\n\n const totals = new Array<number>(outputs).fill(0);\n for (let tree = 0; tree < trees; tree += 1) {\n let node = offsets[tree] as number;\n let column = feature[node] as number;\n while (column >= 0) {\n node =\n Math.fround(row[column] as number) <= (threshold[node] as number)\n ? (left[node] as number)\n : (right[node] as number);\n column = feature[node] as number;\n }\n const slot = (-1 - column) * outputs;\n for (let output = 0; output < outputs; output += 1) {\n totals[output] += leaf[slot + output] as number;\n }\n }\n for (let output = 0; output < outputs; output += 1) {\n totals[output] = (totals[output] as number) / trees;\n }\n return totals;\n }\n\n /**\n * Turn raw scores into labels and probabilities.\n *\n * @param scores One score array per row.\n * @returns Labels and probabilities in the shape the ONNX route uses,\n * so an app can swap runtimes without touching its own code. That\n * includes the label's **type**: an integer class comes back as a\n * number here exactly as ONNX returns it, because two routes over one\n * model that disagree on `0` versus `\"0\"` break the day someone\n * switches.\n */\n private finish(scores: readonly number[][]): {\n labels: PredictedLabel[];\n probabilities: number[][];\n } {\n const link = this.header.link;\n const classes = this.header.classes;\n\n if (link === \"identity\") {\n return { labels: scores.map((row) => row[0] as number), probabilities: [] };\n }\n\n const probabilities = scores.map((row) => {\n if (link === \"sigmoid\") {\n const positive = 1 / (1 + Math.exp(-(row[0] as number)));\n return [1 - positive, positive];\n }\n if (link === \"softmax\") {\n const highest = Math.max(...row);\n const exponentiated = row.map((value) => Math.exp(value - highest));\n const total = exponentiated.reduce((sum, value) => sum + value, 0);\n return exponentiated.map((value) => value / total);\n }\n const total = row.reduce((sum, value) => sum + value, 0);\n return total === 0 ? [...row] : row.map((value) => value / total);\n });\n\n const numeric = this.header.class_type !== \"str\";\n const labels = probabilities.map((row) => {\n let best = 0;\n for (let index = 1; index < row.length; index += 1) {\n if ((row[index] as number) > (row[best] as number)) best = index;\n }\n const label = classes[best];\n if (label === undefined) return best;\n return numeric ? Number(label) : label;\n });\n\n return { labels, probabilities };\n }\n}\n\n/**\n * Read a source into an `ArrayBuffer`.\n *\n * @param source A URL or the bytes.\n * @param requestInit `fetch` options.\n * @returns The bytes.\n * @throws {@link ModelFetchError} when the URL cannot be read.\n */\nasync function toBuffer(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n): Promise<ArrayBuffer> {\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not download the model: ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not download the model: ${response.status} ${response.statusText}`,\n );\n }\n return await response.arrayBuffer();\n }\n if (source instanceof Uint8Array) {\n return source.buffer.slice(\n source.byteOffset,\n source.byteOffset + source.byteLength,\n ) as ArrayBuffer;\n }\n return source;\n}\n\n/**\n * Check a batch before predicting on it.\n *\n * @param rows The batch.\n * @param expected Features the model wants per row.\n * @returns The batch width.\n * @throws {@link FeatureShapeError} when the batch cannot be predicted on.\n */\nfunction validateRows(rows: readonly FeatureRow[], expected: number): number {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\"predict() needs at least one row, shaped [[f1, f2, ...]].\");\n }\n const width = rows[0]?.length ?? 0;\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n if (width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n return width;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"compact.cjs","names":["CompactFormatError","ModelFetchError","FeatureShapeError","require_exceptions","MAGIC","CompactPredictor","header","arrays","info","buffer","toBuffer","source","requestInit","bytes","headerLength","cursor","section","assertTreesTerminate","width","validateRows","rows","started","scores","row","prepared","preprocess","scaled","index","coef","intercept","features","outputs","output","total","base","feature","threshold","left","right","leaf","offsets","trees","totals","tree","node","column","slot","link","classes","probabilities","positive","highest","exponentiated","value","sum","numeric","best","label","response","error","nodes","broken","detail","start","end","lower","upper","ragged","expected"],"sources":["../../src/tabular/compact.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the compact model format: the header, the column\n * dictionary, the quantised weights and the decoder that reads them back. Writer and\n * reader in one file because a format whose two halves live apart drifts.\n */\n/**\n * Running a model with no inference runtime at all.\n *\n * ONNX in the browser costs a **25.6 MB WebAssembly runtime** (6.0 MB\n * gzipped) before the first prediction, while the model itself is a few\n * hundred kilobytes. For an app whose only model is tabular, that runtime\n * *is* the download — so this reader replaces it with 1.49 KB of\n * arithmetic (measured, brotli).\n *\n * A linear model is a dot product. A tree is a chain of comparisons. That\n * is the whole implementation; there is nothing here that a WebAssembly\n * kernel would do better at this size.\n *\n * The file it reads (`.tmc`, magic `TMC1`) is written by\n * `tempest-fastapi-sdk`'s `export_sklearn_to_compact`, which verifies the\n * bytes against scikit-learn's own predictions and refuses to write a file\n * that disagrees. **It is data, not code**: no `eval`, no generated\n * JavaScript, nothing a strict CSP forbids.\n *\n * The trade against {@link TabularPredictor}: ONNX covers every estimator,\n * this covers linear models and tree ensembles. Pick by what your app\n * already ships — a page that loads ONNX for vision pays nothing extra for\n * tabular ONNX.\n */\n\nimport { CompactFormatError, FeatureShapeError, ModelFetchError } from \"./exceptions\";\nimport type { FeatureRow, PredictedLabel, TabularPrediction } from \"./types\";\n\n/** Magic bytes opening every compact file. */\nconst MAGIC = \"TMC1\";\n\n/** Layout version this reader understands. */\nexport const SUPPORTED_COMPACT_SCHEMA = 1;\n\n/** What the file holds. */\nexport type CompactKind = \"linear\" | \"tree_ensemble\";\n\n/** How raw scores become probabilities. */\ntype CompactLink = \"softmax\" | \"sigmoid\" | \"normalize\" | \"identity\";\n\n/** One array stored after the header. */\ninterface CompactSection {\n readonly name: string;\n readonly dtype: \"float32\" | \"int32\";\n readonly length: number;\n}\n\n/** The parsed header. */\ninterface CompactHeader {\n readonly schema_version: number;\n readonly kind: CompactKind;\n readonly task: \"classification\" | \"regression\";\n readonly link: CompactLink;\n readonly classes: readonly string[];\n /** How scikit-learn typed those labels: ``int``, ``float`` or ``str``. */\n readonly class_type?: \"int\" | \"float\" | \"str\";\n readonly n_features: number;\n readonly n_outputs: number;\n readonly n_trees?: number;\n readonly estimator: string;\n readonly feature_names: readonly string[];\n readonly preprocess: { readonly offset: number[]; readonly scale: number[] } | null;\n readonly sections: readonly CompactSection[];\n}\n\n/** What a loaded compact model is. */\nexport interface CompactPredictorInfo {\n /** Which reader path the file uses. */\n readonly kind: CompactKind;\n /** Class labels in score-column order; empty for a regressor. */\n readonly classes: readonly string[];\n /** Values expected per row. */\n readonly numFeatures: number;\n /** Column order recorded at training time, when the export had one. */\n readonly featureNames: readonly string[];\n /** Trees in the ensemble; `0` for a linear model. */\n readonly numTrees: number;\n /** Whether the model produces class scores. */\n readonly isClassifier: boolean;\n /** Class name of the exported estimator. */\n readonly estimator: string;\n}\n\n/**\n * A compact model, loaded and ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await CompactPredictor.create(\"/models/risk.tmc\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class CompactPredictor {\n private constructor(\n private readonly header: CompactHeader,\n private readonly arrays: Record<string, Float32Array | Int32Array>,\n /** What is loaded. */\n public readonly info: CompactPredictorInfo,\n ) {}\n\n /**\n * Load a `.tmc` file.\n *\n * @param source A URL, or the bytes when the app already has them.\n * @param requestInit `fetch` options, when `source` is a URL.\n * @returns The loaded predictor.\n * @throws {@link ModelFetchError} when a URL cannot be read.\n * @throws {@link CompactFormatError} when the bytes are not a compact\n * model, or use a layout newer than this reader.\n */\n static async create(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n ): Promise<CompactPredictor> {\n const buffer = await toBuffer(source, requestInit);\n const bytes = new Uint8Array(buffer);\n\n if (String.fromCharCode(...bytes.subarray(0, 4)) !== MAGIC) {\n throw new CompactFormatError(\n \"This is not a compact model file: it does not start with \" +\n `\"${MAGIC}\". A .onnx file goes through TabularPredictor instead.`,\n );\n }\n\n const view = new DataView(buffer);\n const headerLength = view.getUint32(4, true);\n const header = JSON.parse(\n new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)),\n ) as CompactHeader;\n\n if (header.schema_version > SUPPORTED_COMPACT_SCHEMA) {\n throw new CompactFormatError(\n `This file uses compact layout ${header.schema_version}, and this ` +\n `SDK understands ${SUPPORTED_COMPACT_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving it.\",\n );\n }\n\n const arrays: Record<string, Float32Array | Int32Array> = {};\n let cursor = 8 + headerLength;\n for (const section of header.sections) {\n arrays[section.name] =\n section.dtype === \"float32\"\n ? new Float32Array(buffer, cursor, section.length)\n : new Int32Array(buffer, cursor, section.length);\n cursor += section.length * 4;\n }\n\n if (header.kind === \"tree_ensemble\") assertTreesTerminate(header, arrays);\n\n return new CompactPredictor(header, arrays, {\n kind: header.kind,\n classes: header.classes,\n numFeatures: header.n_features,\n featureNames: header.feature_names,\n numTrees: header.n_trees ?? 0,\n isClassifier: header.task === \"classification\",\n estimator: header.estimator,\n });\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model is a classifier, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n const width = validateRows(rows, this.header.n_features);\n const started = performance.now();\n\n const scores: number[][] = [];\n for (const row of rows) {\n const prepared = this.preprocess(row, width);\n scores.push(\n this.header.kind === \"linear\"\n ? this.linearScores(prepared)\n : this.treeScores(prepared),\n );\n }\n\n const { labels, probabilities } = this.finish(scores);\n return {\n labels,\n probabilities,\n numRows: rows.length,\n ms: performance.now() - started,\n };\n }\n\n /** Releasing nothing, so callers can swap predictors without branching. */\n async dispose(): Promise<void> {\n /* no runtime, nothing to release */\n }\n\n /**\n * Apply the folded scaler, when the export had one.\n *\n * @param row The raw feature values.\n * @param width How many there are.\n * @returns The values the model was trained on.\n */\n private preprocess(row: FeatureRow, width: number): number[] {\n const preprocess = this.header.preprocess;\n if (preprocess === null) return row as number[];\n const scaled = new Array<number>(width);\n for (let index = 0; index < width; index += 1) {\n scaled[index] =\n ((row[index] as number) - (preprocess.offset[index] as number)) /\n (preprocess.scale[index] as number);\n }\n return scaled;\n }\n\n /**\n * Score one row against the coefficient matrix.\n *\n * @param row The prepared feature values.\n * @returns One raw score per output.\n */\n private linearScores(row: readonly number[]): number[] {\n const coef = this.arrays.coef as Float32Array;\n const intercept = this.arrays.intercept as Float32Array;\n const features = this.header.n_features;\n const outputs = this.header.n_outputs;\n\n const scores = new Array<number>(outputs);\n for (let output = 0; output < outputs; output += 1) {\n let total = intercept[output] as number;\n const base = output * features;\n for (let index = 0; index < features; index += 1) {\n total += (coef[base + index] as number) * (row[index] as number);\n }\n scores[output] = total;\n }\n return scores;\n }\n\n /**\n * Walk every tree and average what the leaves hold.\n *\n * A leaf is marked by a negative `feature` entry, which also carries\n * its slot in the value array — so the walk needs no second lookup and\n * the file stores values only for leaves.\n *\n * **The comparison runs in float32** (`Math.fround`), because that is\n * what scikit-learn does: it casts its input to float32 before\n * traversing, so a threshold like 5.099999904632568 — a float32 value\n * widened for storage — and an input of 5.1 compare *equal* and go\n * left. Comparing in float64 sends that row right instead, which on an\n * iris forest changed one tree's vote and moved a probability by 0.05.\n *\n * @param row The prepared feature values.\n * @returns One averaged score per output.\n */\n private treeScores(row: readonly number[]): number[] {\n const feature = this.arrays.node_feature as Int32Array;\n const threshold = this.arrays.node_threshold as Float32Array;\n const left = this.arrays.node_left as Int32Array;\n const right = this.arrays.node_right as Int32Array;\n const leaf = this.arrays.leaf_value as Float32Array;\n const offsets = this.arrays.tree_offset as Int32Array;\n const outputs = this.header.n_outputs;\n const trees = offsets.length - 1;\n\n const totals = new Array<number>(outputs).fill(0);\n for (let tree = 0; tree < trees; tree += 1) {\n let node = offsets[tree] as number;\n let column = feature[node] as number;\n while (column >= 0) {\n node =\n Math.fround(row[column] as number) <= (threshold[node] as number)\n ? (left[node] as number)\n : (right[node] as number);\n column = feature[node] as number;\n }\n const slot = (-1 - column) * outputs;\n for (let output = 0; output < outputs; output += 1) {\n totals[output] += leaf[slot + output] as number;\n }\n }\n for (let output = 0; output < outputs; output += 1) {\n totals[output] = (totals[output] as number) / trees;\n }\n return totals;\n }\n\n /**\n * Turn raw scores into labels and probabilities.\n *\n * @param scores One score array per row.\n * @returns Labels and probabilities in the shape the ONNX route uses,\n * so an app can swap runtimes without touching its own code. That\n * includes the label's **type**: an integer class comes back as a\n * number here exactly as ONNX returns it, because two routes over one\n * model that disagree on `0` versus `\"0\"` break the day someone\n * switches.\n */\n private finish(scores: readonly number[][]): {\n labels: PredictedLabel[];\n probabilities: number[][];\n } {\n const link = this.header.link;\n const classes = this.header.classes;\n\n if (link === \"identity\") {\n return { labels: scores.map((row) => row[0] as number), probabilities: [] };\n }\n\n const probabilities = scores.map((row) => {\n if (link === \"sigmoid\") {\n const positive = 1 / (1 + Math.exp(-(row[0] as number)));\n return [1 - positive, positive];\n }\n if (link === \"softmax\") {\n const highest = Math.max(...row);\n const exponentiated = row.map((value) => Math.exp(value - highest));\n const total = exponentiated.reduce((sum, value) => sum + value, 0);\n return exponentiated.map((value) => value / total);\n }\n const total = row.reduce((sum, value) => sum + value, 0);\n return total === 0 ? [...row] : row.map((value) => value / total);\n });\n\n const numeric = this.header.class_type !== \"str\";\n const labels = probabilities.map((row) => {\n let best = 0;\n for (let index = 1; index < row.length; index += 1) {\n if ((row[index] as number) > (row[best] as number)) best = index;\n }\n const label = classes[best];\n if (label === undefined) return best;\n return numeric ? Number(label) : label;\n });\n\n return { labels, probabilities };\n }\n}\n\n/**\n * Read a source into an `ArrayBuffer`.\n *\n * @param source A URL or the bytes.\n * @param requestInit `fetch` options.\n * @returns The bytes.\n * @throws {@link ModelFetchError} when the URL cannot be read.\n */\nasync function toBuffer(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n): Promise<ArrayBuffer> {\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not download the model: ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not download the model: ${response.status} ${response.statusText}`,\n );\n }\n return await response.arrayBuffer();\n }\n if (source instanceof Uint8Array) {\n return source.buffer.slice(\n source.byteOffset,\n source.byteOffset + source.byteLength,\n ) as ArrayBuffer;\n }\n return source;\n}\n\n/**\n * Prove once, at load, that every tree walk ends inside its arrays.\n *\n * The walk in `treeScores` follows `node_left`/`node_right` until it meets a\n * leaf, with no step limit — a limit there would be paid on every node of every\n * row. So a file whose child index points back up the tree spun the tab forever\n * on the first `predict`, and one pointing past the arrays read `undefined` and\n * answered `NaN`. scikit-learn numbers a node before its children, and\n * `export_sklearn_to_compact` keeps that order with each tree's nodes contiguous,\n * so a well-formed file satisfies `node < child < next tree's offset` everywhere\n * — measured on every tree fixture in `__fixtures__` (1 761 nodes, 0 violations).\n * Checking that invariant is linear in the node count and rules out a cycle by\n * construction, since every step strictly increases the index.\n *\n * @param header The parsed header.\n * @param arrays The sections, by name.\n * @throws {@link CompactFormatError} when a tree could loop, leave its arrays,\n * read a feature the model does not have, or a leaf slot past the values.\n */\nfunction assertTreesTerminate(\n header: CompactHeader,\n arrays: Record<string, Float32Array | Int32Array>,\n): void {\n const feature = arrays.node_feature;\n const left = arrays.node_left;\n const right = arrays.node_right;\n const leaf = arrays.leaf_value;\n const offsets = arrays.tree_offset;\n if (!feature || !left || !right || !leaf || !offsets || offsets.length < 2) {\n throw new CompactFormatError(\"This tree model is missing its node or offset sections.\");\n }\n const nodes = feature.length;\n const outputs = header.n_outputs;\n const broken = (detail: string): CompactFormatError =>\n new CompactFormatError(`This tree model is malformed: ${detail}. Re-export it.`);\n\n if (left.length !== nodes || right.length !== nodes || offsets[0] !== 0) {\n throw broken(\"the node sections disagree on the node count\");\n }\n if (offsets[offsets.length - 1] !== nodes) {\n throw broken(\"the last tree does not end at the last node\");\n }\n for (let tree = 0; tree < offsets.length - 1; tree += 1) {\n const start = offsets[tree] as number;\n const end = offsets[tree + 1] as number;\n if (end <= start) throw broken(`tree ${tree} has no nodes`);\n for (let node = start; node < end; node += 1) {\n const column = feature[node] as number;\n if (column >= 0) {\n const lower = left[node] as number;\n const upper = right[node] as number;\n if (column >= header.n_features) {\n throw broken(`node ${node} splits on feature ${column}`);\n }\n if (lower <= node || upper <= node || lower >= end || upper >= end) {\n throw broken(`node ${node} points outside its tree or back up it`);\n }\n } else if ((-1 - column) * outputs + outputs > leaf.length) {\n throw broken(`leaf ${node} reads past the leaf values`);\n }\n }\n }\n}\n\n/**\n * Check a batch before predicting on it.\n *\n * @param rows The batch.\n * @param expected Features the model wants per row.\n * @returns The batch width.\n * @throws {@link FeatureShapeError} when the batch cannot be predicted on.\n */\nfunction validateRows(rows: readonly FeatureRow[], expected: number): number {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\"predict() needs at least one row, shaped [[f1, f2, ...]].\");\n }\n const width = rows[0]?.length ?? 0;\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n if (width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n return width;\n}\n"],"mappings":"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"}
|
package/dist/tabular/compact.js
CHANGED
|
@@ -8,24 +8,24 @@ var r = "TMC1", i = class t {
|
|
|
8
8
|
this.header = e, this.arrays = t, this.info = n;
|
|
9
9
|
}
|
|
10
10
|
static async create(n, i) {
|
|
11
|
-
let
|
|
12
|
-
if (String.fromCharCode(...
|
|
13
|
-
let
|
|
14
|
-
if (
|
|
15
|
-
let
|
|
16
|
-
for (let e of
|
|
17
|
-
return new t(
|
|
18
|
-
kind:
|
|
19
|
-
classes:
|
|
20
|
-
numFeatures:
|
|
21
|
-
featureNames:
|
|
22
|
-
numTrees:
|
|
23
|
-
isClassifier:
|
|
24
|
-
estimator:
|
|
11
|
+
let s = await a(n, i), c = new Uint8Array(s);
|
|
12
|
+
if (String.fromCharCode(...c.subarray(0, 4)) !== r) throw new e(`This is not a compact model file: it does not start with "${r}". A .onnx file goes through TabularPredictor instead.`);
|
|
13
|
+
let l = new DataView(s).getUint32(4, !0), u = JSON.parse(new TextDecoder().decode(c.subarray(8, 8 + l)));
|
|
14
|
+
if (u.schema_version > 1) throw new e(`This file uses compact layout ${u.schema_version}, and this SDK understands 1. Upgrade tempest-react-sdk before serving it.`);
|
|
15
|
+
let d = {}, f = 8 + l;
|
|
16
|
+
for (let e of u.sections) d[e.name] = e.dtype === "float32" ? new Float32Array(s, f, e.length) : new Int32Array(s, f, e.length), f += e.length * 4;
|
|
17
|
+
return u.kind === "tree_ensemble" && o(u, d), new t(u, d, {
|
|
18
|
+
kind: u.kind,
|
|
19
|
+
classes: u.classes,
|
|
20
|
+
numFeatures: u.n_features,
|
|
21
|
+
featureNames: u.feature_names,
|
|
22
|
+
numTrees: u.n_trees ?? 0,
|
|
23
|
+
isClassifier: u.task === "classification",
|
|
24
|
+
estimator: u.estimator
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
async predict(e) {
|
|
28
|
-
let t =
|
|
28
|
+
let t = s(e, this.header.n_features), n = performance.now(), r = [];
|
|
29
29
|
for (let n of e) {
|
|
30
30
|
let e = this.preprocess(n, t);
|
|
31
31
|
r.push(this.header.kind === "linear" ? this.linearScores(e) : this.treeScores(e));
|
|
@@ -108,7 +108,26 @@ async function a(e, t) {
|
|
|
108
108
|
}
|
|
109
109
|
return e instanceof Uint8Array ? e.buffer.slice(e.byteOffset, e.byteOffset + e.byteLength) : e;
|
|
110
110
|
}
|
|
111
|
-
function o(
|
|
111
|
+
function o(t, n) {
|
|
112
|
+
let r = n.node_feature, i = n.node_left, a = n.node_right, o = n.leaf_value, s = n.tree_offset;
|
|
113
|
+
if (!r || !i || !a || !o || !s || s.length < 2) throw new e("This tree model is missing its node or offset sections.");
|
|
114
|
+
let c = r.length, l = t.n_outputs, u = (t) => new e(`This tree model is malformed: ${t}. Re-export it.`);
|
|
115
|
+
if (i.length !== c || a.length !== c || s[0] !== 0) throw u("the node sections disagree on the node count");
|
|
116
|
+
if (s[s.length - 1] !== c) throw u("the last tree does not end at the last node");
|
|
117
|
+
for (let e = 0; e < s.length - 1; e += 1) {
|
|
118
|
+
let n = s[e], c = s[e + 1];
|
|
119
|
+
if (c <= n) throw u(`tree ${e} has no nodes`);
|
|
120
|
+
for (let e = n; e < c; e += 1) {
|
|
121
|
+
let n = r[e];
|
|
122
|
+
if (n >= 0) {
|
|
123
|
+
let r = i[e], o = a[e];
|
|
124
|
+
if (n >= t.n_features) throw u(`node ${e} splits on feature ${n}`);
|
|
125
|
+
if (r <= e || o <= e || r >= c || o >= c) throw u(`node ${e} points outside its tree or back up it`);
|
|
126
|
+
} else if ((-1 - n) * l + l > o.length) throw u(`leaf ${e} reads past the leaf values`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function s(e, n) {
|
|
112
131
|
if (!Array.isArray(e) || e.length === 0) throw new t("predict() needs at least one row, shaped [[f1, f2, ...]].");
|
|
113
132
|
let r = e[0]?.length ?? 0, i = e.findIndex((e) => e.length !== r);
|
|
114
133
|
if (i !== -1) throw new t(`All rows must have the same width; row ${i} has ${e[i]?.length} values, expected ${r}.`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compact.js","names":["MAGIC","CompactPredictor","header","arrays","info","buffer","toBuffer","source","requestInit","bytes","CompactFormatError","headerLength","cursor","section","width","validateRows","rows","started","scores","row","prepared","preprocess","scaled","index","coef","intercept","features","outputs","output","total","base","feature","threshold","left","right","leaf","offsets","trees","totals","tree","node","column","slot","link","classes","probabilities","positive","highest","exponentiated","value","sum","numeric","best","label","response","error","ModelFetchError","FeatureShapeError","ragged","expected"],"sources":["../../src/tabular/compact.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the compact model format: the header, the column\n * dictionary, the quantised weights and the decoder that reads them back. Writer and\n * reader in one file because a format whose two halves live apart drifts.\n */\n/**\n * Running a model with no inference runtime at all.\n *\n * ONNX in the browser costs a **25.6 MB WebAssembly runtime** (6.0 MB\n * gzipped) before the first prediction, while the model itself is a few\n * hundred kilobytes. For an app whose only model is tabular, that runtime\n * *is* the download — so this reader replaces it with 1.49 KB of\n * arithmetic (measured, brotli).\n *\n * A linear model is a dot product. A tree is a chain of comparisons. That\n * is the whole implementation; there is nothing here that a WebAssembly\n * kernel would do better at this size.\n *\n * The file it reads (`.tmc`, magic `TMC1`) is written by\n * `tempest-fastapi-sdk`'s `export_sklearn_to_compact`, which verifies the\n * bytes against scikit-learn's own predictions and refuses to write a file\n * that disagrees. **It is data, not code**: no `eval`, no generated\n * JavaScript, nothing a strict CSP forbids.\n *\n * The trade against {@link TabularPredictor}: ONNX covers every estimator,\n * this covers linear models and tree ensembles. Pick by what your app\n * already ships — a page that loads ONNX for vision pays nothing extra for\n * tabular ONNX.\n */\n\nimport { CompactFormatError, FeatureShapeError, ModelFetchError } from \"./exceptions\";\nimport type { FeatureRow, PredictedLabel, TabularPrediction } from \"./types\";\n\n/** Magic bytes opening every compact file. */\nconst MAGIC = \"TMC1\";\n\n/** Layout version this reader understands. */\nexport const SUPPORTED_COMPACT_SCHEMA = 1;\n\n/** What the file holds. */\nexport type CompactKind = \"linear\" | \"tree_ensemble\";\n\n/** How raw scores become probabilities. */\ntype CompactLink = \"softmax\" | \"sigmoid\" | \"normalize\" | \"identity\";\n\n/** One array stored after the header. */\ninterface CompactSection {\n readonly name: string;\n readonly dtype: \"float32\" | \"int32\";\n readonly length: number;\n}\n\n/** The parsed header. */\ninterface CompactHeader {\n readonly schema_version: number;\n readonly kind: CompactKind;\n readonly task: \"classification\" | \"regression\";\n readonly link: CompactLink;\n readonly classes: readonly string[];\n /** How scikit-learn typed those labels: ``int``, ``float`` or ``str``. */\n readonly class_type?: \"int\" | \"float\" | \"str\";\n readonly n_features: number;\n readonly n_outputs: number;\n readonly n_trees?: number;\n readonly estimator: string;\n readonly feature_names: readonly string[];\n readonly preprocess: { readonly offset: number[]; readonly scale: number[] } | null;\n readonly sections: readonly CompactSection[];\n}\n\n/** What a loaded compact model is. */\nexport interface CompactPredictorInfo {\n /** Which reader path the file uses. */\n readonly kind: CompactKind;\n /** Class labels in score-column order; empty for a regressor. */\n readonly classes: readonly string[];\n /** Values expected per row. */\n readonly numFeatures: number;\n /** Column order recorded at training time, when the export had one. */\n readonly featureNames: readonly string[];\n /** Trees in the ensemble; `0` for a linear model. */\n readonly numTrees: number;\n /** Whether the model produces class scores. */\n readonly isClassifier: boolean;\n /** Class name of the exported estimator. */\n readonly estimator: string;\n}\n\n/**\n * A compact model, loaded and ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await CompactPredictor.create(\"/models/risk.tmc\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class CompactPredictor {\n private constructor(\n private readonly header: CompactHeader,\n private readonly arrays: Record<string, Float32Array | Int32Array>,\n /** What is loaded. */\n public readonly info: CompactPredictorInfo,\n ) {}\n\n /**\n * Load a `.tmc` file.\n *\n * @param source A URL, or the bytes when the app already has them.\n * @param requestInit `fetch` options, when `source` is a URL.\n * @returns The loaded predictor.\n * @throws {@link ModelFetchError} when a URL cannot be read.\n * @throws {@link CompactFormatError} when the bytes are not a compact\n * model, or use a layout newer than this reader.\n */\n static async create(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n ): Promise<CompactPredictor> {\n const buffer = await toBuffer(source, requestInit);\n const bytes = new Uint8Array(buffer);\n\n if (String.fromCharCode(...bytes.subarray(0, 4)) !== MAGIC) {\n throw new CompactFormatError(\n \"This is not a compact model file: it does not start with \" +\n `\"${MAGIC}\". A .onnx file goes through TabularPredictor instead.`,\n );\n }\n\n const view = new DataView(buffer);\n const headerLength = view.getUint32(4, true);\n const header = JSON.parse(\n new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)),\n ) as CompactHeader;\n\n if (header.schema_version > SUPPORTED_COMPACT_SCHEMA) {\n throw new CompactFormatError(\n `This file uses compact layout ${header.schema_version}, and this ` +\n `SDK understands ${SUPPORTED_COMPACT_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving it.\",\n );\n }\n\n const arrays: Record<string, Float32Array | Int32Array> = {};\n let cursor = 8 + headerLength;\n for (const section of header.sections) {\n arrays[section.name] =\n section.dtype === \"float32\"\n ? new Float32Array(buffer, cursor, section.length)\n : new Int32Array(buffer, cursor, section.length);\n cursor += section.length * 4;\n }\n\n return new CompactPredictor(header, arrays, {\n kind: header.kind,\n classes: header.classes,\n numFeatures: header.n_features,\n featureNames: header.feature_names,\n numTrees: header.n_trees ?? 0,\n isClassifier: header.task === \"classification\",\n estimator: header.estimator,\n });\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model is a classifier, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n const width = validateRows(rows, this.header.n_features);\n const started = performance.now();\n\n const scores: number[][] = [];\n for (const row of rows) {\n const prepared = this.preprocess(row, width);\n scores.push(\n this.header.kind === \"linear\"\n ? this.linearScores(prepared)\n : this.treeScores(prepared),\n );\n }\n\n const { labels, probabilities } = this.finish(scores);\n return {\n labels,\n probabilities,\n numRows: rows.length,\n ms: performance.now() - started,\n };\n }\n\n /** Releasing nothing, so callers can swap predictors without branching. */\n async dispose(): Promise<void> {\n /* no runtime, nothing to release */\n }\n\n /**\n * Apply the folded scaler, when the export had one.\n *\n * @param row The raw feature values.\n * @param width How many there are.\n * @returns The values the model was trained on.\n */\n private preprocess(row: FeatureRow, width: number): number[] {\n const preprocess = this.header.preprocess;\n if (preprocess === null) return row as number[];\n const scaled = new Array<number>(width);\n for (let index = 0; index < width; index += 1) {\n scaled[index] =\n ((row[index] as number) - (preprocess.offset[index] as number)) /\n (preprocess.scale[index] as number);\n }\n return scaled;\n }\n\n /**\n * Score one row against the coefficient matrix.\n *\n * @param row The prepared feature values.\n * @returns One raw score per output.\n */\n private linearScores(row: readonly number[]): number[] {\n const coef = this.arrays.coef as Float32Array;\n const intercept = this.arrays.intercept as Float32Array;\n const features = this.header.n_features;\n const outputs = this.header.n_outputs;\n\n const scores = new Array<number>(outputs);\n for (let output = 0; output < outputs; output += 1) {\n let total = intercept[output] as number;\n const base = output * features;\n for (let index = 0; index < features; index += 1) {\n total += (coef[base + index] as number) * (row[index] as number);\n }\n scores[output] = total;\n }\n return scores;\n }\n\n /**\n * Walk every tree and average what the leaves hold.\n *\n * A leaf is marked by a negative `feature` entry, which also carries\n * its slot in the value array — so the walk needs no second lookup and\n * the file stores values only for leaves.\n *\n * **The comparison runs in float32** (`Math.fround`), because that is\n * what scikit-learn does: it casts its input to float32 before\n * traversing, so a threshold like 5.099999904632568 — a float32 value\n * widened for storage — and an input of 5.1 compare *equal* and go\n * left. Comparing in float64 sends that row right instead, which on an\n * iris forest changed one tree's vote and moved a probability by 0.05.\n *\n * @param row The prepared feature values.\n * @returns One averaged score per output.\n */\n private treeScores(row: readonly number[]): number[] {\n const feature = this.arrays.node_feature as Int32Array;\n const threshold = this.arrays.node_threshold as Float32Array;\n const left = this.arrays.node_left as Int32Array;\n const right = this.arrays.node_right as Int32Array;\n const leaf = this.arrays.leaf_value as Float32Array;\n const offsets = this.arrays.tree_offset as Int32Array;\n const outputs = this.header.n_outputs;\n const trees = offsets.length - 1;\n\n const totals = new Array<number>(outputs).fill(0);\n for (let tree = 0; tree < trees; tree += 1) {\n let node = offsets[tree] as number;\n let column = feature[node] as number;\n while (column >= 0) {\n node =\n Math.fround(row[column] as number) <= (threshold[node] as number)\n ? (left[node] as number)\n : (right[node] as number);\n column = feature[node] as number;\n }\n const slot = (-1 - column) * outputs;\n for (let output = 0; output < outputs; output += 1) {\n totals[output] += leaf[slot + output] as number;\n }\n }\n for (let output = 0; output < outputs; output += 1) {\n totals[output] = (totals[output] as number) / trees;\n }\n return totals;\n }\n\n /**\n * Turn raw scores into labels and probabilities.\n *\n * @param scores One score array per row.\n * @returns Labels and probabilities in the shape the ONNX route uses,\n * so an app can swap runtimes without touching its own code. That\n * includes the label's **type**: an integer class comes back as a\n * number here exactly as ONNX returns it, because two routes over one\n * model that disagree on `0` versus `\"0\"` break the day someone\n * switches.\n */\n private finish(scores: readonly number[][]): {\n labels: PredictedLabel[];\n probabilities: number[][];\n } {\n const link = this.header.link;\n const classes = this.header.classes;\n\n if (link === \"identity\") {\n return { labels: scores.map((row) => row[0] as number), probabilities: [] };\n }\n\n const probabilities = scores.map((row) => {\n if (link === \"sigmoid\") {\n const positive = 1 / (1 + Math.exp(-(row[0] as number)));\n return [1 - positive, positive];\n }\n if (link === \"softmax\") {\n const highest = Math.max(...row);\n const exponentiated = row.map((value) => Math.exp(value - highest));\n const total = exponentiated.reduce((sum, value) => sum + value, 0);\n return exponentiated.map((value) => value / total);\n }\n const total = row.reduce((sum, value) => sum + value, 0);\n return total === 0 ? [...row] : row.map((value) => value / total);\n });\n\n const numeric = this.header.class_type !== \"str\";\n const labels = probabilities.map((row) => {\n let best = 0;\n for (let index = 1; index < row.length; index += 1) {\n if ((row[index] as number) > (row[best] as number)) best = index;\n }\n const label = classes[best];\n if (label === undefined) return best;\n return numeric ? Number(label) : label;\n });\n\n return { labels, probabilities };\n }\n}\n\n/**\n * Read a source into an `ArrayBuffer`.\n *\n * @param source A URL or the bytes.\n * @param requestInit `fetch` options.\n * @returns The bytes.\n * @throws {@link ModelFetchError} when the URL cannot be read.\n */\nasync function toBuffer(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n): Promise<ArrayBuffer> {\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not download the model: ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not download the model: ${response.status} ${response.statusText}`,\n );\n }\n return await response.arrayBuffer();\n }\n if (source instanceof Uint8Array) {\n return source.buffer.slice(\n source.byteOffset,\n source.byteOffset + source.byteLength,\n ) as ArrayBuffer;\n }\n return source;\n}\n\n/**\n * Check a batch before predicting on it.\n *\n * @param rows The batch.\n * @param expected Features the model wants per row.\n * @returns The batch width.\n * @throws {@link FeatureShapeError} when the batch cannot be predicted on.\n */\nfunction validateRows(rows: readonly FeatureRow[], expected: number): number {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\"predict() needs at least one row, shaped [[f1, f2, ...]].\");\n }\n const width = rows[0]?.length ?? 0;\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n if (width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n return width;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"compact.js","names":["MAGIC","CompactPredictor","header","arrays","info","buffer","toBuffer","source","requestInit","bytes","CompactFormatError","headerLength","cursor","section","assertTreesTerminate","width","validateRows","rows","started","scores","row","prepared","preprocess","scaled","index","coef","intercept","features","outputs","output","total","base","feature","threshold","left","right","leaf","offsets","trees","totals","tree","node","column","slot","link","classes","probabilities","positive","highest","exponentiated","value","sum","numeric","best","label","response","error","ModelFetchError","nodes","broken","detail","start","end","lower","upper","FeatureShapeError","ragged","expected"],"sources":["../../src/tabular/compact.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the compact model format: the header, the column\n * dictionary, the quantised weights and the decoder that reads them back. Writer and\n * reader in one file because a format whose two halves live apart drifts.\n */\n/**\n * Running a model with no inference runtime at all.\n *\n * ONNX in the browser costs a **25.6 MB WebAssembly runtime** (6.0 MB\n * gzipped) before the first prediction, while the model itself is a few\n * hundred kilobytes. For an app whose only model is tabular, that runtime\n * *is* the download — so this reader replaces it with 1.49 KB of\n * arithmetic (measured, brotli).\n *\n * A linear model is a dot product. A tree is a chain of comparisons. That\n * is the whole implementation; there is nothing here that a WebAssembly\n * kernel would do better at this size.\n *\n * The file it reads (`.tmc`, magic `TMC1`) is written by\n * `tempest-fastapi-sdk`'s `export_sklearn_to_compact`, which verifies the\n * bytes against scikit-learn's own predictions and refuses to write a file\n * that disagrees. **It is data, not code**: no `eval`, no generated\n * JavaScript, nothing a strict CSP forbids.\n *\n * The trade against {@link TabularPredictor}: ONNX covers every estimator,\n * this covers linear models and tree ensembles. Pick by what your app\n * already ships — a page that loads ONNX for vision pays nothing extra for\n * tabular ONNX.\n */\n\nimport { CompactFormatError, FeatureShapeError, ModelFetchError } from \"./exceptions\";\nimport type { FeatureRow, PredictedLabel, TabularPrediction } from \"./types\";\n\n/** Magic bytes opening every compact file. */\nconst MAGIC = \"TMC1\";\n\n/** Layout version this reader understands. */\nexport const SUPPORTED_COMPACT_SCHEMA = 1;\n\n/** What the file holds. */\nexport type CompactKind = \"linear\" | \"tree_ensemble\";\n\n/** How raw scores become probabilities. */\ntype CompactLink = \"softmax\" | \"sigmoid\" | \"normalize\" | \"identity\";\n\n/** One array stored after the header. */\ninterface CompactSection {\n readonly name: string;\n readonly dtype: \"float32\" | \"int32\";\n readonly length: number;\n}\n\n/** The parsed header. */\ninterface CompactHeader {\n readonly schema_version: number;\n readonly kind: CompactKind;\n readonly task: \"classification\" | \"regression\";\n readonly link: CompactLink;\n readonly classes: readonly string[];\n /** How scikit-learn typed those labels: ``int``, ``float`` or ``str``. */\n readonly class_type?: \"int\" | \"float\" | \"str\";\n readonly n_features: number;\n readonly n_outputs: number;\n readonly n_trees?: number;\n readonly estimator: string;\n readonly feature_names: readonly string[];\n readonly preprocess: { readonly offset: number[]; readonly scale: number[] } | null;\n readonly sections: readonly CompactSection[];\n}\n\n/** What a loaded compact model is. */\nexport interface CompactPredictorInfo {\n /** Which reader path the file uses. */\n readonly kind: CompactKind;\n /** Class labels in score-column order; empty for a regressor. */\n readonly classes: readonly string[];\n /** Values expected per row. */\n readonly numFeatures: number;\n /** Column order recorded at training time, when the export had one. */\n readonly featureNames: readonly string[];\n /** Trees in the ensemble; `0` for a linear model. */\n readonly numTrees: number;\n /** Whether the model produces class scores. */\n readonly isClassifier: boolean;\n /** Class name of the exported estimator. */\n readonly estimator: string;\n}\n\n/**\n * A compact model, loaded and ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await CompactPredictor.create(\"/models/risk.tmc\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class CompactPredictor {\n private constructor(\n private readonly header: CompactHeader,\n private readonly arrays: Record<string, Float32Array | Int32Array>,\n /** What is loaded. */\n public readonly info: CompactPredictorInfo,\n ) {}\n\n /**\n * Load a `.tmc` file.\n *\n * @param source A URL, or the bytes when the app already has them.\n * @param requestInit `fetch` options, when `source` is a URL.\n * @returns The loaded predictor.\n * @throws {@link ModelFetchError} when a URL cannot be read.\n * @throws {@link CompactFormatError} when the bytes are not a compact\n * model, or use a layout newer than this reader.\n */\n static async create(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n ): Promise<CompactPredictor> {\n const buffer = await toBuffer(source, requestInit);\n const bytes = new Uint8Array(buffer);\n\n if (String.fromCharCode(...bytes.subarray(0, 4)) !== MAGIC) {\n throw new CompactFormatError(\n \"This is not a compact model file: it does not start with \" +\n `\"${MAGIC}\". A .onnx file goes through TabularPredictor instead.`,\n );\n }\n\n const view = new DataView(buffer);\n const headerLength = view.getUint32(4, true);\n const header = JSON.parse(\n new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)),\n ) as CompactHeader;\n\n if (header.schema_version > SUPPORTED_COMPACT_SCHEMA) {\n throw new CompactFormatError(\n `This file uses compact layout ${header.schema_version}, and this ` +\n `SDK understands ${SUPPORTED_COMPACT_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving it.\",\n );\n }\n\n const arrays: Record<string, Float32Array | Int32Array> = {};\n let cursor = 8 + headerLength;\n for (const section of header.sections) {\n arrays[section.name] =\n section.dtype === \"float32\"\n ? new Float32Array(buffer, cursor, section.length)\n : new Int32Array(buffer, cursor, section.length);\n cursor += section.length * 4;\n }\n\n if (header.kind === \"tree_ensemble\") assertTreesTerminate(header, arrays);\n\n return new CompactPredictor(header, arrays, {\n kind: header.kind,\n classes: header.classes,\n numFeatures: header.n_features,\n featureNames: header.feature_names,\n numTrees: header.n_trees ?? 0,\n isClassifier: header.task === \"classification\",\n estimator: header.estimator,\n });\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model is a classifier, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n const width = validateRows(rows, this.header.n_features);\n const started = performance.now();\n\n const scores: number[][] = [];\n for (const row of rows) {\n const prepared = this.preprocess(row, width);\n scores.push(\n this.header.kind === \"linear\"\n ? this.linearScores(prepared)\n : this.treeScores(prepared),\n );\n }\n\n const { labels, probabilities } = this.finish(scores);\n return {\n labels,\n probabilities,\n numRows: rows.length,\n ms: performance.now() - started,\n };\n }\n\n /** Releasing nothing, so callers can swap predictors without branching. */\n async dispose(): Promise<void> {\n /* no runtime, nothing to release */\n }\n\n /**\n * Apply the folded scaler, when the export had one.\n *\n * @param row The raw feature values.\n * @param width How many there are.\n * @returns The values the model was trained on.\n */\n private preprocess(row: FeatureRow, width: number): number[] {\n const preprocess = this.header.preprocess;\n if (preprocess === null) return row as number[];\n const scaled = new Array<number>(width);\n for (let index = 0; index < width; index += 1) {\n scaled[index] =\n ((row[index] as number) - (preprocess.offset[index] as number)) /\n (preprocess.scale[index] as number);\n }\n return scaled;\n }\n\n /**\n * Score one row against the coefficient matrix.\n *\n * @param row The prepared feature values.\n * @returns One raw score per output.\n */\n private linearScores(row: readonly number[]): number[] {\n const coef = this.arrays.coef as Float32Array;\n const intercept = this.arrays.intercept as Float32Array;\n const features = this.header.n_features;\n const outputs = this.header.n_outputs;\n\n const scores = new Array<number>(outputs);\n for (let output = 0; output < outputs; output += 1) {\n let total = intercept[output] as number;\n const base = output * features;\n for (let index = 0; index < features; index += 1) {\n total += (coef[base + index] as number) * (row[index] as number);\n }\n scores[output] = total;\n }\n return scores;\n }\n\n /**\n * Walk every tree and average what the leaves hold.\n *\n * A leaf is marked by a negative `feature` entry, which also carries\n * its slot in the value array — so the walk needs no second lookup and\n * the file stores values only for leaves.\n *\n * **The comparison runs in float32** (`Math.fround`), because that is\n * what scikit-learn does: it casts its input to float32 before\n * traversing, so a threshold like 5.099999904632568 — a float32 value\n * widened for storage — and an input of 5.1 compare *equal* and go\n * left. Comparing in float64 sends that row right instead, which on an\n * iris forest changed one tree's vote and moved a probability by 0.05.\n *\n * @param row The prepared feature values.\n * @returns One averaged score per output.\n */\n private treeScores(row: readonly number[]): number[] {\n const feature = this.arrays.node_feature as Int32Array;\n const threshold = this.arrays.node_threshold as Float32Array;\n const left = this.arrays.node_left as Int32Array;\n const right = this.arrays.node_right as Int32Array;\n const leaf = this.arrays.leaf_value as Float32Array;\n const offsets = this.arrays.tree_offset as Int32Array;\n const outputs = this.header.n_outputs;\n const trees = offsets.length - 1;\n\n const totals = new Array<number>(outputs).fill(0);\n for (let tree = 0; tree < trees; tree += 1) {\n let node = offsets[tree] as number;\n let column = feature[node] as number;\n while (column >= 0) {\n node =\n Math.fround(row[column] as number) <= (threshold[node] as number)\n ? (left[node] as number)\n : (right[node] as number);\n column = feature[node] as number;\n }\n const slot = (-1 - column) * outputs;\n for (let output = 0; output < outputs; output += 1) {\n totals[output] += leaf[slot + output] as number;\n }\n }\n for (let output = 0; output < outputs; output += 1) {\n totals[output] = (totals[output] as number) / trees;\n }\n return totals;\n }\n\n /**\n * Turn raw scores into labels and probabilities.\n *\n * @param scores One score array per row.\n * @returns Labels and probabilities in the shape the ONNX route uses,\n * so an app can swap runtimes without touching its own code. That\n * includes the label's **type**: an integer class comes back as a\n * number here exactly as ONNX returns it, because two routes over one\n * model that disagree on `0` versus `\"0\"` break the day someone\n * switches.\n */\n private finish(scores: readonly number[][]): {\n labels: PredictedLabel[];\n probabilities: number[][];\n } {\n const link = this.header.link;\n const classes = this.header.classes;\n\n if (link === \"identity\") {\n return { labels: scores.map((row) => row[0] as number), probabilities: [] };\n }\n\n const probabilities = scores.map((row) => {\n if (link === \"sigmoid\") {\n const positive = 1 / (1 + Math.exp(-(row[0] as number)));\n return [1 - positive, positive];\n }\n if (link === \"softmax\") {\n const highest = Math.max(...row);\n const exponentiated = row.map((value) => Math.exp(value - highest));\n const total = exponentiated.reduce((sum, value) => sum + value, 0);\n return exponentiated.map((value) => value / total);\n }\n const total = row.reduce((sum, value) => sum + value, 0);\n return total === 0 ? [...row] : row.map((value) => value / total);\n });\n\n const numeric = this.header.class_type !== \"str\";\n const labels = probabilities.map((row) => {\n let best = 0;\n for (let index = 1; index < row.length; index += 1) {\n if ((row[index] as number) > (row[best] as number)) best = index;\n }\n const label = classes[best];\n if (label === undefined) return best;\n return numeric ? Number(label) : label;\n });\n\n return { labels, probabilities };\n }\n}\n\n/**\n * Read a source into an `ArrayBuffer`.\n *\n * @param source A URL or the bytes.\n * @param requestInit `fetch` options.\n * @returns The bytes.\n * @throws {@link ModelFetchError} when the URL cannot be read.\n */\nasync function toBuffer(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n): Promise<ArrayBuffer> {\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not download the model: ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not download the model: ${response.status} ${response.statusText}`,\n );\n }\n return await response.arrayBuffer();\n }\n if (source instanceof Uint8Array) {\n return source.buffer.slice(\n source.byteOffset,\n source.byteOffset + source.byteLength,\n ) as ArrayBuffer;\n }\n return source;\n}\n\n/**\n * Prove once, at load, that every tree walk ends inside its arrays.\n *\n * The walk in `treeScores` follows `node_left`/`node_right` until it meets a\n * leaf, with no step limit — a limit there would be paid on every node of every\n * row. So a file whose child index points back up the tree spun the tab forever\n * on the first `predict`, and one pointing past the arrays read `undefined` and\n * answered `NaN`. scikit-learn numbers a node before its children, and\n * `export_sklearn_to_compact` keeps that order with each tree's nodes contiguous,\n * so a well-formed file satisfies `node < child < next tree's offset` everywhere\n * — measured on every tree fixture in `__fixtures__` (1 761 nodes, 0 violations).\n * Checking that invariant is linear in the node count and rules out a cycle by\n * construction, since every step strictly increases the index.\n *\n * @param header The parsed header.\n * @param arrays The sections, by name.\n * @throws {@link CompactFormatError} when a tree could loop, leave its arrays,\n * read a feature the model does not have, or a leaf slot past the values.\n */\nfunction assertTreesTerminate(\n header: CompactHeader,\n arrays: Record<string, Float32Array | Int32Array>,\n): void {\n const feature = arrays.node_feature;\n const left = arrays.node_left;\n const right = arrays.node_right;\n const leaf = arrays.leaf_value;\n const offsets = arrays.tree_offset;\n if (!feature || !left || !right || !leaf || !offsets || offsets.length < 2) {\n throw new CompactFormatError(\"This tree model is missing its node or offset sections.\");\n }\n const nodes = feature.length;\n const outputs = header.n_outputs;\n const broken = (detail: string): CompactFormatError =>\n new CompactFormatError(`This tree model is malformed: ${detail}. Re-export it.`);\n\n if (left.length !== nodes || right.length !== nodes || offsets[0] !== 0) {\n throw broken(\"the node sections disagree on the node count\");\n }\n if (offsets[offsets.length - 1] !== nodes) {\n throw broken(\"the last tree does not end at the last node\");\n }\n for (let tree = 0; tree < offsets.length - 1; tree += 1) {\n const start = offsets[tree] as number;\n const end = offsets[tree + 1] as number;\n if (end <= start) throw broken(`tree ${tree} has no nodes`);\n for (let node = start; node < end; node += 1) {\n const column = feature[node] as number;\n if (column >= 0) {\n const lower = left[node] as number;\n const upper = right[node] as number;\n if (column >= header.n_features) {\n throw broken(`node ${node} splits on feature ${column}`);\n }\n if (lower <= node || upper <= node || lower >= end || upper >= end) {\n throw broken(`node ${node} points outside its tree or back up it`);\n }\n } else if ((-1 - column) * outputs + outputs > leaf.length) {\n throw broken(`leaf ${node} reads past the leaf values`);\n }\n }\n }\n}\n\n/**\n * Check a batch before predicting on it.\n *\n * @param rows The batch.\n * @param expected Features the model wants per row.\n * @returns The batch width.\n * @throws {@link FeatureShapeError} when the batch cannot be predicted on.\n */\nfunction validateRows(rows: readonly FeatureRow[], expected: number): number {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\"predict() needs at least one row, shaped [[f1, f2, ...]].\");\n }\n const width = rows[0]?.length ?? 0;\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n if (width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n return width;\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./exceptions.cjs"),t=require("./cache.cjs"),n=require("./compact.cjs"),r=require("./predictor.cjs");var i=`manifest.json`;async function a(t,n){let r=
|
|
1
|
+
const e=require("./exceptions.cjs"),t=require("./cache.cjs"),n=require("./compact.cjs"),r=require("./predictor.cjs");var i=`manifest.json`;async function a(t,n){let r=d(t),i;try{i=await fetch(r,n)}catch(t){throw new e.ModelFetchError(`Could not read the manifest at ${r}`,{cause:t})}if(!i.ok)throw new e.ModelFetchError(`Could not read the manifest at ${r}: ${i.status} ${i.statusText}`);let a=await i.json();if(typeof a?.schema_version!=`number`)throw new e.ModelFetchError(`${r} is not an edge package manifest (no schema_version).`);if(a.schema_version>1)throw new e.ModelFetchError(`The manifest at ${r} uses schema_version ${a.schema_version}, and this SDK understands 1. Upgrade tempest-react-sdk before serving this package.`);return a}async function o(e,t={}){let i=await a(e),o=e.endsWith(`/`)?e:`${e}/`,l=s(i,t.runtime??`auto`),{file:d,sha256:f}=c(i,l),p=`${o}${d}`,m=t.cache??!0,h=m===!1?p:await u(p,f,typeof m==`object`?m:{}),g=l===`compact`?await n.CompactPredictor.create(h):await r.TabularPredictor.create(h,{providers:t.providers,warmup:t.warmup,sessionOptions:t.sessionOptions}),_=i.output.classes;return{manifest:i,predictor:g,runtime:l,featureNames:i.input.feature_names,classes:_,explain:e=>e.map((e,t)=>({name:_[t]??String(t),score:e})).sort((e,t)=>t.score-e.score)}}function s(t,n){let r=t.runtimes??[],i=r.some(e=>e.kind===`compact`);if(n===`auto`)return i?`compact`:`onnx`;if(n===`compact`&&!i)throw new e.ModelFetchError(`This package carries no compact model (${t.name} lists ${r.map(e=>e.kind).join(`, `)||`onnx`}). Re-export it with edge_pipeline(compact=True), or load it as ONNX.`);return n}function c(e,t){return(e.runtimes??[]).find(e=>e.kind===t)??e.model}async function l(e,t){let n=globalThis.crypto?.subtle;if(!n||!t)return!0;let r=new Uint8Array(await n.digest(`SHA-256`,e));return Array.from(r,e=>e.toString(16).padStart(2,`0`)).join(``)===t.toLowerCase()}async function u(n,r,i){let a=await t.fetchModelBytes(n,i);if(await l(a,r))return a;if(i.revalidate!==!0){let e=await t.fetchModelBytes(n,{...i,revalidate:!0});if(await l(e,r))return e}throw new e.ModelFetchError(`The model at ${n} does not match the sha256 in its manifest. The file and the manifest were published separately, or the download was altered — republish the package so both describe the same model.`)}function d(e){return e.endsWith(`.json`)?e:e.endsWith(`/`)?`${e}${i}`:`${e}/${i}`}exports.MANIFEST_FILENAME=i,exports.fetchEdgeManifest=a,exports.loadEdgePackage=o;
|
|
2
2
|
//# sourceMappingURL=manifest.cjs.map
|