Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/viewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"unist-util-visit": "^4.1.2"
},
"devDependencies": {
"@cloudflare/workers-types": "4.20260627.1",
"@sveltejs/adapter-cloudflare": "^7.2.9",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
Expand Down
8 changes: 6 additions & 2 deletions packages/viewer/src/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ export type ServerEvent =
}
| {
// One per bundle sync. `changed` is true on a 200 (full parse+transform),
// false on a 304. Counts size the parse/transform work; `ms` is the (valid)
// network fetch duration.
// false on a 304. `cached` is true when the request was served from this
// isolate's in-memory bundle cache (a warm isolate revalidating via ETag)
// instead of parsing fresh — the signal that the cache is doing its job;
// group by it to read the warm-hit rate. Counts size the parse/transform
// work; `ms` is the (valid) network fetch duration.
evt: 'bundle_sync'
changed: boolean
cached: boolean
spaces: number
properties: number
theorems: number
Expand Down
76 changes: 65 additions & 11 deletions packages/viewer/src/gateway.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { browser } from '$app/environment'
import * as pb from '@pi-base/core'

import { Id, type Property, type Space, type Trait } from './models'
Expand All @@ -18,20 +19,50 @@ export type Result = {
sha: string
}

// Isolate-level cache of the parsed + transformed bundle, keyed by source. A
// warm Cloudflare Worker isolate serves many SSR requests; caching here lets
// them reuse the parse/transform work and revalidate cheaply against the S3
// ETag (a conditional request → 304) instead of re-downloading and re-parsing
// on every request. We only cache during SSR — client-side the store already
// persists to localStorage.
//
// Bounded so a parsed bundle (the whole dataset) can't accumulate unbounded in
// a long-lived isolate: a site serves a single host/branch, so in practice this
// holds one entry, and we keep at most the few most-recently used.
const SSR_CACHE_MAX = 2
const ssrCache = new Map<string, { etag: string; result: Result }>()

function cacheBundle(key: string, etag: string, result: Result): void {
// Re-insert to mark most-recently used, then evict the oldest over the cap.
ssrCache.delete(key)
ssrCache.set(key, { etag, result })
if (ssrCache.size > SSR_CACHE_MAX) {
const oldest = ssrCache.keys().next().value
if (oldest !== undefined) {
ssrCache.delete(oldest)
}
}
}

export function sync(
fetch: (input: RequestInfo, init?: RequestInit) => Promise<Response>,
bundle?: pb.Bundle,
): Sync {
return async (host: string, branch: string, etag?: string) => {
trace({ event: 'remote_fetch_started', host, branch })

// A warm isolate may already hold the parsed bundle; pass its ETag so an
// unchanged source costs a conditional request instead of a re-parse. An
// injected bundle (tests/prerender) is never cached against the live source.
const key = `${host}/${branch}`
const warm = browser || bundle ? undefined : ssrCache.get(key)

// `ms` wraps the network fetch — real I/O, so the Workers clock advances and
// this is a trustworthy duration (unlike CPU-bound timings). Faithful to
// current behaviour: there is no isolate bundle cache, so every SSR request
// re-fetches and (on a 200) re-parses/transforms the whole bundle.
// this is a trustworthy duration (unlike CPU-bound timings).
const startedAt = Date.now()
const result = bundle
? { bundle, etag: 'etag' }
: await pb.bundle.fetch({ host, branch, etag, fetch })
: await pb.bundle.fetch({ host, branch, etag: etag ?? warm?.etag, fetch })
const ms = Date.now() - startedAt

if (result) {
Expand All @@ -40,25 +71,37 @@ export function sync(
serverLog({
evt: 'bundle_sync',
changed: true,
cached: false,
spaces: spaces.size,
properties: properties.size,
theorems: theorems.size,
traits: traits.size,
ms,
})
return {
spaces: transform(space, result.bundle.spaces),
properties: transform(property, result.bundle.properties),
traits: transform(trait, result.bundle.traits),
theorems: transform(theorem, result.bundle.theorems),
etag: result.etag,
sha: result.bundle.version.sha,
const transformed = build(result.bundle, result.etag)
if (!browser && !bundle) {
cacheBundle(key, result.etag, transformed)
}
return transformed
} else if (warm) {
// 304 Not Modified — reuse the transform this isolate already parsed.
serverLog({
evt: 'bundle_sync',
changed: false,
cached: true,
spaces: 0,
properties: 0,
theorems: 0,
traits: 0,
ms,
})
return warm.result
} else if (etag) {
trace({ event: 'bundle_unchanged', etag })
serverLog({
evt: 'bundle_sync',
changed: false,
cached: false,
spaces: 0,
properties: 0,
theorems: 0,
Expand All @@ -69,6 +112,17 @@ export function sync(
}
}

function build(bundle: pb.Bundle, etag: string): Result {
return {
spaces: transform(space, bundle.spaces),
properties: transform(property, bundle.properties),
traits: transform(trait, bundle.traits),
theorems: transform(theorem, bundle.theorems),
etag,
sha: bundle.version.sha,
}
}

function property({
uid,
name,
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading