298 lines · 8.9 KB · TypeScript
RawDownload
/** * Preview - Schemas for the in-app browser preview surface. * * The preview is desktop-only (Chromium <webview>); the server tracks per-thread * tab metadata so it survives client reconnects and multi-window. The desktop * renderer mediates: it owns the actual <webview> and reports navigation back to * the server via these RPCs, the server fans events to all subscribers. * * @module Preview */import { Schema } from "effect";import { ThreadId, TrimmedNonEmptyString } from "./baseSchemas.ts"; const Url = TrimmedNonEmptyString.check(Schema.isMaxLength(2048));const Title = Schema.String.check(Schema.isMaxLength(512)); export const PreviewTabId = TrimmedNonEmptyString.check(Schema.isMaxLength(128));export type PreviewTabId = typeof PreviewTabId.Type; export const PREVIEW_VIEWPORT_MIN_DIMENSION = 240;export const PREVIEW_VIEWPORT_MAX_DIMENSION = 3840;export const PREVIEW_VIEWPORT_MAX_AREA = 3840 * 2160; const PreviewViewportDimension = Schema.Int.check(  Schema.isBetween({    minimum: PREVIEW_VIEWPORT_MIN_DIMENSION,    maximum: PREVIEW_VIEWPORT_MAX_DIMENSION,  }),); const viewportAreaFilter = Schema.makeFilter(  ({ width, height }: { readonly width: number; readonly height: number }) =>    width * height <= PREVIEW_VIEWPORT_MAX_AREA ||    `Viewport area must not exceed ${PREVIEW_VIEWPORT_MAX_AREA} pixels.`,); export const PreviewViewportSize = Schema.Struct({  width: PreviewViewportDimension,  height: PreviewViewportDimension,}).check(viewportAreaFilter);export type PreviewViewportSize = typeof PreviewViewportSize.Type; /** * The page's measured viewport can be smaller than the minimum selectable * fixed size while fill mode follows a narrow panel. Keep measurement * validation separate from the stricter user-selectable size constraints. */export const PreviewRenderedViewportSize = Schema.Struct({  width: Schema.Int.check(Schema.isGreaterThan(0)),  height: Schema.Int.check(Schema.isGreaterThan(0)),});export type PreviewRenderedViewportSize = typeof PreviewRenderedViewportSize.Type; export const PREVIEW_VIEWPORT_PRESET_IDS = [  "iphone-se",  "iphone-xr",  "iphone-12-pro",  "iphone-14-pro-max",  "pixel-7",  "samsung-galaxy-s8-plus",  "samsung-galaxy-s20-ultra",  "ipad-mini",  "ipad-air",  "ipad-pro",  "surface-pro-7",  "surface-duo",  "galaxy-z-fold-5",  "asus-zenbook-fold",  "samsung-galaxy-a51-71",  "nest-hub",  "nest-hub-max",] as const; export const PreviewViewportPresetId = Schema.Literals(PREVIEW_VIEWPORT_PRESET_IDS);export type PreviewViewportPresetId = typeof PreviewViewportPresetId.Type; /** * Preset IDs shipped before the Chrome-compatible catalog. Existing sessions * can still reconnect with these values, but new resize requests only expose * PREVIEW_VIEWPORT_PRESET_IDS. */const LEGACY_PREVIEW_VIEWPORT_PRESET_IDS = [  "desktop-1920x1080",  "desktop-1440x900",  "laptop-1366x768",  "laptop-1280x800",  "ipad-pro-11",  "iphone-15-pro",  "pixel-8",  "galaxy-s24",] as const; const StoredPreviewViewportPresetId = Schema.Literals([  ...PREVIEW_VIEWPORT_PRESET_IDS,  ...LEGACY_PREVIEW_VIEWPORT_PRESET_IDS,]); export const PreviewViewportSetting = Schema.Union([  Schema.TaggedStruct("fill", {}),  Schema.TaggedStruct("freeform", {    ...PreviewViewportSize.fields,  }).check(viewportAreaFilter),  Schema.TaggedStruct("preset", {    ...PreviewViewportSize.fields,    presetId: StoredPreviewViewportPresetId,  }).check(viewportAreaFilter),]);export type PreviewViewportSetting = typeof PreviewViewportSetting.Type; export const FILL_PREVIEW_VIEWPORT = {  _tag: "fill",} as const satisfies PreviewViewportSetting; export const PreviewNavStatus = Schema.Union([  Schema.TaggedStruct("Idle", {}),  Schema.TaggedStruct("Loading", {    url: Url,    title: Title,  }),  Schema.TaggedStruct("Success", {    url: Url,    title: Title,  }),  Schema.TaggedStruct("LoadFailed", {    url: Url,    title: Title,    code: Schema.Int,    description: Schema.String,  }),]);export type PreviewNavStatus = typeof PreviewNavStatus.Type; export const PreviewSessionSnapshot = Schema.Struct({  threadId: TrimmedNonEmptyString,  tabId: PreviewTabId,  navStatus: PreviewNavStatus,  canGoBack: Schema.Boolean,  canGoForward: Schema.Boolean,  /** Missing snapshots from older servers are treated as fill-panel mode. */  viewport: Schema.optional(PreviewViewportSetting),  updatedAt: Schema.String,});export type PreviewSessionSnapshot = typeof PreviewSessionSnapshot.Type; export const PreviewOpenInput = Schema.Struct({  threadId: ThreadId,  /** Omit to create an empty (Idle) tab the user can type into. */  url: Schema.optional(Url),});export type PreviewOpenInput = typeof PreviewOpenInput.Type; export const PreviewNavigateInput = Schema.Struct({  threadId: ThreadId,  tabId: PreviewTabId,  url: Url,  resolvedTitle: Schema.optional(Title),});export type PreviewNavigateInput = typeof PreviewNavigateInput.Type; export const PreviewReportStatusInput = Schema.Struct({  threadId: ThreadId,  tabId: PreviewTabId,  navStatus: PreviewNavStatus,  canGoBack: Schema.Boolean,  canGoForward: Schema.Boolean,});export type PreviewReportStatusInput = typeof PreviewReportStatusInput.Type; export const PreviewRefreshInput = Schema.Struct({  threadId: ThreadId,  tabId: PreviewTabId,});export type PreviewRefreshInput = typeof PreviewRefreshInput.Type; export const PreviewResizeInput = Schema.Struct({  threadId: ThreadId,  tabId: PreviewTabId,  viewport: PreviewViewportSetting,});export type PreviewResizeInput = typeof PreviewResizeInput.Type; export const PreviewCloseInput = Schema.Struct({  threadId: ThreadId,  tabId: Schema.optional(PreviewTabId),});export type PreviewCloseInput = typeof PreviewCloseInput.Type; export const PreviewListInput = Schema.Struct({  threadId: ThreadId,});export type PreviewListInput = typeof PreviewListInput.Type; export const PreviewListResult = Schema.Struct({  sessions: Schema.Array(PreviewSessionSnapshot),});export type PreviewListResult = typeof PreviewListResult.Type; const PreviewEventBaseSchema = Schema.Struct({  threadId: TrimmedNonEmptyString,  tabId: PreviewTabId,  createdAt: Schema.String,}); const PreviewOpenedEvent = Schema.Struct({  ...PreviewEventBaseSchema.fields,  type: Schema.Literal("opened"),  snapshot: PreviewSessionSnapshot,}); const PreviewNavigatedEvent = Schema.Struct({  ...PreviewEventBaseSchema.fields,  type: Schema.Literal("navigated"),  snapshot: PreviewSessionSnapshot,}); const PreviewResizedEvent = Schema.Struct({  ...PreviewEventBaseSchema.fields,  type: Schema.Literal("resized"),  snapshot: PreviewSessionSnapshot,}); const PreviewFailedEvent = Schema.Struct({  ...PreviewEventBaseSchema.fields,  type: Schema.Literal("failed"),  url: Url,  title: Title,  code: Schema.Int,  description: Schema.String,}); const PreviewClosedEvent = Schema.Struct({  ...PreviewEventBaseSchema.fields,  type: Schema.Literal("closed"),}); export const PreviewEvent = Schema.Union([  PreviewOpenedEvent,  PreviewNavigatedEvent,  PreviewResizedEvent,  PreviewFailedEvent,  PreviewClosedEvent,]);export type PreviewEvent = typeof PreviewEvent.Type; /** * A localhost server detected by the port scanner. Used to populate the * "Local" recommendations in the empty-state of the preview panel. */export const DiscoveredLocalServer = Schema.Struct({  host: TrimmedNonEmptyString,  port: Schema.Int.check(Schema.isGreaterThan(0)).check(Schema.isLessThan(65536)),  url: Url,  processName: Schema.NullOr(TrimmedNonEmptyString),  pid: Schema.NullOr(Schema.Int.check(Schema.isGreaterThan(0))),  terminal: Schema.NullOr(    Schema.Struct({      threadId: ThreadId,      terminalId: TrimmedNonEmptyString,    }),  ),});export type DiscoveredLocalServer = typeof DiscoveredLocalServer.Type; export const DiscoveredLocalServerList = Schema.Struct({  servers: Schema.Array(DiscoveredLocalServer),  scannedAt: Schema.String,});export type DiscoveredLocalServerList = typeof DiscoveredLocalServerList.Type; export class PreviewSessionLookupError extends Schema.TaggedErrorClass<PreviewSessionLookupError>()(  "PreviewSessionLookupError",  {    threadId: Schema.String,    tabId: Schema.String,  },) {  override get message() {    return `Unknown preview session: thread=${this.threadId}, tab=${this.tabId}`;  }} export class PreviewInvalidUrlError extends Schema.TaggedErrorClass<PreviewInvalidUrlError>()(  "PreviewInvalidUrlError",  {    inputLength: Schema.Number,    reason: Schema.Literals(["empty", "parse", "unsupported-protocol", "unexpected"]),    protocol: Schema.optional(Schema.String),    cause: Schema.Defect(),  },) {  override get message() {    const protocol = this.protocol === undefined ? "" : `: ${this.protocol}`;    return `Invalid preview URL (${this.reason}${protocol}; input length ${this.inputLength}).`;  }} export const PreviewError = Schema.Union([PreviewSessionLookupError, PreviewInvalidUrlError]);export type PreviewError = typeof PreviewError.Type;