@printhive/react 0.0.0-stage → 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/FilamentManagement.tsx","../src/presentation.ts","../src/provider.tsx","../src/queries.ts","../src/SlotEditor.tsx","../src/CatalogPicker.tsx","../src/mutations.ts","../src/http-data-source.ts"],"sourcesContent":["import { useState } from \"react\";\nimport {\n QueryClientProvider,\n useIsMutating,\n useQuery,\n type UseQueryResult,\n} from \"@tanstack/react-query\";\nimport { Button } from \"@ui-sdk/react/button\";\nimport {\n Dialog,\n DialogBackdrop,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogTitle,\n DialogTrigger,\n} from \"@ui-sdk/react/dialog\";\nimport type { PrinterMaterialLayout } from \"@printhive/api\";\nimport {\n fillStyle,\n layoutSlots,\n slotContext,\n slotStatus,\n} from \"./presentation\";\nimport { useFilamentRuntime } from \"./provider\";\nimport { requirePrinterLayout } from \"./layout-identity\";\nimport { layoutQueryKey, operationQueryKey } from \"./queries\";\nimport { SlotEditor } from \"./SlotEditor\";\nimport type {\n FilamentLayoutSlot,\n FilamentLayoutSource,\n FilamentManagementProps,\n FilamentManagementDataSource,\n} from \"./types\";\n\ntype RefreshState = { kind: \"idle\" } | { kind: \"pending\" } | { kind: \"failed\" };\ntype LayoutState =\n | { kind: \"loading\" }\n | { kind: \"failed\" }\n | { kind: \"ready\"; layout: PrinterMaterialLayout; refresh: RefreshState };\n\nfunction layoutState(\n query: UseQueryResult<PrinterMaterialLayout, Error>\n): LayoutState {\n if (query.data === undefined)\n return query.isPending ? { kind: \"loading\" } : { kind: \"failed\" };\n let refresh: RefreshState = { kind: \"idle\" };\n if (query.isFetching) refresh = { kind: \"pending\" };\n else if (query.isRefetchError) refresh = { kind: \"failed\" };\n return { kind: \"ready\", layout: query.data, refresh };\n}\n\nfunction SlotCard({\n slot,\n context,\n}: {\n slot: FilamentLayoutSlot;\n context: string;\n}) {\n return (\n <DialogTrigger\n render={<Button />}\n payload={slot.id}\n className=\"ph-filament-slot\"\n aria-label={`Manage ${context} · ${slot.label}`}\n >\n <span className=\"ph-filament-slot-top\">\n <span className=\"ph-filament-slot-label\">{slot.label}</span>\n {slot.feeding && <span className=\"ph-filament-feeding\">Feeding</span>}\n </span>\n <span\n className=\"ph-filament-swatch\"\n style={fillStyle(slot.fill?.colors ?? [], slot.fill?.kind)}\n aria-hidden=\"true\"\n />\n <strong>\n {slot.displayName ??\n slot.materialType ??\n (slot.assignment ? \"Assigned filament\" : \"No assignment\")}\n </strong>\n <span className=\"ph-filament-slot-status\" data-state={slot.presentation}>\n {slotStatus(slot)}\n </span>\n {slot.remainingPct !== null && (\n <span className=\"ph-filament-muted\">\n {Math.round(slot.remainingPct)}% remaining\n </span>\n )}\n </DialogTrigger>\n );\n}\n\nfunction SourceUnit({\n source,\n context,\n}: {\n source: FilamentLayoutSource;\n context: string;\n}) {\n return (\n <section className=\"ph-filament-source\" aria-label={source.label}>\n <header>\n <h3>{source.label}</h3>\n {source.humidityPct !== null && (\n <span className=\"ph-filament-muted\">\n {source.humidityPct}% humidity\n </span>\n )}\n </header>\n <div className=\"ph-filament-slots\" data-layout={source.layout}>\n {source.slots.map((slot) => (\n <SlotCard key={slot.id} slot={slot} context={context} />\n ))}\n </div>\n </section>\n );\n}\n\nfunction HotendRack({ rack }: { rack: PrinterMaterialLayout[\"hotendRack\"] }) {\n if (rack.kind === \"no-rack-reported\") return null;\n if (rack.kind === \"invalid-report\")\n return (\n <p className=\"ph-filament-muted\">\n Hotend rack information is unavailable.\n </p>\n );\n return (\n <section className=\"ph-filament-rack\" aria-label=\"Hotend rack\">\n <h3>Hotend rack</h3>\n {rack.kind === \"reported\" && rack.tools === null && (\n <p className=\"ph-filament-muted\">Hotend details unavailable.</p>\n )}\n <div className=\"ph-filament-rack-tools\">\n {rack.kind === \"legacy\"\n ? rack.slots.map((slot) => (\n <span key={slot.id}>\n Holder {slot.id + 1}\n <strong>\n {slot.occupied\n ? slot.diameterMm !== null\n ? `${slot.diameterMm} mm`\n : \"Hotend\"\n : \"Empty\"}\n </strong>\n {slot.active && <small>Active</small>}\n </span>\n ))\n : (rack.tools ?? []).map((tool) => (\n <span key={tool.id}>\n {tool.location === \"toolhead\"\n ? \"Toolhead\"\n : tool.holderAddress !== null\n ? `Holder ${tool.holderAddress + 1}`\n : \"Rack\"}\n <strong>\n {tool.diameterMm !== null\n ? `${tool.diameterMm} mm`\n : tool.kind}\n </strong>\n {tool.serialNumber && <small>{tool.serialNumber}</small>}\n </span>\n ))}\n </div>\n <p className=\"ph-filament-muted\">\n Hardware information only. Filament assignments belong to the sources\n below.\n </p>\n </section>\n );\n}\n\nexport function FilamentManagement({\n printerId,\n className,\n}: FilamentManagementProps) {\n const { dataSource, queryClient } = useFilamentRuntime();\n const [scope, setScope] = useState({ dataSource, revision: 0 });\n if (scope.dataSource !== dataSource) {\n setScope({ dataSource, revision: scope.revision + 1 });\n }\n return (\n <QueryClientProvider client={queryClient}>\n <PrinterFilamentManagement\n key={`${printerId}:${scope.revision}`}\n printerId={printerId}\n className={className}\n source={dataSource}\n />\n </QueryClientProvider>\n );\n}\n\nfunction PrinterFilamentManagement({\n printerId,\n className,\n source,\n}: FilamentManagementProps & { source: FilamentManagementDataSource }) {\n const { queryClient } = useFilamentRuntime();\n const query = useQuery(\n {\n queryKey: layoutQueryKey(printerId),\n queryFn: async ({ signal }) =>\n requirePrinterLayout(\n await source.read(printerId, { signal }),\n printerId\n ),\n },\n queryClient\n );\n const busy =\n useIsMutating({ mutationKey: operationQueryKey(printerId) }, queryClient) >\n 0;\n const refresh = () => {\n void query.refetch();\n };\n const state = layoutState(query);\n\n const rootClass = `ph-filament-management${className ? ` ${className}` : \"\"}`;\n if (state.kind === \"loading\")\n return (\n <section\n className={rootClass}\n aria-label=\"Filament management\"\n aria-busy=\"true\"\n >\n <p role=\"status\">Loading printer filaments…</p>\n </section>\n );\n if (state.kind === \"failed\")\n return (\n <section className={rootClass} aria-label=\"Filament management\">\n <h2>Filaments unavailable</h2>\n <p role=\"alert\">\n The printer filaments could not be loaded. Check your connection and\n access to this printer.\n </p>\n <Button onClick={refresh}>Retry loading filaments</Button>\n </section>\n );\n const { layout } = state;\n const slots = layoutSlots(layout);\n const sources = layout.nozzles\n .flatMap((nozzle) => nozzle.sources)\n .concat(layout.unboundSources);\n const observedAt =\n \"observedAt\" in layout.freshness ? layout.freshness.observedAt : null;\n\n return (\n <Dialog dismissible={!busy}>\n <section\n className={rootClass}\n aria-label={`Filaments for ${layout.printer.name}`}\n >\n <header className=\"ph-filament-header\">\n <div>\n <p className=\"ph-filament-eyebrow\">Printer filaments</p>\n <h2>{layout.printer.name}</h2>\n <p className=\"ph-filament-muted\">\n {layout.printer.make} {layout.printer.model}\n </p>\n </div>\n <div className=\"ph-filament-header-actions\">\n <span className=\"ph-filament-connection\">\n {layout.printer.connected === true\n ? \"Online\"\n : layout.printer.connected === false\n ? \"Offline\"\n : \"Connection unknown\"}\n </span>\n <Button\n disabled={state.refresh.kind === \"pending\" || busy}\n onClick={refresh}\n >\n Refresh\n </Button>\n </div>\n </header>\n <div className=\"ph-filament-freshness\">\n <span>\n {layout.freshness.status === \"fresh\"\n ? \"Current printer report\"\n : layout.freshness.status === \"stale\"\n ? \"Last known printer report\"\n : \"Live printer report unavailable\"}\n </span>\n {observedAt && (\n <time dateTime={observedAt}>\n {new Date(observedAt).toLocaleString()}\n </time>\n )}\n </div>\n {state.refresh.kind === \"pending\" && (\n <p className=\"ph-filament-muted\" role=\"status\">\n Refreshing printer filaments…\n </p>\n )}\n {state.refresh.kind === \"failed\" && (\n <p className=\"ph-filament-inline-error\" role=\"alert\">\n The latest state could not be loaded. Showing the previous snapshot.\n </p>\n )}\n {layout.topology.kind === \"unavailable\" && (\n <p className=\"ph-filament-inline-error\">\n The printer has not reported its hardware layout. Saved assignments\n are shown without guessing their nozzle or AMS unit.\n </p>\n )}\n <HotendRack rack={layout.hotendRack} />\n <div className=\"ph-filament-nozzles\">\n {layout.nozzles.map((nozzle) => (\n <section\n className=\"ph-filament-nozzle\"\n key={nozzle.id}\n aria-label={nozzle.label}\n >\n <header>\n <h3>{nozzle.label}</h3>\n {nozzle.hotend?.diameterMm !== null &&\n nozzle.hotend?.diameterMm !== undefined && (\n <span className=\"ph-filament-muted\">\n {nozzle.hotend.diameterMm} mm\n {nozzle.hotend.mount === \"vortek\" ? \" · Vortek\" : \"\"}\n </span>\n )}\n </header>\n {nozzle.sources.map((unit) => (\n <SourceUnit\n key={unit.id}\n source={unit}\n context={\n layout.nozzles.length > 1\n ? `${nozzle.label} · ${unit.label}`\n : unit.label\n }\n />\n ))}\n </section>\n ))}\n </div>\n {layout.unboundSources.length > 0 && (\n <section className=\"ph-filament-unbound\">\n <h3>Not assigned to a nozzle</h3>\n {layout.unboundSources.map((unit) => (\n <SourceUnit key={unit.id} source={unit} context={unit.label} />\n ))}\n </section>\n )}\n {layout.unmappedAssignments.length > 0 && (\n <section className=\"ph-filament-unbound\">\n <h3>Saved assignments</h3>\n <div className=\"ph-filament-slots\" data-layout=\"row4\">\n {layout.unmappedAssignments.map((slot) => (\n <SlotCard\n key={slot.id}\n slot={slot}\n context=\"Saved assignments\"\n />\n ))}\n </div>\n </section>\n )}\n {sources.length === 0 && slots.length === 0 && (\n <p className=\"ph-filament-muted\">\n No filament sources have been reported for this printer.\n </p>\n )}\n <DialogBackdrop className=\"ph-filament-backdrop\" />\n <DialogContent<string>\n className=\"ph-filament-editor\"\n aria-label={`Manage ${layout.printer.name} filaments`}\n keepMounted\n >\n {(dialog) => {\n const slot = slots.find(\n (candidate) => candidate.id === dialog.payload\n );\n return dialog.open && slot ? (\n <SlotEditor\n key={slot.id}\n printerId={printerId}\n slot={slot}\n context={slotContext(layout, slot.id)}\n />\n ) : (\n <>\n <DialogTitle>\n {!dialog.open || dialog.payload === undefined\n ? `Manage ${layout.printer.name} filaments`\n : \"Slot unavailable\"}\n </DialogTitle>\n <DialogDescription>\n This slot is no longer part of the reported printer layout.\n Refresh the printer before making changes.\n </DialogDescription>\n <DialogClose>Close</DialogClose>\n </>\n );\n }}\n </DialogContent>\n </section>\n </Dialog>\n );\n}\n","import type { CSSProperties } from \"react\";\nimport type {\n AssignFilamentsResult,\n ClearFilamentAssignmentsResult,\n PrinterMaterialLayout,\n RefreshMaterialSlotRfidResult,\n} from \"@printhive/api\";\nimport type { FilamentLayoutSlot } from \"./types\";\n\nexport interface OperationNotice {\n tone: \"success\" | \"warning\" | \"error\";\n message: string;\n}\n\nexport function layoutSlots(\n layout: PrinterMaterialLayout\n): FilamentLayoutSlot[] {\n return [\n ...layout.nozzles.flatMap((nozzle) =>\n nozzle.sources.flatMap((source) => source.slots)\n ),\n ...layout.unboundSources.flatMap((source) => source.slots),\n ...layout.unmappedAssignments,\n ];\n}\n\nexport function slotContext(\n layout: PrinterMaterialLayout,\n slotId: string\n): string {\n for (const nozzle of layout.nozzles) {\n for (const source of nozzle.sources) {\n if (source.slots.some((slot) => slot.id === slotId)) {\n return layout.nozzles.length > 1\n ? `${nozzle.label} · ${source.label}`\n : source.label;\n }\n }\n }\n return (\n layout.unboundSources.find((source) =>\n source.slots.some((slot) => slot.id === slotId)\n )?.label ?? \"Saved assignments\"\n );\n}\n\nexport function slotStatus(slot: FilamentLayoutSlot): string {\n if (slot.reading) return \"Reading RFID\";\n if (slot.feeding) return \"Feeding nozzle\";\n switch (slot.presentation) {\n case \"synced\":\n return \"Matches printer\";\n case \"mismatch\":\n return \"Different from printer\";\n case \"assigned-empty\":\n return \"Assigned, no filament detected\";\n case \"assigned-unknown\":\n return \"Assignment saved, presence unknown\";\n case \"presence-unknown\":\n return \"Filament presence unknown\";\n case \"printer-only\":\n return \"Reported by printer, not assigned\";\n case \"empty\":\n return \"No filament detected\";\n }\n}\n\nexport function fillStyle(\n colors: readonly string[],\n kind?: NonNullable<FilamentLayoutSlot[\"fill\"]>[\"kind\"]\n): CSSProperties {\n if (colors.length === 0) return {};\n if (colors.length === 1) return { backgroundColor: colors[0] };\n const stops =\n kind && kind !== \"gradient\"\n ? colors.map(\n (color, index) =>\n `${color} ${(index / colors.length) * 100}% ${((index + 1) / colors.length) * 100}%`\n )\n : colors;\n return { backgroundImage: `linear-gradient(135deg, ${stops.join(\", \")})` };\n}\n\nexport function assignmentNotice(\n result: AssignFilamentsResult\n): OperationNotice {\n const slot = result.slots[0];\n if (!slot || slot.record !== \"saved\") {\n return {\n tone: \"error\",\n message:\n \"The assignment was not saved. Refresh this printer before trying again; its metadata may have changed.\",\n };\n }\n if (slot.printerSync.status === \"failed\") {\n return {\n tone: \"warning\",\n message:\n \"Assignment saved in PrintHive. The printer metadata request could not be confirmed.\",\n };\n }\n if (slot.printerSync.status === \"skipped\") {\n return {\n tone: \"success\",\n message:\n \"Assignment saved in PrintHive. Printer metadata was not changed.\",\n };\n }\n if (slot.observation?.status === \"observed\") {\n return {\n tone: \"success\",\n message: \"Assignment saved and confirmed by the printer.\",\n };\n }\n if (slot.observation?.status === \"mismatch\") {\n return {\n tone: \"warning\",\n message:\n \"Assignment saved. The printer still reports different filament.\",\n };\n }\n return {\n tone: \"success\",\n message:\n \"Assignment saved. Metadata request sent; printer confirmation is still pending.\",\n };\n}\n\nexport function clearNotice(\n result: ClearFilamentAssignmentsResult\n): OperationNotice {\n return result.slots.some((slot) => slot.record === \"cleared\")\n ? {\n tone: \"success\",\n message: \"Assignment cleared. Printer metadata was not changed.\",\n }\n : {\n tone: \"success\",\n message:\n \"No saved assignment remained. Printer metadata was not changed.\",\n };\n}\n\nexport function rfidNotice(\n result: RefreshMaterialSlotRfidResult\n): OperationNotice {\n switch (result.printerSync.status) {\n case \"failed\":\n return {\n tone: \"warning\",\n message:\n \"The RFID request could not be confirmed. Refresh this printer before trying again.\",\n };\n case \"skipped\":\n return {\n tone: \"warning\",\n message:\n \"The RFID reread was not requested. Refresh this printer to check which actions are available.\",\n };\n case \"sent\":\n return {\n tone: \"success\",\n message:\n \"RFID reread requested. A completed tag read has not yet been confirmed.\",\n };\n }\n}\n","import { createContext, useContext, useState, type ReactNode } from \"react\";\nimport { QueryClient } from \"@tanstack/react-query\";\nimport type { FilamentManagementDataSource } from \"./types\";\n\ninterface FilamentRuntime {\n dataSource: FilamentManagementDataSource;\n queryClient: QueryClient;\n}\n\nconst DataSourceContext = createContext<FilamentRuntime | null>(null);\n\nexport interface PrintHiveProviderProps {\n dataSource: FilamentManagementDataSource;\n children: ReactNode;\n}\n\nexport function PrintHiveProvider({\n dataSource,\n children,\n}: PrintHiveProviderProps) {\n const [scope, setScope] = useState(() => ({\n dataSource,\n queryClient: createQueryClient(),\n }));\n if (scope.dataSource !== dataSource) {\n setScope({ dataSource, queryClient: createQueryClient() });\n }\n return <DataSourceContext value={scope}>{children}</DataSourceContext>;\n}\n\nfunction createQueryClient() {\n return new QueryClient({\n defaultOptions: {\n queries: { retry: false, refetchOnWindowFocus: false, gcTime: 0 },\n mutations: { retry: false },\n },\n });\n}\n\nexport function useFilamentRuntime(): FilamentRuntime {\n const source = useContext(DataSourceContext);\n if (source === null) {\n throw new Error(\"FilamentManagement must be inside a PrintHiveProvider.\");\n }\n return source;\n}\n","export function layoutQueryKey(printerId: string): readonly [string, string] {\n return [\"printhive-filaments\", printerId];\n}\n\nexport function operationQueryKey(\n printerId: string\n): readonly [string, string] {\n return [\"printhive-filament-operation\", printerId];\n}\n\nexport async function catalogDebounce(\n signal: AbortSignal,\n delayMs: number\n): Promise<void> {\n if (signal.aborted)\n throw new DOMException(\"Catalog search aborted\", \"AbortError\");\n await new Promise<void>((resolve, reject) => {\n const finish = () => {\n signal.removeEventListener(\"abort\", cancel);\n resolve();\n };\n const timer = setTimeout(finish, delayMs);\n const cancel = () => {\n clearTimeout(timer);\n reject(new DOMException(\"Catalog search aborted\", \"AbortError\"));\n };\n signal.addEventListener(\"abort\", cancel, { once: true });\n });\n}\n","import { useState } from \"react\";\nimport { useIsMutating } from \"@tanstack/react-query\";\nimport { Button } from \"@ui-sdk/react/button\";\nimport {\n DialogClose,\n DialogDescription,\n DialogTitle,\n} from \"@ui-sdk/react/dialog\";\nimport { CatalogPicker } from \"./CatalogPicker\";\nimport { FilamentManagementError } from \"./errors\";\nimport { useFilamentOperation, type FilamentOperation } from \"./mutations\";\nimport { fillStyle } from \"./presentation\";\nimport { useFilamentRuntime } from \"./provider\";\nimport { operationQueryKey } from \"./queries\";\nimport type {\n ClearFilamentInput,\n FilamentLayoutSlot,\n FilamentOption,\n} from \"./types\";\n\nfunction SlotComparison({ slot }: { slot: FilamentLayoutSlot }) {\n const assignment = slot.assignment;\n const reported =\n \"reported\" in slot.observation ? slot.observation.reported : null;\n const unavailable =\n slot.observation.status === \"stale\"\n ? \"Last report is stale\"\n : \"Observation unavailable\";\n return (\n <div className=\"ph-filament-comparison\">\n <section>\n <h3>Saved assignment</h3>\n <div className=\"ph-filament-material\">\n <span\n className=\"ph-filament-swatch\"\n style={fillStyle(assignment?.color ? [assignment.color] : [])}\n aria-hidden=\"true\"\n />\n <div>\n <strong>\n {assignment\n ? (slot.displayName ??\n assignment.materialType ??\n \"Assigned filament\")\n : \"No assignment\"}\n </strong>\n <p>{assignment?.color ?? \"No saved color\"}</p>\n </div>\n </div>\n </section>\n <section>\n <h3>Printer reports</h3>\n <div className=\"ph-filament-material\">\n <span\n className=\"ph-filament-swatch\"\n style={fillStyle(reported?.color ? [reported.color] : [])}\n aria-hidden=\"true\"\n />\n <div>\n <strong>{reported?.materialType ?? \"No current report\"}</strong>\n <p>{reported?.color ?? unavailable}</p>\n </div>\n </div>\n </section>\n </div>\n );\n}\n\nfunction ActionReason({\n capability,\n}: {\n capability: FilamentLayoutSlot[\"capabilities\"][\"assign\"];\n}) {\n return capability.kind === \"unavailable\" ? (\n <p className=\"ph-filament-muted\">{capability.message}</p>\n ) : null;\n}\n\nfunction OperationFeedback({\n operation,\n}: {\n operation: ReturnType<typeof useFilamentOperation>;\n}) {\n if (operation.isPending) {\n const message =\n operation.variables.kind === \"assign\"\n ? \"Saving assignment…\"\n : operation.variables.kind === \"clear\"\n ? \"Clearing assignment…\"\n : \"Requesting RFID reread…\";\n return (\n <p className=\"ph-filament-muted\" role=\"status\">\n {message}\n </p>\n );\n }\n if (operation.isError) {\n const message =\n operation.error instanceof FilamentManagementError\n ? operation.error.message\n : \"The request could not be confirmed. Your draft is still here. Refresh this printer before trying again.\";\n return (\n <p className=\"ph-filament-notice\" data-tone=\"error\" role=\"alert\">\n {message}\n </p>\n );\n }\n if (!operation.data) return null;\n return (\n <p\n className=\"ph-filament-notice\"\n data-tone={operation.data.tone}\n role={operation.data.tone === \"error\" ? \"alert\" : \"status\"}\n >\n {operation.data.message}\n </p>\n );\n}\n\nexport function SlotEditor({\n printerId,\n slot,\n context,\n}: {\n printerId: string;\n slot: FilamentLayoutSlot;\n context: string;\n}) {\n const { queryClient } = useFilamentRuntime();\n const operation = useFilamentOperation(printerId);\n const busy =\n useIsMutating({ mutationKey: operationQueryKey(printerId) }, queryClient) >\n 0;\n const [selected, setSelected] = useState<FilamentOption | null>(null);\n const [clearTarget, setClearTarget] = useState<ClearFilamentInput | null>(\n null\n );\n const assignAvailability = slot.capabilities.assign;\n const clearAvailability = slot.capabilities.clearAssignment;\n const rfidAvailability = slot.capabilities.refreshRfid;\n const clearVersion = slot.assignment?.version ?? null;\n\n const run = (intent: FilamentOperation) => {\n if (\n queryClient.isMutating({ mutationKey: operationQueryKey(printerId) }) > 0\n )\n return;\n return operation\n .mutateAsync(intent, {\n onSuccess: (notice, completed) => {\n if (completed.kind === \"assign\" && notice.tone !== \"error\")\n setSelected(null);\n setClearTarget(null);\n },\n })\n .catch(() => undefined);\n };\n\n const requestClear = () => {\n if (clearVersion === null) return;\n setClearTarget({ slotId: slot.id, expectedVersion: clearVersion });\n };\n\n return (\n <>\n <div className=\"ph-filament-editor-heading\">\n <div>\n <DialogTitle>\n {context} · {slot.label}\n </DialogTitle>\n <DialogDescription>\n Manage the saved assignment and compare it with the printer report.\n </DialogDescription>\n </div>\n <DialogClose className=\"ph-filament-close\" disabled={busy}>\n Close\n </DialogClose>\n </div>\n <SlotComparison slot={slot} />\n <CatalogPicker\n printerId={printerId}\n selected={selected}\n onSelect={setSelected}\n disabled={busy || assignAvailability.kind !== \"available\"}\n />\n <ActionReason capability={assignAvailability} />\n {slot.capabilities.writePrinterMetadata.kind === \"unavailable\" &&\n assignAvailability.kind === \"available\" && (\n <p className=\"ph-filament-muted\">\n {slot.capabilities.writePrinterMetadata.message} Saving an\n assignment still records it in PrintHive.\n </p>\n )}\n <OperationFeedback operation={operation} />\n <div className=\"ph-filament-editor-primary\">\n <Button\n className=\"ph-filament-primary\"\n disabled={\n busy || selected === null || assignAvailability.kind !== \"available\"\n }\n onClick={() =>\n selected\n ? run({\n kind: \"assign\",\n input: { slotId: slot.id, materialId: selected.id },\n })\n : undefined\n }\n pendingAnnouncement=\"Saving assignment\"\n >\n Save assignment\n </Button>\n </div>\n <div className=\"ph-filament-editor-secondary\">\n <div>\n <Button\n disabled={busy || rfidAvailability.kind !== \"available\"}\n onClick={() => run({ kind: \"rfid\", input: { slotId: slot.id } })}\n pendingAnnouncement=\"Requesting RFID reread\"\n >\n Read RFID\n </Button>\n <ActionReason capability={rfidAvailability} />\n </div>\n <div>\n <Button\n disabled={\n busy ||\n clearVersion === null ||\n clearAvailability.kind !== \"available\"\n }\n onClick={requestClear}\n >\n Clear assignment\n </Button>\n <ActionReason capability={clearAvailability} />\n {slot.assignment && clearVersion === null && (\n <p className=\"ph-filament-muted\">\n Refresh this printer before clearing its assignment; the saved\n version is unavailable.\n </p>\n )}\n </div>\n </div>\n {clearTarget && (\n <section\n className=\"ph-filament-clear-confirmation\"\n aria-label=\"Confirm clearing assignment\"\n >\n <p>\n Clear the saved assignment for {slot.label}? This does not unload\n filament or change printer metadata.\n </p>\n <div className=\"ph-filament-actions\">\n <Button\n className=\"ph-filament-danger\"\n data-agent-confirm={`Clears the saved filament assignment for ${context} · ${slot.label}. Printer metadata is unchanged.`}\n disabled={busy}\n onClick={() => run({ kind: \"clear\", input: clearTarget })}\n >\n Confirm clear assignment\n </Button>\n <Button disabled={busy} onClick={() => setClearTarget(null)}>\n Keep assignment\n </Button>\n </div>\n </section>\n )}\n </>\n );\n}\n","import { useState } from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Button } from \"@ui-sdk/react/button\";\nimport {\n Combobox,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxInput,\n ComboboxItem,\n ComboboxLabel,\n ComboboxList,\n ComboboxTrigger,\n} from \"@ui-sdk/react/combobox\";\nimport { useFilamentRuntime } from \"./provider\";\nimport { catalogDebounce } from \"./queries\";\nimport { fillStyle } from \"./presentation\";\nimport type { FilamentOption } from \"./types\";\n\ntype SearchState =\n | { kind: \"loading\" }\n | { kind: \"ready\"; options: FilamentOption[] }\n | { kind: \"failed\" };\n\nexport function CatalogPicker({\n printerId,\n selected,\n onSelect,\n disabled,\n}: {\n printerId: string;\n selected: FilamentOption | null;\n onSelect: (option: FilamentOption | null) => void;\n disabled: boolean;\n}) {\n const { dataSource, queryClient } = useFilamentRuntime();\n const [query, setQuery] = useState(\"\");\n const search = useQuery(\n {\n queryKey: [\"printhive-filament-catalog\", printerId, query],\n queryFn: async ({ signal }) => {\n await catalogDebounce(signal, query.length === 0 ? 0 : 180);\n return dataSource.search(printerId, query, { signal });\n },\n enabled: !disabled,\n },\n queryClient\n );\n const state: SearchState = disabled\n ? { kind: \"ready\", options: [] }\n : search.isError\n ? { kind: \"failed\" }\n : search.data !== undefined\n ? { kind: \"ready\", options: search.data }\n : { kind: \"loading\" };\n\n const results = state.kind === \"ready\" ? state.options : [];\n const options =\n selected && !results.some((option) => option.id === selected.id)\n ? [selected, ...results]\n : results;\n\n return (\n <div className=\"ph-filament-catalog\">\n <Combobox<string>\n items={options.map((option) => ({\n value: option.id,\n label: option.label,\n }))}\n value={selected?.id ?? null}\n query={query}\n onQueryChange={(next, details) => {\n setQuery(next);\n if (details.reason === \"input-change\") onSelect(null);\n }}\n onValueChange={(next) =>\n onSelect(options.find((option) => option.id === next) ?? null)\n }\n filter={false}\n disabled={disabled}\n >\n <ComboboxLabel>Assign filament</ComboboxLabel>\n <div className=\"ph-filament-combobox-control\">\n <ComboboxInput\n placeholder=\"Search your filament catalog\"\n autoComplete=\"off\"\n />\n <ComboboxTrigger aria-label=\"Show filament catalog\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" aria-hidden=\"true\">\n <path\n d=\"m4 6 4 4 4-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n />\n </svg>\n </ComboboxTrigger>\n </div>\n <ComboboxContent className=\"ph-filament-catalog-popup\" sideOffset={4}>\n <ComboboxEmpty>\n {state.kind === \"loading\"\n ? \"Searching catalog…\"\n : \"No matching filaments\"}\n </ComboboxEmpty>\n <ComboboxList aria-busy={state.kind === \"loading\"}>\n {options.map((option) => (\n <ComboboxItem\n key={option.id}\n value={option.id}\n label={option.label}\n aria-label={option.label}\n >\n <span\n className=\"ph-filament-swatch ph-filament-swatch-small\"\n style={fillStyle(\n option.colors.length\n ? option.colors\n : option.color\n ? [option.color]\n : []\n )}\n aria-hidden=\"true\"\n />\n <span>\n <span className=\"ph-filament-option-label\">\n {option.label}\n </span>\n <span className=\"ph-filament-muted\">\n {option.materialType}\n {option.description ? ` · ${option.description}` : \"\"}\n </span>\n </span>\n </ComboboxItem>\n ))}\n </ComboboxList>\n </ComboboxContent>\n </Combobox>\n {state.kind === \"failed\" && (\n <div className=\"ph-filament-inline-error\" role=\"alert\">\n <p>The filament catalog could not be loaded.</p>\n <Button onClick={() => search.refetch()}>Retry catalog search</Button>\n </div>\n )}\n </div>\n );\n}\n","import { useMutation } from \"@tanstack/react-query\";\nimport { assignmentNotice, clearNotice, rfidNotice } from \"./presentation\";\nimport { useFilamentRuntime } from \"./provider\";\nimport { layoutQueryKey, operationQueryKey } from \"./queries\";\nimport type { AssignFilamentInput, ClearFilamentInput } from \"./types\";\n\nexport type FilamentOperation =\n | { kind: \"assign\"; input: AssignFilamentInput }\n | { kind: \"clear\"; input: ClearFilamentInput }\n | { kind: \"rfid\"; input: { slotId: string } };\n\nexport function useFilamentOperation(printerId: string) {\n const { dataSource, queryClient } = useFilamentRuntime();\n return useMutation(\n {\n mutationKey: operationQueryKey(printerId),\n retry: false,\n mutationFn: async (operation: FilamentOperation) => {\n switch (operation.kind) {\n case \"assign\":\n return assignmentNotice(\n await dataSource.assign(printerId, operation.input)\n );\n case \"clear\":\n return clearNotice(\n await dataSource.clear(printerId, operation.input)\n );\n case \"rfid\":\n return rfidNotice(\n await dataSource.refreshRfid(printerId, operation.input)\n );\n }\n },\n onSettled: () => {\n void queryClient.invalidateQueries({\n queryKey: layoutQueryKey(printerId),\n });\n },\n },\n queryClient\n );\n}\n","import {\n isAssignFilamentsResult,\n isClearFilamentAssignmentsResult,\n isFilamentSearchPage,\n isPrinterMaterialLayout,\n isRefreshMaterialSlotRfidResult,\n} from \"@printhive/api/validation\";\nimport type {\n AssignFilamentsResult,\n ClearFilamentAssignmentsResult,\n FilamentSearchPage,\n PrinterMaterialLayout,\n RefreshMaterialSlotRfidResult,\n} from \"@printhive/api\";\nimport { z } from \"zod\";\nimport { FilamentManagementError } from \"./errors.js\";\nimport { requirePrinterLayout } from \"./layout-identity.js\";\nimport type {\n AssignFilamentInput,\n ClearFilamentInput,\n FilamentManagementDataSource,\n} from \"./types.js\";\n\nexport interface FilamentManagementDataSourceOptions {\n baseUrl: string;\n fetch?: typeof globalThis.fetch;\n}\n\nconst failureSchema = z.object({ error: z.string(), message: z.string() });\nconst layoutSchema = z.custom<PrinterMaterialLayout>(isPrinterMaterialLayout);\nconst catalogSchema = z.custom<FilamentSearchPage>(isFilamentSearchPage);\nconst assignmentSchema = z.custom<AssignFilamentsResult>(\n isAssignFilamentsResult\n);\nconst clearSchema = z.custom<ClearFilamentAssignmentsResult>(\n isClearFilamentAssignmentsResult\n);\nconst rfidSchema = z.custom<RefreshMaterialSlotRfidResult>(\n isRefreshMaterialSlotRfidResult\n);\n\nfunction invalidResponse(response: Response): FilamentManagementError {\n return new FilamentManagementError(\n \"The filament service returned an invalid response.\",\n {\n code: \"invalid_response\",\n status: response.status,\n }\n );\n}\n\nexport function createFilamentManagementDataSource(\n options: FilamentManagementDataSourceOptions\n): FilamentManagementDataSource {\n const baseUrl = options.baseUrl.trim().replace(/\\/+$/, \"\");\n if (!options.baseUrl.trim()) throw new TypeError(\"baseUrl is required\");\n const fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);\n\n async function request<T>(\n printerId: string,\n suffix: string,\n schema: z.ZodType<T>,\n signal?: AbortSignal,\n body?:\n | AssignFilamentInput\n | ClearFilamentInput\n | Parameters<FilamentManagementDataSource[\"refreshRfid\"]>[1]\n ): Promise<T> {\n if (!printerId.trim()) throw new TypeError(\"printerId is required\");\n const response = await fetcher(\n `${baseUrl}/printers/${encodeURIComponent(printerId)}/filaments${suffix}`,\n {\n method: body === undefined ? \"GET\" : \"POST\",\n credentials: \"same-origin\",\n signal,\n headers:\n body === undefined\n ? undefined\n : { \"content-type\": \"application/json\" },\n body: body === undefined ? undefined : JSON.stringify(body),\n }\n );\n const value: unknown = await response.json().catch(() => {\n throw invalidResponse(response);\n });\n if (!response.ok) {\n const failure = failureSchema.safeParse(value);\n throw new FilamentManagementError(\n failure.success\n ? failure.data.message\n : \"The filament request could not be completed.\",\n {\n code: failure.success ? failure.data.error : \"request_failed\",\n status: response.status,\n }\n );\n }\n const parsed = schema.safeParse(value);\n if (!parsed.success) throw invalidResponse(response);\n return parsed.data;\n }\n\n return {\n read: async (id, options) =>\n requirePrinterLayout(\n await request(id, \"\", layoutSchema, options?.signal),\n id\n ),\n async search(id, query, options) {\n const params = new URLSearchParams({ q: query });\n const page = await request(\n id,\n `/catalog?${params}`,\n catalogSchema,\n options?.signal\n );\n return page.filaments.map((filament) => ({\n id: filament.id,\n label: filament.name,\n materialType: filament.material_type,\n color: filament.color,\n colors: filament.colors,\n description: [filament.brand, filament.colorName]\n .filter(Boolean)\n .join(\" · \"),\n }));\n },\n assign: (id, input, options) =>\n request(id, \"/assign\", assignmentSchema, options?.signal, input),\n clear: (id, input, options) =>\n request(id, \"/clear\", clearSchema, options?.signal, input),\n refreshRfid: (id, input, options) =>\n request(id, \"/refresh-rfid\", rfidSchema, options?.signal, input),\n };\n}\n"],"mappings":"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","names":["useState","useIsMutating","useQuery","Button","DialogClose","DialogDescription","DialogTitle","useState","Button","useState","jsx","useState","jsx","jsxs","useState","Button","Fragment","jsx","jsxs","Button","useState","useQuery","useIsMutating","DialogTitle","DialogDescription","DialogClose","options"]}
@@ -0,0 +1,305 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target2, all) => {
7
+ for (var name in all)
8
+ __defProp(target2, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/server.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ createFilamentManagementHandler: () => createFilamentManagementHandler
24
+ });
25
+ module.exports = __toCommonJS(server_exports);
26
+ var import_api = require("@printhive/api");
27
+ var import_zod = require("zod");
28
+
29
+ // src/errors.ts
30
+ var FilamentManagementError = class extends Error {
31
+ code;
32
+ status;
33
+ constructor(message, options) {
34
+ super(message);
35
+ this.name = "FilamentManagementError";
36
+ this.code = options.code;
37
+ this.status = options.status;
38
+ }
39
+ };
40
+
41
+ // src/layout-identity.ts
42
+ function requirePrinterLayout(layout, printerId) {
43
+ if (layout.printer.id.toLowerCase() !== printerId.toLowerCase()) {
44
+ throw new FilamentManagementError(
45
+ "The filament service returned a different printer. Refresh before making changes.",
46
+ { code: "invalid_response", status: 502 }
47
+ );
48
+ }
49
+ return layout;
50
+ }
51
+
52
+ // src/server.ts
53
+ var slotInput = import_zod.z.object({ slotId: import_zod.z.string().min(1) }).strict();
54
+ var assignInput = slotInput.extend({ materialId: import_zod.z.string().min(1) });
55
+ var clearInput = slotInput.extend({
56
+ expectedVersion: import_zod.z.object({ id: import_zod.z.string().min(1), updatedAt: import_zod.z.string().nullable() }).strict()
57
+ });
58
+ var routeSchema = import_zod.z.object({
59
+ printerId: import_zod.z.string().uuid(),
60
+ action: import_zod.z.enum(["read", "catalog", "assign", "clear", "refresh-rfid"])
61
+ });
62
+ function slots(layout) {
63
+ return [
64
+ ...layout.nozzles.flatMap(
65
+ (nozzle) => nozzle.sources.flatMap((source) => source.slots)
66
+ ),
67
+ ...layout.unboundSources.flatMap((source) => source.slots),
68
+ ...layout.unmappedAssignments
69
+ ];
70
+ }
71
+ function readOnly(layout) {
72
+ const copy = structuredClone(layout);
73
+ const unavailable = {
74
+ kind: "unavailable",
75
+ code: "permission_required",
76
+ message: "Your connection has read-only access."
77
+ };
78
+ for (const slot of slots(copy)) {
79
+ slot.capabilities = {
80
+ assign: unavailable,
81
+ clearAssignment: unavailable,
82
+ writePrinterMetadata: unavailable,
83
+ refreshRfid: unavailable
84
+ };
85
+ }
86
+ return copy;
87
+ }
88
+ function target(layout, slotId, action) {
89
+ const slot = slots(layout).find((candidate) => candidate.id === slotId);
90
+ if (!slot)
91
+ throw new FilamentManagementError(
92
+ "This slot is no longer available. Refresh the printer.",
93
+ { code: "slot_not_present", status: 404 }
94
+ );
95
+ const capability = slot.capabilities[action];
96
+ if (capability.kind === "unavailable") {
97
+ throw new FilamentManagementError(capability.message, {
98
+ code: capability.code,
99
+ status: capability.code === "permission_required" ? 403 : 409
100
+ });
101
+ }
102
+ return slot;
103
+ }
104
+ function route(request, basePath) {
105
+ const prefix = `${basePath}/printers/`;
106
+ const path = new URL(request.url).pathname;
107
+ if (!path.startsWith(prefix)) return null;
108
+ const [id, resource, suffix, ...extra] = path.slice(prefix.length).split("/");
109
+ if (resource !== "filaments" || extra.length || !id) return null;
110
+ const action = suffix || "read";
111
+ let printerId;
112
+ try {
113
+ printerId = decodeURIComponent(id);
114
+ } catch {
115
+ return null;
116
+ }
117
+ const parsed = routeSchema.safeParse({ printerId, action });
118
+ if (!parsed.success) return null;
119
+ const expectedMethod = action === "read" || action === "catalog" ? "GET" : "POST";
120
+ if (request.method !== expectedMethod)
121
+ throw new FilamentManagementError("This request method is not supported.", {
122
+ code: "method_not_allowed",
123
+ status: 405
124
+ });
125
+ if (expectedMethod === "POST" && request.headers.get("content-type")?.split(";")[0]?.trim() !== "application/json") {
126
+ throw new FilamentManagementError("Send filament changes as JSON.", {
127
+ code: "invalid_request",
128
+ status: 415
129
+ });
130
+ }
131
+ return parsed.data;
132
+ }
133
+ async function body(request, schema) {
134
+ const value = await request.json().catch(() => null);
135
+ const result = schema.safeParse(value);
136
+ if (!result.success)
137
+ throw new FilamentManagementError("The filament request is invalid.", {
138
+ code: "invalid_request",
139
+ status: 400
140
+ });
141
+ return result.data;
142
+ }
143
+ var messages = /* @__PURE__ */ new Map([
144
+ [
145
+ "assignment_changed",
146
+ "This assignment changed elsewhere. Refresh before clearing it."
147
+ ],
148
+ ["printer_busy", "The printer is busy. Try again when it is available."],
149
+ ["printer_not_found", "This printer is not available to your connection."],
150
+ [
151
+ "slot_not_present",
152
+ "This slot is no longer available. Refresh the printer."
153
+ ],
154
+ [
155
+ "filament_at_extruder",
156
+ "RFID cannot be refreshed while filament is at the extruder."
157
+ ],
158
+ [
159
+ "filament_state_unknown",
160
+ "The filament state is unknown. Refresh the printer before reading RFID."
161
+ ],
162
+ ["rfid_protocol_unsupported", "This printer does not support RFID refresh."],
163
+ ["external_rfid_unsupported", "External spools do not support RFID refresh."],
164
+ [
165
+ "printer_metadata_unsupported",
166
+ "This printer does not support writing filament metadata."
167
+ ]
168
+ ]);
169
+ function failure(error) {
170
+ if (error instanceof FilamentManagementError) {
171
+ return Response.json(
172
+ { error: error.code, message: error.message },
173
+ { status: error.status ?? 500 }
174
+ );
175
+ }
176
+ if (error instanceof import_api.PrintHiveApiError) {
177
+ return Response.json(
178
+ {
179
+ error: messages.has(error.code) ? error.code : "request_failed",
180
+ message: messages.get(error.code) ?? "The filament request could not be completed. Try again."
181
+ },
182
+ { status: error.status }
183
+ );
184
+ }
185
+ return Response.json(
186
+ {
187
+ error: "service_unavailable",
188
+ message: "The filament service is unavailable. Try again."
189
+ },
190
+ { status: 503 }
191
+ );
192
+ }
193
+ async function operate(client, request, printerId, action, access) {
194
+ if (action === "catalog") {
195
+ const query = new URL(request.url).searchParams.get("q") ?? "";
196
+ return client.filaments.search({
197
+ query,
198
+ limit: 20,
199
+ signal: request.signal
200
+ });
201
+ }
202
+ if (action === "read") {
203
+ const layout2 = requirePrinterLayout(
204
+ await client.printers.materialSlots.layout(printerId, request.signal),
205
+ printerId
206
+ );
207
+ return access === "read" ? readOnly(layout2) : layout2;
208
+ }
209
+ if (action === "assign") {
210
+ const input2 = await body(request, assignInput);
211
+ const layout2 = requirePrinterLayout(
212
+ await client.printers.materialSlots.layout(printerId, request.signal),
213
+ printerId
214
+ );
215
+ const slot2 = target(layout2, input2.slotId, "assign");
216
+ return client.printers.materialSlots.assign(
217
+ printerId,
218
+ {
219
+ syncPolicy: "automatic",
220
+ slots: [{ position: slot2.position, materialId: input2.materialId }]
221
+ },
222
+ request.signal
223
+ );
224
+ }
225
+ if (action === "clear") {
226
+ const input2 = await body(request, clearInput);
227
+ const layout2 = requirePrinterLayout(
228
+ await client.printers.materialSlots.layout(printerId, request.signal),
229
+ printerId
230
+ );
231
+ const slot2 = target(layout2, input2.slotId, "clearAssignment");
232
+ if (!slot2.assignment?.version) {
233
+ throw new FilamentManagementError(
234
+ "This assignment changed elsewhere. Refresh before clearing it.",
235
+ { code: "assignment_changed", status: 409 }
236
+ );
237
+ }
238
+ return client.printers.materialSlots.clearAssignments(
239
+ printerId,
240
+ [slot2.position],
241
+ { expectedVersion: input2.expectedVersion, signal: request.signal }
242
+ );
243
+ }
244
+ const input = await body(request, slotInput);
245
+ const layout = requirePrinterLayout(
246
+ await client.printers.materialSlots.layout(printerId, request.signal),
247
+ printerId
248
+ );
249
+ const slot = target(layout, input.slotId, "refreshRfid");
250
+ return client.printers.materialSlots.refreshRfid(
251
+ printerId,
252
+ slot.position,
253
+ request.signal
254
+ );
255
+ }
256
+ function createFilamentManagementHandler(options) {
257
+ const basePath = (options.basePath ?? "/api/printhive").replace(/\/+$/, "");
258
+ if (basePath && !basePath.startsWith("/"))
259
+ throw new TypeError("basePath must start with /");
260
+ return async (request) => {
261
+ try {
262
+ const parsed = route(request, basePath);
263
+ if (!parsed)
264
+ return Response.json(
265
+ {
266
+ error: "not_found",
267
+ message: "This filament route does not exist."
268
+ },
269
+ { status: 404 }
270
+ );
271
+ const access = await options.authorize(request, parsed.printerId);
272
+ if (access !== "read" && access !== "control" || access !== "control" && parsed.action !== "read" && parsed.action !== "catalog") {
273
+ return Response.json(
274
+ {
275
+ error: "permission_required",
276
+ message: "You do not have access to this filament operation."
277
+ },
278
+ { status: 403 }
279
+ );
280
+ }
281
+ return Response.json(
282
+ await operate(
283
+ options.client,
284
+ request,
285
+ parsed.printerId,
286
+ parsed.action,
287
+ access
288
+ ),
289
+ { headers: { "cache-control": "no-store" } }
290
+ );
291
+ } catch (error) {
292
+ return failure(
293
+ error instanceof FilamentManagementError || error instanceof import_api.PrintHiveApiError ? error : new FilamentManagementError(
294
+ "The filament service is unavailable. Try again.",
295
+ { code: "service_unavailable", status: 503 }
296
+ )
297
+ );
298
+ }
299
+ };
300
+ }
301
+ // Annotate the CommonJS export names for ESM import in node:
302
+ 0 && (module.exports = {
303
+ createFilamentManagementHandler
304
+ });
305
+ //# sourceMappingURL=server.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts","../src/errors.ts","../src/layout-identity.ts"],"sourcesContent":["import {\n PrintHiveApiError,\n type PrintHive,\n type PrinterMaterialLayout,\n type AssignFilamentsResult,\n type ClearFilamentAssignmentsResult,\n type RefreshMaterialSlotRfidResult,\n type FilamentSearchPage,\n} from \"@printhive/api\";\nimport { z } from \"zod\";\nimport { FilamentManagementError } from \"./errors.js\";\nimport { requirePrinterLayout } from \"./layout-identity.js\";\nimport type { FilamentLayoutSlot } from \"./types.js\";\n\ntype FilamentAccess = \"read\" | \"control\" | null;\ntype Action = \"read\" | \"catalog\" | \"assign\" | \"clear\" | \"refresh-rfid\";\ntype FilamentResponse =\n | PrinterMaterialLayout\n | AssignFilamentsResult\n | ClearFilamentAssignmentsResult\n | RefreshMaterialSlotRfidResult\n | FilamentSearchPage;\n\nexport interface FilamentManagementHandlerOptions {\n client: PrintHive;\n authorize: (\n request: Request,\n printerId: string\n ) => FilamentAccess | Promise<FilamentAccess>;\n basePath?: string;\n}\n\nconst slotInput = z.object({ slotId: z.string().min(1) }).strict();\nconst assignInput = slotInput.extend({ materialId: z.string().min(1) });\nconst clearInput = slotInput.extend({\n expectedVersion: z\n .object({ id: z.string().min(1), updatedAt: z.string().nullable() })\n .strict(),\n});\nconst routeSchema = z.object({\n printerId: z.string().uuid(),\n action: z.enum([\"read\", \"catalog\", \"assign\", \"clear\", \"refresh-rfid\"]),\n});\n\nfunction slots(layout: PrinterMaterialLayout): FilamentLayoutSlot[] {\n return [\n ...layout.nozzles.flatMap((nozzle) =>\n nozzle.sources.flatMap((source) => source.slots)\n ),\n ...layout.unboundSources.flatMap((source) => source.slots),\n ...layout.unmappedAssignments,\n ];\n}\n\nfunction readOnly(layout: PrinterMaterialLayout): PrinterMaterialLayout {\n const copy = structuredClone(layout);\n const unavailable: FilamentLayoutSlot[\"capabilities\"][\"assign\"] = {\n kind: \"unavailable\",\n code: \"permission_required\",\n message: \"Your connection has read-only access.\",\n };\n for (const slot of slots(copy)) {\n slot.capabilities = {\n assign: unavailable,\n clearAssignment: unavailable,\n writePrinterMetadata: unavailable,\n refreshRfid: unavailable,\n };\n }\n return copy;\n}\n\nfunction target(\n layout: PrinterMaterialLayout,\n slotId: string,\n action: keyof FilamentLayoutSlot[\"capabilities\"]\n): FilamentLayoutSlot {\n const slot = slots(layout).find((candidate) => candidate.id === slotId);\n if (!slot)\n throw new FilamentManagementError(\n \"This slot is no longer available. Refresh the printer.\",\n { code: \"slot_not_present\", status: 404 }\n );\n const capability = slot.capabilities[action];\n if (capability.kind === \"unavailable\") {\n throw new FilamentManagementError(capability.message, {\n code: capability.code,\n status: capability.code === \"permission_required\" ? 403 : 409,\n });\n }\n return slot;\n}\n\nfunction route(request: Request, basePath: string) {\n const prefix = `${basePath}/printers/`;\n const path = new URL(request.url).pathname;\n if (!path.startsWith(prefix)) return null;\n const [id, resource, suffix, ...extra] = path.slice(prefix.length).split(\"/\");\n if (resource !== \"filaments\" || extra.length || !id) return null;\n const action = suffix || \"read\";\n let printerId: string;\n try {\n printerId = decodeURIComponent(id);\n } catch {\n return null;\n }\n const parsed = routeSchema.safeParse({ printerId, action });\n if (!parsed.success) return null;\n const expectedMethod =\n action === \"read\" || action === \"catalog\" ? \"GET\" : \"POST\";\n if (request.method !== expectedMethod)\n throw new FilamentManagementError(\"This request method is not supported.\", {\n code: \"method_not_allowed\",\n status: 405,\n });\n if (\n expectedMethod === \"POST\" &&\n request.headers.get(\"content-type\")?.split(\";\")[0]?.trim() !==\n \"application/json\"\n ) {\n throw new FilamentManagementError(\"Send filament changes as JSON.\", {\n code: \"invalid_request\",\n status: 415,\n });\n }\n return parsed.data;\n}\n\nasync function body<T>(request: Request, schema: z.ZodType<T>): Promise<T> {\n const value: unknown = await request.json().catch(() => null);\n const result = schema.safeParse(value);\n if (!result.success)\n throw new FilamentManagementError(\"The filament request is invalid.\", {\n code: \"invalid_request\",\n status: 400,\n });\n return result.data;\n}\n\nconst messages = new Map([\n [\n \"assignment_changed\",\n \"This assignment changed elsewhere. Refresh before clearing it.\",\n ],\n [\"printer_busy\", \"The printer is busy. Try again when it is available.\"],\n [\"printer_not_found\", \"This printer is not available to your connection.\"],\n [\n \"slot_not_present\",\n \"This slot is no longer available. Refresh the printer.\",\n ],\n [\n \"filament_at_extruder\",\n \"RFID cannot be refreshed while filament is at the extruder.\",\n ],\n [\n \"filament_state_unknown\",\n \"The filament state is unknown. Refresh the printer before reading RFID.\",\n ],\n [\"rfid_protocol_unsupported\", \"This printer does not support RFID refresh.\"],\n [\"external_rfid_unsupported\", \"External spools do not support RFID refresh.\"],\n [\n \"printer_metadata_unsupported\",\n \"This printer does not support writing filament metadata.\",\n ],\n]);\n\nfunction failure(error: FilamentManagementError | PrintHiveApiError): Response {\n if (error instanceof FilamentManagementError) {\n return Response.json(\n { error: error.code, message: error.message },\n { status: error.status ?? 500 }\n );\n }\n if (error instanceof PrintHiveApiError) {\n return Response.json(\n {\n error: messages.has(error.code) ? error.code : \"request_failed\",\n message:\n messages.get(error.code) ??\n \"The filament request could not be completed. Try again.\",\n },\n { status: error.status }\n );\n }\n return Response.json(\n {\n error: \"service_unavailable\",\n message: \"The filament service is unavailable. Try again.\",\n },\n { status: 503 }\n );\n}\n\nasync function operate(\n client: PrintHive,\n request: Request,\n printerId: string,\n action: Action,\n access: Exclude<FilamentAccess, null>\n): Promise<FilamentResponse> {\n if (action === \"catalog\") {\n const query = new URL(request.url).searchParams.get(\"q\") ?? \"\";\n return client.filaments.search({\n query,\n limit: 20,\n signal: request.signal,\n });\n }\n if (action === \"read\") {\n const layout = requirePrinterLayout(\n await client.printers.materialSlots.layout(printerId, request.signal),\n printerId\n );\n return access === \"read\" ? readOnly(layout) : layout;\n }\n if (action === \"assign\") {\n const input = await body(request, assignInput);\n const layout = requirePrinterLayout(\n await client.printers.materialSlots.layout(printerId, request.signal),\n printerId\n );\n const slot = target(layout, input.slotId, \"assign\");\n return client.printers.materialSlots.assign(\n printerId,\n {\n syncPolicy: \"automatic\",\n slots: [{ position: slot.position, materialId: input.materialId }],\n },\n request.signal\n );\n }\n if (action === \"clear\") {\n const input = await body(request, clearInput);\n const layout = requirePrinterLayout(\n await client.printers.materialSlots.layout(printerId, request.signal),\n printerId\n );\n const slot = target(layout, input.slotId, \"clearAssignment\");\n if (!slot.assignment?.version) {\n throw new FilamentManagementError(\n \"This assignment changed elsewhere. Refresh before clearing it.\",\n { code: \"assignment_changed\", status: 409 }\n );\n }\n return client.printers.materialSlots.clearAssignments(\n printerId,\n [slot.position],\n { expectedVersion: input.expectedVersion, signal: request.signal }\n );\n }\n const input = await body(request, slotInput);\n const layout = requirePrinterLayout(\n await client.printers.materialSlots.layout(printerId, request.signal),\n printerId\n );\n const slot = target(layout, input.slotId, \"refreshRfid\");\n return client.printers.materialSlots.refreshRfid(\n printerId,\n slot.position,\n request.signal\n );\n}\n\nexport function createFilamentManagementHandler(\n options: FilamentManagementHandlerOptions\n): (request: Request) => Promise<Response> {\n const basePath = (options.basePath ?? \"/api/printhive\").replace(/\\/+$/, \"\");\n if (basePath && !basePath.startsWith(\"/\"))\n throw new TypeError(\"basePath must start with /\");\n return async (request) => {\n try {\n const parsed = route(request, basePath);\n if (!parsed)\n return Response.json(\n {\n error: \"not_found\",\n message: \"This filament route does not exist.\",\n },\n { status: 404 }\n );\n const access = await options.authorize(request, parsed.printerId);\n if (\n (access !== \"read\" && access !== \"control\") ||\n (access !== \"control\" &&\n parsed.action !== \"read\" &&\n parsed.action !== \"catalog\")\n ) {\n return Response.json(\n {\n error: \"permission_required\",\n message: \"You do not have access to this filament operation.\",\n },\n { status: 403 }\n );\n }\n return Response.json(\n await operate(\n options.client,\n request,\n parsed.printerId,\n parsed.action,\n access\n ),\n { headers: { \"cache-control\": \"no-store\" } }\n );\n } catch (error) {\n return failure(\n error instanceof FilamentManagementError ||\n error instanceof PrintHiveApiError\n ? error\n : new FilamentManagementError(\n \"The filament service is unavailable. Try again.\",\n { code: \"service_unavailable\", status: 503 }\n )\n );\n }\n };\n}\n","export class FilamentManagementError extends Error {\n readonly code: string;\n readonly status: number | undefined;\n\n constructor(message: string, options: { code: string; status?: number }) {\n super(message);\n this.name = \"FilamentManagementError\";\n this.code = options.code;\n this.status = options.status;\n }\n}\n","import type { PrinterMaterialLayout } from \"@printhive/api\";\nimport { FilamentManagementError } from \"./errors.js\";\n\nexport function requirePrinterLayout(\n layout: PrinterMaterialLayout,\n printerId: string\n): PrinterMaterialLayout {\n if (layout.printer.id.toLowerCase() !== printerId.toLowerCase()) {\n throw new FilamentManagementError(\n \"The filament service returned a different printer. Refresh before making changes.\",\n { code: \"invalid_response\", status: 502 }\n );\n }\n return layout;\n}\n"],"mappings":"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","names":["layout","input","slot"]}
@@ -0,0 +1,11 @@
1
+ import { PrintHive } from '@printhive/api';
2
+
3
+ type FilamentAccess = "read" | "control" | null;
4
+ interface FilamentManagementHandlerOptions {
5
+ client: PrintHive;
6
+ authorize: (request: Request, printerId: string) => FilamentAccess | Promise<FilamentAccess>;
7
+ basePath?: string;
8
+ }
9
+ declare function createFilamentManagementHandler(options: FilamentManagementHandlerOptions): (request: Request) => Promise<Response>;
10
+
11
+ export { type FilamentManagementHandlerOptions, createFilamentManagementHandler };
@@ -0,0 +1,11 @@
1
+ import { PrintHive } from '@printhive/api';
2
+
3
+ type FilamentAccess = "read" | "control" | null;
4
+ interface FilamentManagementHandlerOptions {
5
+ client: PrintHive;
6
+ authorize: (request: Request, printerId: string) => FilamentAccess | Promise<FilamentAccess>;
7
+ basePath?: string;
8
+ }
9
+ declare function createFilamentManagementHandler(options: FilamentManagementHandlerOptions): (request: Request) => Promise<Response>;
10
+
11
+ export { type FilamentManagementHandlerOptions, createFilamentManagementHandler };