فهرست منبع

feat: better code and diff perf

Adam 4 ماه پیش
والد
کامیت
9363c15b4a

+ 4 - 2
bun.lock

@@ -458,7 +458,7 @@
     "@hono/zod-validator": "0.4.2",
     "@hono/zod-validator": "0.4.2",
     "@kobalte/core": "0.13.11",
     "@kobalte/core": "0.13.11",
     "@openauthjs/openauth": "0.0.0-20250322224806",
     "@openauthjs/openauth": "0.0.0-20250322224806",
-    "@pierre/precision-diffs": "0.6.0-beta.3",
+    "@pierre/precision-diffs": "0.6.0-beta.10",
     "@solidjs/meta": "0.29.4",
     "@solidjs/meta": "0.29.4",
     "@solidjs/router": "0.15.4",
     "@solidjs/router": "0.15.4",
     "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
     "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
@@ -1273,7 +1273,7 @@
 
 
     "@petamoriken/float16": ["@petamoriken/[email protected]", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="],
     "@petamoriken/float16": ["@petamoriken/[email protected]", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="],
 
 
-    "@pierre/precision-diffs": ["@pierre/[email protected].3", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/transformers": "3.15.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.15.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-1FBm9jhLWZvs7BqN3yG2Wh9SpGuO1us2QsKZlQqSwyCctMr9DRGzYQJ9lF6yR03LHzXs3fuIzO++d9sCObYzrQ=="],
+    "@pierre/precision-diffs": ["@pierre/[email protected].10", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/transformers": "3.15.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.15.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-2rdd1Q1xJbB0Z4oUbm0Ybrr2gLFEdvNetZLadJboZSFL7Q4gFujdQZfXfV3vB9X+esjt++v0nzb3mioW25BOTA=="],
 
 
     "@pkgjs/parseargs": ["@pkgjs/[email protected]", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
     "@pkgjs/parseargs": ["@pkgjs/[email protected]", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
 
 
@@ -2819,6 +2819,8 @@
 
 
     "lru.min": ["[email protected]", "", {}, "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q=="],
     "lru.min": ["[email protected]", "", {}, "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q=="],
 
 
+    "lru_map": ["[email protected]", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="],
+
     "luxon": ["[email protected]", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="],
     "luxon": ["[email protected]", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="],
 
 
     "magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
     "magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],

+ 1 - 1
package.json

@@ -30,7 +30,7 @@
       "@tsconfig/bun": "1.0.9",
       "@tsconfig/bun": "1.0.9",
       "@cloudflare/workers-types": "4.20251008.0",
       "@cloudflare/workers-types": "4.20251008.0",
       "@openauthjs/openauth": "0.0.0-20250322224806",
       "@openauthjs/openauth": "0.0.0-20250322224806",
-      "@pierre/precision-diffs": "0.6.0-beta.3",
+      "@pierre/precision-diffs": "0.6.0-beta.10",
       "@tailwindcss/vite": "4.1.11",
       "@tailwindcss/vite": "4.1.11",
       "diff": "8.0.2",
       "diff": "8.0.2",
       "ai": "5.0.97",
       "ai": "5.0.97",

+ 1 - 1
packages/desktop/src/DesktopInterface.tsx

@@ -13,7 +13,7 @@ import Session from "@/pages/session"
 import { LayoutProvider } from "./context/layout"
 import { LayoutProvider } from "./context/layout"
 import { GlobalSDKProvider } from "./context/global-sdk"
 import { GlobalSDKProvider } from "./context/global-sdk"
 import { SessionProvider } from "./context/session"
 import { SessionProvider } from "./context/session"
-import { base64Encode } from "./utils"
+import { base64Encode } from "@opencode-ai/util/encode"
 import { createMemo, Show } from "solid-js"
 import { createMemo, Show } from "solid-js"
 
 
 const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1"
 const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1"

+ 1 - 1
packages/desktop/src/context/local.tsx

@@ -5,7 +5,7 @@ import type { FileContent, FileNode, Model, Provider, File as FileStatus } from
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useSDK } from "./sdk"
 import { useSDK } from "./sdk"
 import { useSync } from "./sync"
 import { useSync } from "./sync"
-import { base64Encode } from "@/utils"
+import { base64Encode } from "@opencode-ai/util/encode"
 
 
 export type LocalFile = FileNode &
 export type LocalFile = FileNode &
   Partial<{
   Partial<{

+ 1 - 1
packages/desktop/src/context/session.tsx

@@ -7,7 +7,7 @@ import { TextSelection } from "./local"
 import { pipe, sumBy } from "remeda"
 import { pipe, sumBy } from "remeda"
 import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
 import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
 import { useParams } from "@solidjs/router"
 import { useParams } from "@solidjs/router"
-import { base64Encode } from "@/utils"
+import { base64Encode } from "@opencode-ai/util/encode"
 import { useSDK } from "./sdk"
 import { useSDK } from "./sdk"
 
 
 export type LocalPTY = {
 export type LocalPTY = {

+ 1 - 1
packages/desktop/src/pages/directory-layout.tsx

@@ -4,7 +4,7 @@ import { SDKProvider } from "@/context/sdk"
 import { SyncProvider, useSync } from "@/context/sync"
 import { SyncProvider, useSync } from "@/context/sync"
 import { LocalProvider } from "@/context/local"
 import { LocalProvider } from "@/context/local"
 import { useGlobalSync } from "@/context/global-sync"
 import { useGlobalSync } from "@/context/global-sync"
-import { base64Decode } from "@/utils"
+import { base64Decode } from "@opencode-ai/util/encode"
 import { DataProvider } from "@opencode-ai/ui/context"
 import { DataProvider } from "@opencode-ai/ui/context"
 import { iife } from "@opencode-ai/util/iife"
 import { iife } from "@opencode-ai/util/iife"
 
 

+ 1 - 1
packages/desktop/src/pages/home.tsx

@@ -1,5 +1,5 @@
 import { useGlobalSync } from "@/context/global-sync"
 import { useGlobalSync } from "@/context/global-sync"
-import { base64Encode } from "@/utils"
+import { base64Encode } from "@opencode-ai/util/encode"
 import { For } from "solid-js"
 import { For } from "solid-js"
 import { A } from "@solidjs/router"
 import { A } from "@solidjs/router"
 import { Button } from "@opencode-ai/ui/button"
 import { Button } from "@opencode-ai/ui/button"

+ 1 - 1
packages/desktop/src/pages/layout.tsx

@@ -3,7 +3,7 @@ import { DateTime } from "luxon"
 import { A, useNavigate, useParams } from "@solidjs/router"
 import { A, useNavigate, useParams } from "@solidjs/router"
 import { useLayout } from "@/context/layout"
 import { useLayout } from "@/context/layout"
 import { useGlobalSync } from "@/context/global-sync"
 import { useGlobalSync } from "@/context/global-sync"
-import { base64Decode, base64Encode } from "@/utils"
+import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
 import { Mark } from "@opencode-ai/ui/logo"
 import { Mark } from "@opencode-ai/ui/logo"
 import { Button } from "@opencode-ai/ui/button"
 import { Button } from "@opencode-ai/ui/button"
 import { Icon } from "@opencode-ai/ui/icon"
 import { Icon } from "@opencode-ai/ui/icon"

+ 6 - 1
packages/desktop/src/pages/session.tsx

@@ -31,6 +31,7 @@ import { useSession } from "@/context/session"
 import { useLayout } from "@/context/layout"
 import { useLayout } from "@/context/layout"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
 import { Terminal } from "@/components/terminal"
 import { Terminal } from "@/components/terminal"
+import { checksum } from "@opencode-ai/util/encode"
 
 
 export default function Page() {
 export default function Page() {
   const layout = useLayout()
   const layout = useLayout()
@@ -489,7 +490,11 @@ export default function Page() {
                       <Match when={file()}>
                       <Match when={file()}>
                         {(f) => (
                         {(f) => (
                           <Code
                           <Code
-                            file={{ name: f().path, contents: f().content?.content ?? "" }}
+                            file={{
+                              name: f().path,
+                              contents: f().content?.content ?? "",
+                              cacheKey: checksum(f().content?.content ?? ""),
+                            }}
                             overflow="scroll"
                             overflow="scroll"
                             class="pb-40"
                             class="pb-40"
                           />
                           />

+ 0 - 7
packages/desktop/src/utils/encode.ts

@@ -1,7 +0,0 @@
-export function base64Encode(value: string) {
-  return btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
-}
-
-export function base64Decode(value: string) {
-  return atob(value.replace(/-/g, "+").replace(/_/g, "/"))
-}

+ 0 - 1
packages/desktop/src/utils/index.ts

@@ -1,2 +1 @@
 export * from "./dom"
 export * from "./dom"
-export * from "./encode"

+ 1 - 1
packages/enterprise/src/routes/share/[shareID].tsx

@@ -354,7 +354,7 @@ export default function () {
                                   Session
                                   Session
                                 </Tabs.Trigger>
                                 </Tabs.Trigger>
                                 <Tabs.Trigger value="review" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
                                 <Tabs.Trigger value="review" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
-                                  5 Files Changed
+                                  {diffs().length} Files Changed
                                 </Tabs.Trigger>
                                 </Tabs.Trigger>
                               </Tabs.List>
                               </Tabs.List>
                               <Tabs.Content value="session" class="!overflow-hidden">
                               <Tabs.Content value="session" class="!overflow-hidden">

+ 14 - 26
packages/ui/src/components/code.tsx

@@ -1,22 +1,7 @@
 import { type FileContents, File, FileOptions, LineAnnotation } from "@pierre/precision-diffs"
 import { type FileContents, File, FileOptions, LineAnnotation } from "@pierre/precision-diffs"
-import { ComponentProps, createEffect, splitProps } from "solid-js"
+import { ComponentProps, createEffect, createMemo, splitProps } from "solid-js"
 import { createDefaultOptions, styleVariables } from "../pierre"
 import { createDefaultOptions, styleVariables } from "../pierre"
-import { getOrCreateWorkerPoolSingleton } from "@pierre/precision-diffs/worker"
-import { workerFactory } from "../pierre/worker"
-
-const workerPool = getOrCreateWorkerPoolSingleton({
-  poolOptions: {
-    workerFactory,
-    // poolSize defaults to 8. More workers = more parallelism but
-    // also more memory. Too many can actually slow things down.
-    // poolSize: 8,
-  },
-  highlighterOptions: {
-    theme: "OpenCode",
-    // Optionally preload languages to avoid lazy-loading delays
-    // langs: ["typescript", "javascript", "css", "html"],
-  },
-})
+import { workerPool } from "../pierre/worker"
 
 
 export type CodeProps<T = {}> = FileOptions<T> & {
 export type CodeProps<T = {}> = FileOptions<T> & {
   file: FileContents
   file: FileContents
@@ -29,17 +14,20 @@ export function Code<T>(props: CodeProps<T>) {
   let container!: HTMLDivElement
   let container!: HTMLDivElement
   const [local, others] = splitProps(props, ["file", "class", "classList", "annotations"])
   const [local, others] = splitProps(props, ["file", "class", "classList", "annotations"])
 
 
-  createEffect(() => {
-    const instance = new File<T>(
-      {
-        ...createDefaultOptions<T>("unified"),
-        ...others,
-      },
-      workerPool,
-    )
+  const file = createMemo(
+    () =>
+      new File<T>(
+        {
+          ...createDefaultOptions<T>("unified"),
+          ...others,
+        },
+        workerPool,
+      ),
+  )
 
 
+  createEffect(() => {
     container.innerHTML = ""
     container.innerHTML = ""
-    instance.render({
+    file().render({
       file: local.file,
       file: local.file,
       lineAnnotations: local.annotations,
       lineAnnotations: local.annotations,
       containerWrapper: container,
       containerWrapper: container,

+ 14 - 28
packages/ui/src/components/diff.tsx

@@ -1,22 +1,7 @@
 import { FileDiff } from "@pierre/precision-diffs"
 import { FileDiff } from "@pierre/precision-diffs"
-import { getOrCreateWorkerPoolSingleton } from "@pierre/precision-diffs/worker"
-import { createEffect, onCleanup, splitProps } from "solid-js"
+import { createEffect, createMemo, onCleanup, splitProps } from "solid-js"
 import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre"
 import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre"
-import { workerFactory } from "../pierre/worker"
-
-const workerPool = getOrCreateWorkerPoolSingleton({
-  poolOptions: {
-    workerFactory,
-    // poolSize defaults to 8. More workers = more parallelism but
-    // also more memory. Too many can actually slow things down.
-    // poolSize: 8,
-  },
-  highlighterOptions: {
-    theme: "OpenCode",
-    // Optionally preload languages to avoid lazy-loading delays
-    // langs: ["typescript", "javascript", "css", "html"],
-  },
-})
+import { workerPool } from "../pierre/worker"
 
 
 // interface ThreadMetadata {
 // interface ThreadMetadata {
 //   threadId: string
 //   threadId: string
@@ -28,21 +13,22 @@ export function Diff<T>(props: DiffProps<T>) {
   let container!: HTMLDivElement
   let container!: HTMLDivElement
   const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations"])
   const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations"])
 
 
-  let fileDiffInstance: FileDiff<T> | undefined
-  const cleanupFunctions: Array<() => void> = []
-
-  createEffect(() => {
-    container.innerHTML = ""
-    if (!fileDiffInstance) {
-      fileDiffInstance = new FileDiff<T>(
+  const fileDiff = createMemo(
+    () =>
+      new FileDiff<T>(
         {
         {
           ...createDefaultOptions(props.diffStyle),
           ...createDefaultOptions(props.diffStyle),
           ...others,
           ...others,
         },
         },
         workerPool,
         workerPool,
-      )
-    }
-    fileDiffInstance.render({
+      ),
+  )
+
+  const cleanupFunctions: Array<() => void> = []
+
+  createEffect(() => {
+    container.innerHTML = ""
+    fileDiff().render({
       oldFile: local.before,
       oldFile: local.before,
       newFile: local.after,
       newFile: local.after,
       lineAnnotations: local.annotations,
       lineAnnotations: local.annotations,
@@ -52,7 +38,7 @@ export function Diff<T>(props: DiffProps<T>) {
 
 
   onCleanup(() => {
   onCleanup(() => {
     // Clean up FileDiff event handlers and dispose SolidJS components
     // Clean up FileDiff event handlers and dispose SolidJS components
-    fileDiffInstance?.cleanUp()
+    fileDiff()?.cleanUp()
     cleanupFunctions.forEach((dispose) => dispose())
     cleanupFunctions.forEach((dispose) => dispose())
   })
   })
 
 

+ 3 - 0
packages/ui/src/components/session-review.tsx

@@ -11,6 +11,7 @@ import { createStore } from "solid-js/store"
 import { type FileDiff } from "@opencode-ai/sdk"
 import { type FileDiff } from "@opencode-ai/sdk"
 import { PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
 import { PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
 import { Dynamic } from "solid-js/web"
 import { Dynamic } from "solid-js/web"
+import { checksum } from "@opencode-ai/util/encode"
 
 
 export interface SessionReviewProps {
 export interface SessionReviewProps {
   split?: boolean
   split?: boolean
@@ -105,10 +106,12 @@ export const SessionReview = (props: SessionReviewProps) => {
                     before={{
                     before={{
                       name: diff.file!,
                       name: diff.file!,
                       contents: diff.before!,
                       contents: diff.before!,
+                      cacheKey: checksum(diff.before),
                     }}
                     }}
                     after={{
                     after={{
                       name: diff.file!,
                       name: diff.file!,
                       contents: diff.after!,
                       contents: diff.after!,
+                      cacheKey: checksum(diff.after),
                     }}
                     }}
                   />
                   />
                 </Accordion.Content>
                 </Accordion.Content>

+ 3 - 0
packages/ui/src/components/session-turn.tsx

@@ -3,6 +3,7 @@ import { useData } from "../context"
 import { useDiffComponent } from "../context/diff"
 import { useDiffComponent } from "../context/diff"
 import { Binary } from "@opencode-ai/util/binary"
 import { Binary } from "@opencode-ai/util/binary"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
+import { checksum } from "@opencode-ai/util/encode"
 import { createEffect, createMemo, createSignal, For, Match, onMount, ParentProps, Show, Switch } from "solid-js"
 import { createEffect, createMemo, createSignal, For, Match, onMount, ParentProps, Show, Switch } from "solid-js"
 import { DiffChanges } from "./diff-changes"
 import { DiffChanges } from "./diff-changes"
 import { Typewriter } from "./typewriter"
 import { Typewriter } from "./typewriter"
@@ -174,10 +175,12 @@ export function SessionTurn(
                                 before={{
                                 before={{
                                   name: diff.file!,
                                   name: diff.file!,
                                   contents: diff.before!,
                                   contents: diff.before!,
+                                  cacheKey: checksum(diff.before!),
                                 }}
                                 }}
                                 after={{
                                 after={{
                                   name: diff.file!,
                                   name: diff.file!,
                                   contents: diff.after!,
                                   contents: diff.after!,
+                                  cacheKey: checksum(diff.after!),
                                 }}
                                 }}
                               />
                               />
                             </Accordion.Content>
                             </Accordion.Content>

+ 18 - 16
packages/ui/src/pierre/index.ts

@@ -9,21 +9,7 @@ export type DiffProps<T = {}> = FileDiffOptions<T> & {
   classList?: ComponentProps<"div">["classList"]
   classList?: ComponentProps<"div">["classList"]
 }
 }
 
 
-export function createDefaultOptions<T>(style: FileDiffOptions<T>["diffStyle"]) {
-  return {
-    theme: "OpenCode",
-    themeType: "system",
-    disableLineNumbers: false,
-    overflow: "wrap",
-    diffStyle: style ?? "unified",
-    diffIndicators: "bars",
-    disableBackground: false,
-    expansionLineCount: 20,
-    lineDiffType: style === "split" ? "word-alt" : "none",
-    maxLineDiffLength: 1000,
-    maxLineLengthForHighlighting: 1000,
-    disableFileHeader: true,
-    unsafeCSS: `
+const unsafeCSS = `
 [data-pjs-header],
 [data-pjs-header],
 [data-pjs] {
 [data-pjs] {
   [data-separator-wrapper] {
   [data-separator-wrapper] {
@@ -46,7 +32,23 @@ export function createDefaultOptions<T>(style: FileDiffOptions<T>["diffStyle"])
   [data-separator-content] {
   [data-separator-content] {
     height: 24px !important;
     height: 24px !important;
   }
   }
-}`,
+}`
+
+export function createDefaultOptions<T>(style: FileDiffOptions<T>["diffStyle"]) {
+  return {
+    theme: "OpenCode",
+    themeType: "system",
+    disableLineNumbers: false,
+    overflow: "wrap",
+    diffStyle: style ?? "unified",
+    diffIndicators: "bars",
+    disableBackground: false,
+    expansionLineCount: 20,
+    lineDiffType: style === "split" ? "word-alt" : "none",
+    maxLineDiffLength: 1000,
+    maxLineLengthForHighlighting: 1000,
+    disableFileHeader: true,
+    unsafeCSS,
     // hunkSeparators(hunkData: HunkData) {
     // hunkSeparators(hunkData: HunkData) {
     //   const fragment = document.createDocumentFragment()
     //   const fragment = document.createDocumentFragment()
     //   const numCol = document.createElement("div")
     //   const numCol = document.createElement("div")

+ 16 - 1
packages/ui/src/pierre/worker.ts

@@ -1,5 +1,20 @@
-import ShikiWorkerUrl from "@pierre/precision-diffs/worker/shiki-worker.js?worker&url"
+import { getOrCreateWorkerPoolSingleton } from "@pierre/precision-diffs/worker"
+import ShikiWorkerUrl from "@pierre/precision-diffs/worker/worker.js?worker&url"
 
 
 export function workerFactory(): Worker {
 export function workerFactory(): Worker {
   return new Worker(ShikiWorkerUrl, { type: "module" })
   return new Worker(ShikiWorkerUrl, { type: "module" })
 }
 }
+
+export const workerPool = getOrCreateWorkerPoolSingleton({
+  poolOptions: {
+    workerFactory,
+    // poolSize defaults to 8. More workers = more parallelism but
+    // also more memory. Too many can actually slow things down.
+    // poolSize: 8,
+  },
+  highlighterOptions: {
+    theme: "OpenCode",
+    // Optionally preload languages to avoid lazy-loading delays
+    // langs: ["typescript", "javascript", "css", "html"],
+  },
+})

+ 25 - 0
packages/util/src/encode.ts

@@ -0,0 +1,25 @@
+export function base64Encode(value: string) {
+  return btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
+}
+
+export function base64Decode(value: string) {
+  return atob(value.replace(/-/g, "+").replace(/_/g, "/"))
+}
+
+export async function hash(content: string, algorithm = "SHA-256"): Promise<string> {
+  const encoder = new TextEncoder()
+  const data = encoder.encode(content)
+  const hashBuffer = await crypto.subtle.digest(algorithm, data)
+  const hashArray = Array.from(new Uint8Array(hashBuffer))
+  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")
+  return hashHex
+}
+
+export function checksum(content: string): string {
+  let hash = 0x811c9dc5
+  for (let i = 0; i < content.length; i++) {
+    hash ^= content.charCodeAt(i)
+    hash = Math.imul(hash, 0x01000193)
+  }
+  return (hash >>> 0).toString(36)
+}