Skip to content

feat(sdk+demo): SonicJS SDK, demo site fixes, cold-start perf#1006

Merged
lane711 merged 33 commits into
mainfrom
lane711/sonicjs-dev-sdk
Jul 9, 2026
Merged

feat(sdk+demo): SonicJS SDK, demo site fixes, cold-start perf#1006
lane711 merged 33 commits into
mainfrom
lane711/sonicjs-dev-sdk

Conversation

@lane711

@lane711 lane711 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • @sonicjs-cms/sdk — new isomorphic TypeScript client package with employee directory demo app
  • Demo site fixes: pagination, CORS wildcard subdomains, API key removal from build, real backend wiring
  • SDK fix: bind fetch to globalThis to prevent Illegal invocation in Workers
  • Filter fix: support where[field][op] bracket syntax + data.* JSON field paths
  • Cold-start perf: 14s → <750ms via module-level caches, parallel bootstrap, KV skip layer
  • Demo login: demoLoginPlugin registered, isDemoModeActive() flag wired to auth page prefill
  • Cache plugin: enabled by default (defaultActive: true in manifest)

Test plan

  • cd packages/core && npm run type-check passes
  • npm test passes
  • Demo site login: https://demo.sonicjs.com/auth/login — prefill visible, login succeeds
  • Demo site cold start < 2s on first load
  • Cache plugin shows as active in admin plugin list
  • Employee directory demo at Cloudflare Pages fetches from live CMS

🤖 Generated with Claude Code

lane711 and others added 10 commits July 7, 2026 15:26
- 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>
@lane711 lane711 merged commit 45fec2d into main Jul 9, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant