feat(sdk+demo): SonicJS SDK, demo site fixes, cold-start perf#1006
Merged
Conversation
- New packages/sdk/ — isomorphic TypeScript client: content, documents, media, auth, system resources; keyset iterate(); listWithHeaders() for sonicjs-source cache header; zero runtime deps; dual ESM+CJS build via tsup - CLI codegen: sonicjs-sdk codegen --url --api-key --out emits typed per-collection interfaces from live instance or local config - demos/employee-directory/ — React+Vite SPA using SDK with mock fetch injection; edge vs origin speed toggle; live SDK code snippet sidebar - departments/regions/employees collections added to my-sonicjs-app with department+region as type:reference cross-collection relations - Seed script creates departments → regions → employees with reference IDs - publish.yml + sync-versions.js wired for SDK release alongside core Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Auto-deploys demos/employee-directory/ to sonicjs-sdk-demo.pages.dev on main branch pushes that touch the demo or SDK source. Accepts DEMO_CMS_URL and DEMO_API_KEY secrets to wire a real SonicJS backend when ready. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch from total-count to hasNextPage pattern (meta.count is page size from API, not total); Pagination now shows "X–Y · Page N" - Mock fetch returns page.length (matches real API behavior) - Seed script updated to upsert departments/regions before employees - Add .gitignore for demo dist/ and wrangler-demo.toml (has secrets) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allow "*.example.com" entries in CORS_ORIGINS to match any subdomain, enabling Cloudflare Pages preview deployments (hash-prefixed URLs) to work without listing every preview URL explicitly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…CORS x-api-key header triggered CORS preflight on every request; preflight hitting a cold isolate returned 500, which caused the browser to block the subsequent GET (no employees shown). Public collections need no key. Also regenerate migrations bundle (tsup side-effect from core rebuild). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Storing globalThis.fetch as a reference loses the Window binding; calling it throws "Illegal invocation". Use .bind(globalThis) so the native fetch always runs with the correct receiver. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…aths Three related fixes to make department/region filters work in the demo: 1. parseFromQuery: extend bracket regex to handle where[field][op]=value (was filter-only; SDK sends where-prefixed brackets per the documented DSL) 2. CollectionConfig: add optional queryableFields so collections can declare filterable fields that autoRegisterCollectionDocumentTypes propagates to the document_types row (was always []) 3. App.tsx: use data.department / data.region paths so json_extract(data,'$.field') is used — simple field names map to non-existent top-level columns Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parallelise bootstrap steps, add preloadAll() + schemaChange early-return in DocumentTypeRegistry, module-level column/index cache in document-scalar-schema.ts, COUNT fast-paths in RBAC seed and plugin bootstrap, and a KV skip that bypasses D1 entirely on warm isolates. Demo site: register demoLoginPlugin in my-sonicjs-app, add isDemoModeActive() flag (set by plugin onBoot), wire it to auth.ts login page so prefill script renders. Fixes "Invalid email or password" — demoLoginActive was querying a non-existent plugins table. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Set defaultActive:true on core-cache in manifest-registry so it is auto-bootstrapped as active on new installs and the demo site. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Active per-isolate ensurePluginInstalled now activates any bootstrapped plugin (defaultActive list) found in DB with inactive status — not just core-auth. Fixes core-cache staying inactive after adding defaultActive:true when a plugin document already existed. isPluginActive() result cached in module-level Map per isolate — eliminates D1 round-trip on every API request. invalidatePluginStatusCache() called from activatePlugin/deactivatePlugin so admin UI toggles take effect immediately within the same isolate. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dev:demo now runs setup:db:demo first:
1. wrangler d1 migrations apply --local --config wrangler-demo.toml
2. tsx scripts/seed-demo.ts (upserts admin@sonicjs.com/sonicjs! + RBAC role
using getPlatformProxy({ configPath: wrangler-demo.toml }) so it targets
the same local miniflare D1 as the wrangler dev instance)
Fixes login failure when running dev:demo — wrangler dev uses local miniflare
by default (not remote demo D1), so without seeding the local DB the admin
user doesn't exist.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix doc.root_id → doc.rootId (camelCase) typo causing undefined bind error - Bypass DocumentTypeRegistry in seed — direct raw SQL INSERT OR IGNORE for departments/regions/employees types (registry silently fails in proxy env) - Add seed:employees:demo script to setup:db:demo chain Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KV key _sonicjs_bootstrap_v3.0.0-beta.24 was written by pre-fix isolates that didn't activate core-cache/lexical-editor. New isolates see the old KV key and skip bootstrap entirely, so activatePlugin never runs. Bumping to beta.25 forces a fresh bootstrap on next cold start where ensurePluginInstalled activates all defaultActive plugins. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
COUNT-only check returned false when all plugins existed but were inactive — bootstrapCorePlugins() never ran, activatePlugin never fired, core-cache stayed inactive forever. Now checks both installed count AND active count. Returns true if any bootstrapped plugin is installed but not active. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Public API returns identical published data regardless of auth. Only skip cache for needsAcl=true (non-privileged authed users whose result set differs per principal). Admin/editor now share the anonymous cache key — same data, faster response. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deep-dive on concept, market comparison (Vercel ISR / Fastly / CF / headless CMSes), feasibility on Cloudflare stack, logged-in user cardinality problem, limitations, and recommended phased approach (tag invalidation → URL catalog → SWR → optional pre-warm). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 1 — collection-scoped invalidation already live in route handlers.
Phase 2 — URL catalog (catalog.ts):
- Per-isolate in-memory Map tracking every cache-eligible API request
- Records cacheKey, collection, path, queryString, hits/misses/staleServes
- Exposed at GET /admin/cache/catalog (JSON) and new "URL Catalog" tab in UI
- Bounded to 500 entries; evicts oldest on overflow
Phase 3 — Stale-While-Revalidate (swr.ts):
- POST/PUT/DELETE mutation handlers stash current memory-cached response
objects before invalidating (listKeys → getMany → markStale)
- GET handlers check for stale entry on cache miss; serve immediately with
X-Cache-Status: STALE header and consume the entry (one stale serve per
invalidation to prevent indefinite stale data)
- 30-second stale window; per-isolate, no KV required
Added listKeys()/getMany() delegation to the services/cache.ts adapter.
"URL Catalog" tab shows per-URL hit rate, collection, stale serve count,
and last-seen time; resets note and architecture explanation included.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clicking a tab now updates window.location.hash so the URL reflects the active tab. On load (and browser back/forward), the hash is read back and the correct tab is activated — refreshing stays on the same tab. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Catalog entries are now persisted to KV (key: _catalog:{cacheKey},
TTL 24h) via waitUntil so API requests from any isolate appear in
the admin panel. Writes are throttled to 1/30s per key to stay
within KV rate limits. Admin GET /catalog merges KV entries into
the in-memory map before returning; clear also purges KV.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Demo data is static — maximize KV hit rate for all visitors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
setGlobalCatalogKv was only called in the full bootstrap path (first boot after deploy). The fast path (KV-flagged subsequent isolates) skipped it, leaving globalKv=null so scheduleKvWrite was always a no-op. Catalog only survived within a single isolate's lifetime. Also pre-warm in-memory catalog from KV via waitUntil so fresh isolates start with accumulated counts rather than overwriting KV with low per-isolate counts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cache keys include full SQL strings from queryResult.sql — when prefixed with _catalog: the total KV key often exceeded the 512-byte limit, causing kv.put() to throw silently inside waitUntil. Hash the cache key with SHA-256 (first 32 hex chars) before writing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SpeedBadge shows real cache source (memory/kv/db/stale) from X-Cache-Source header - Add sonicOrigin client with Cache-Control: no-cache for origin bypass - Fix Tailwind CSS setup (vite plugin + index.css) - Smooth filter transitions (opacity fade, no skeleton flash) - Pill button filters for department/region - Refresh icon in SpeedBadge header - CORS exposeHeaders for X-Cache-Source and related headers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- npm run dev:demo starts both SonicJS backend (port 9876) and React frontend (port 5199) in one command - Vite proxy forwards /api and /v1 to backend — no CORS issues - React demo always uses real backend in dev (no mock data) - Origin toggle now persists bypass across all fetches (filter/page changes) - dev:demo:react script in my-sonicjs-app uses fixed BACKEND_PORT Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
@sonicjs-cms/sdk— new isomorphic TypeScript client package with employee directory demo appfetchtoglobalThisto prevent Illegal invocation in Workerswhere[field][op]bracket syntax +data.*JSON field pathsdemoLoginPluginregistered,isDemoModeActive()flag wired to auth page prefilldefaultActive: truein manifest)Test plan
cd packages/core && npm run type-checkpassesnpm testpasseshttps://demo.sonicjs.com/auth/login— prefill visible, login succeeds🤖 Generated with Claude Code