Skip to content

Realtime package - #51

Merged
cobraprojects merged 6 commits into
mainfrom
realtime-package
Jun 7, 2026
Merged

Realtime package#51
cobraprojects merged 6 commits into
mainfrom
realtime-package

Conversation

@cobraprojects

@cobraprojects cobraprojects commented Jun 7, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Realtime queries & mutations with live updates, access control (public/authenticated/custom), DB-driven invalidation, and client APIs
    • Framework integrations (React, Next, Nuxt/Vue, SvelteKit) and CLI scaffolding/install support
    • Demo admin UIs in example apps demonstrating realtime edits
  • Documentation

    • New Realtime guide, installation instructions, and docs navigation entry
  • Tests

    • Extensive realtime unit, integration, type, and browser-flow tests added

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 45801fe3-8c9d-4d0d-ac3b-c1169aa77fa9

📥 Commits

Reviewing files that changed from the base of the PR and between a222f15 and 424c867.

📒 Files selected for processing (4)
  • packages/adapter-sveltekit/src/realtime-definition-transform.ts
  • packages/adapter-sveltekit/tests/realtime-definition-transform.test.ts
  • packages/cli/src/project/scaffold.ts
  • packages/cli/tests/cli.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/adapter-sveltekit/tests/realtime-definition-transform.test.ts
  • packages/cli/src/project/scaffold.ts

📝 Walkthrough

Walkthrough

Adds a new @holo-js/realtime package (contracts, runtime, client transports/stores, definition builders), integrates realtime into adapters, broadcast worker, CLI scaffold/install and project tooling, records DB query dependencies and invalidation listeners, updates docs/examples, and adds broad tests.

Changes

Realtime platform rollout

Layer / File(s) Summary
Release, docs, workspace and manifest updates
.changeset/*, apps/docs/docs/*, package.json, packages/realtime/package.json, packages/realtime/tsconfig*.json, packages/realtime/tsup.config.ts, packages/realtime/vitest.config.ts, vitest.workspace.ts
Adds changeset, docs navigation and install docs, workspace catalog entry, and realtime package build/test configs.
Realtime package core: contracts, client, definition, index
packages/realtime/src/contracts.ts, packages/realtime/src/definition.ts, packages/realtime/src/client.ts, packages/realtime/src/index.ts
Introduces realtime contracts, definition marking, client transports and per-query stores, runtime configuration APIs, and public definition builders/exports.
Realtime runtime and server resolution
packages/realtime/src/runtime.ts, packages/realtime/src/server.ts
Implements server-side execution, subscription management, dependency collection/refresh, auth resolution, and definition resolution from filesystem modules.
Transforms and adapter integration
packages/adapter-next/*, packages/adapter-nuxt/*, packages/adapter-sveltekit/*
Adds realtime definition transforms/loaders, adapter runtime hooks/composables, client useQuery wiring, loader/plugin/Vite wiring, tsconfig/vitest aliases, and package export metadata.
Broadcast worker realtime protocol
packages/broadcast/src/*
Extends broadcast worker to accept holo:realtime websocket frames (query/mutation/subscribe/unsubscribe), manage per-socket subscriptions, and serialize realtime error frames.
CLI scaffold, worker bindings, and registry
packages/cli/src/*
Adds realtime as install/scaffold option, dependency upsert, Next/SvelteKit artifact generation/injection, broadcast worker bindings gated on project deps, and CLI tests.
DB cache dependency collection & model typing
packages/db/src/cache.ts, packages/db/src/index.ts, packages/db/src/query/TableQueryBuilder.ts, packages/db/src/model/*, packages/media/src/defineMediaModel.ts
Adds AsyncLocalStorage-based dependency collection, listener APIs, notifiers on invalidation, records dependencies from queries, and expands model/type exports and media typings.
Example apps and browser flow tests
apps/blog-*/server/realtime/*, apps/blog-*/app/*, tests/example-app-*.mjs
Adds realtime admin queries/mutations and demo pages across example apps, Playwright helpers for realtime flows, and updates test runner flows.
Tests
packages/realtime/tests/*, packages/adapter-*/tests/*, packages/broadcast/tests/*, packages/db/tests/*, packages/media/tests/*, packages/validation/tests/*, packages/cli/tests/*
Extensive test additions covering client transport/store behavior, runtime subscriptions/refresh, adapter boundaries, broadcast worker realtime protocol, DB invalidation listener behavior, type tests, and CLI scaffolding.

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant RealtimeClient
  participant BroadcastWorker
  participant RealtimeRuntime
  participant Database

  App->>RealtimeClient: useRealtimeQuery / call client.query or definition()
  RealtimeClient->>BroadcastWorker: send query / subscribe wire frame
  BroadcastWorker->>RealtimeRuntime: route to executeRealtimeQuery / subscribeRealtimeQuery
  RealtimeRuntime->>Database: collectDatabaseQueryDependencies during execution
  Database->>RealtimeRuntime: dependency invalidation event
  RealtimeRuntime->>BroadcastWorker: emit refreshed snapshot
  BroadcastWorker->>RealtimeClient: deliver snapshot update
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I hopped through code with ears held high,
Realtime threads now stitch the sky.
From client hooks to DB’s keen ear,
Snapshots dance when commits draw near.
A carrot patch, quick as a wink—
Live updates served with one clean blink.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch realtime-package

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (6)
apps/docs/docs/realtime/index.md (1)

122-122: Clarify that ~/ is scaffold-supported (not Nuxt-only).

The docs’ import { listPosts } from '~/server/realtime/posts' isn’t relying on Nuxt path alias conventions: the CLI scaffold emits a tsconfig paths mapping of ~/* to ./* (alongside @/*) via packages/cli/src/project/scaffold/workspace-renderers.ts (and also for the loader tsconfig in packages/cli/src/project/runtime.ts). So the portability concern is covered by the project generator; the focus should instead be on whether each framework’s module/bundler resolution honors TS paths at runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/docs/realtime/index.md` at line 122, Update the docs text around
the import example `import { listPosts } from '~/server/realtime/posts'` to
state that the `~/` alias is provided by the project scaffold (via the emitted
tsconfig paths mapping `~/*` and `@/*` to `./*`) rather than being Nuxt-only,
and add a short note that runtime module/bundler resolution must honor
TypeScript `paths` (i.e., users should verify their framework/bundler respects
TS path mappings). Also reference the scaffold generator modules
(workspace-renderers and runtime) as the origin of the mapping so readers know
the CLI emits the alias.
packages/cli/src/project/scaffold.ts (1)

168-171: ⚡ Quick win

Make Next layout provider injection tolerant to normal <body> formatting variants.

The replacement only matches exact <body>{children}</body>. Layouts with attributes/newlines (common in real projects) will skip injection, leaving realtime client wiring incomplete.

Suggested fix
-  const nextContents = layoutContents
-    .replace(
-      '<body>{children}</body>',
-      '<body><HoloRealtime>{children}</HoloRealtime></body>',
-    )
+  const nextContents = layoutContents.replace(
+    /<body([^>]*)>\s*\{children\}\s*<\/body>/,
+    '<body$1><HoloRealtime>{children}</HoloRealtime></body>',
+  )

Also applies to: 173-177

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/project/scaffold.ts` around lines 168 - 171, The current
string replacement only matches the exact literal '<body>{children}</body>' so
layouts with attributes, whitespace or newlines are missed; update the .replace
usages that target '<body>{children}</body>' to use a regex that matches <body>
tags with optional attributes/whitespace (e.g.
/<body[^>]*>\s*{children}\s*<\/body>/s) and replace the whole match with
'<body><HoloRealtime>{children}</HoloRealtime></body>' (apply the same change to
the second replace occurrence as well) so the HoloRealtime provider is injected
robustly.
packages/adapter-nuxt/src/runtime/server/routes/realtime-mutation.post.ts (1)

4-17: ⚡ Quick win

Extract shared request conversion logic to reduce duplication.

The resolveRequest helper is duplicated across realtime-mutation.post.ts, realtime-query.post.ts, and realtime-stream.get.ts with only minor variations (POST routes include body, GET route does not). Extracting this to a shared utility module would eliminate the duplication and prevent future divergence.

♻️ Refactoring approach

Create a shared utility file (e.g., src/runtime/server/utils/request-adapter.ts):

import { getHeaders, getRequestURL, readRawBody, type H3Event } from 'h3'

export async function h3EventToRequest(event: H3Event, includeBody: boolean = true): Promise<Request> {
  const headers = new Headers()
  for (const [key, value] of Object.entries(getHeaders(event))) {
    if (typeof value === 'string') {
      headers.set(key, value)
    }
  }

  return new Request(getRequestURL(event), {
    ...(includeBody && { body: await readRawBody(event) }),
    headers,
    method: event.method,
  })
}

Then simplify each route handler:

-async function resolveRequest(event: H3Event): Promise<Request> {
-  const headers = new Headers()
-  for (const [key, value] of Object.entries(getHeaders(event))) {
-    if (typeof value === 'string') {
-      headers.set(key, value)
-    }
-  }
-
-  return new Request(getRequestURL(event), {
-    body: await readRawBody(event),
-    headers,
-    method: event.method,
-  })
-}
+import { h3EventToRequest } from '../../utils/request-adapter'

 export default defineEventHandler(async (event) => {
-  return await handleRealtimeMutationRequest(await resolveRequest(event), {
+  return await handleRealtimeMutationRequest(await h3EventToRequest(event), {
     projectRoot: useRuntimeConfig().holo.projectRoot ?? process.cwd(),
   })
 })

For the stream route, pass false for the includeBody parameter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/adapter-nuxt/src/runtime/server/routes/realtime-mutation.post.ts`
around lines 4 - 17, Extract the duplicated resolveRequest logic into a shared
utility function h3EventToRequest(event: H3Event, includeBody: boolean = true)
that builds Headers from getHeaders(event), uses getRequestURL(event),
conditionally adds body via readRawBody(event) when includeBody is true, and
returns a new Request; export this utility (e.g., in
src/runtime/server/utils/request-adapter.ts) and replace the local
resolveRequest implementations in realtime-mutation.post.ts,
realtime-query.post.ts (use default includeBody) and realtime-stream.get.ts
(pass includeBody = false), updating imports to use h3EventToRequest.
packages/adapter-nuxt/src/runtime/composables/realtime.ts (1)

56-70: 💤 Low value

Consider removing the unnecessary shallowRef.

The snapshot shallowRef (line 61) is only used locally within the subscription callback and is never returned or exposed to Vue's reactivity system. You could simplify by reading store.snapshot?.data directly in both line 62 and line 65.

♻️ Optional simplification
  const store = getRealtimeQueryStore(definition, args)
- const snapshot = shallowRef(store.snapshot)
- let current = createRealtimeReactiveValue(snapshot.value?.data) as RealtimeResultFor<TDefinition>
+ let current = createRealtimeReactiveValue(store.snapshot?.data) as RealtimeResultFor<TDefinition>
  const unsubscribe = store.subscribe(() => {
-   snapshot.value = store.snapshot
-   current = replaceRealtimeReactiveValue(current, snapshot.value?.data as RealtimeResultFor<TDefinition>)
+   current = replaceRealtimeReactiveValue(current, store.snapshot?.data as RealtimeResultFor<TDefinition>)
  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/adapter-nuxt/src/runtime/composables/realtime.ts` around lines 56 -
70, The shallowRef named snapshot in useReactiveRealtimeQuery is unnecessary
because its value is only used inside the store.subscribe callback; remove the
snapshot declaration and use store.snapshot?.data directly when initializing and
updating current (i.e., set current =
createRealtimeReactiveValue(store.snapshot?.data) and inside the subscribe
callback call replaceRealtimeReactiveValue(current, store.snapshot?.data as
RealtimeResultFor<TDefinition>)); keep the existing unsubscribe =
store.subscribe(...) logic, store.connect(), and onScopeDispose(unsubscribe),
and ensure TypeScript casts/types around store.snapshot?.data match the original
RealtimeResultFor<TDefinition>.
packages/realtime/src/client.ts (1)

183-191: 💤 Low value

Consider surfacing initial query failures.

The initial query on line 189 silently swallows errors with an empty rejection handler. If the query fails, users have no way to know why the store has no snapshot. Consider logging the error or exposing it through a store error state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/realtime/src/client.ts` around lines 183 - 191, The connect() method
currently swallows errors from the initial transport.query<TResult>(name, args)
call via an empty rejection handler; update the promise handling in connect() to
surface failures by replacing the empty rejection handler with logic that logs
the error (using the module's logger) and/or sets an error state on the store
(e.g., call a setError or emitError helper) so consumers can inspect why no
snapshot arrived; ensure setSnapshot still runs on success and keep unsubscribe
assignment to transport.subscribe<TResult>(...) unchanged.
packages/realtime/src/runtime.ts (1)

95-105: 💤 Low value

Consider explicit return typing instead of type assertions.

The type assertions at lines 99 and 102 work around TypeScript inference but may mask type mismatches if TableQueryBuilder or connection.model() return types change in the future. Explicitly typing the returned object or using satisfies would be safer.

♻️ Alternative: explicit typing
  function createRealtimeDatabaseContext(connection: DatabaseContext): RealtimeDatabaseContext {
-   return Object.freeze({
+   const context: RealtimeDatabaseContext = {
      connection,
      table(table: string) {
-       return new TableQueryBuilder(table, connection) as ReturnType<RealtimeDatabaseContext['table']>
+       return new TableQueryBuilder(table, connection)
      },
      model(...parameters: Parameters<DatabaseContext['model']>) {
-       return connection.model(...parameters) as ReturnType<DatabaseContext['model']>
+       return connection.model(...parameters)
      },
-   })
+   }
+   return Object.freeze(context)
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/realtime/src/runtime.ts` around lines 95 - 105, The factory
createRealtimeDatabaseContext currently uses type assertions for the table and
model return values; replace those casts by explicitly typing the returned
object to match RealtimeDatabaseContext (or use the TypeScript "satisfies
RealtimeDatabaseContext" operator) so the compiler verifies shapes instead of
silencing mismatches. Concretely, annotate the object returned from
createRealtimeDatabaseContext as RealtimeDatabaseContext (or append "satisfies
RealtimeDatabaseContext"), remove the "as ReturnType<...>" casts on the table
implementation that constructs new TableQueryBuilder and on the model call to
connection.model(...), and adjust the table method signature to return the exact
TableQueryBuilder-derived type expected by RealtimeDatabaseContext so changes to
TableQueryBuilder or connection.model will surface type errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/docs/docs/realtime/index.md`:
- Line 20: Replace the inconsistent scaffold command `holo new my-app --package
realtime` with the canonical installer used in the installation docs: `npm
create holo-js@latest my-app -- --package realtime`; update the line in the
realtime doc (the occurrence of `holo new my-app --package realtime`) to use the
`npm create holo-js@latest my-app -- --package realtime` form or add a one-line
note pointing readers to the installation guide for the canonical command so
both docs are consistent.

In `@packages/adapter-next/src/realtime.ts`:
- Around line 10-25: The hook useReactiveRealtimeQuery currently casts
snapshot?.data to RealtimeResultFor<TDefinition> even when snapshot can be
undefined; change the function signature to return
RealtimeResultFor<TDefinition> | undefined, stop the unsafe cast, and return
snapshot?.data directly (i.e., update the return type of
useReactiveRealtimeQuery and return snapshot?.data without forcing the cast);
this keeps useSyncExternalStore's store.snapshot handling correct (references:
useReactiveRealtimeQuery, getRealtimeQueryStore, store.snapshot,
useSyncExternalStore).

In `@packages/adapter-nuxt/src/runtime/composables/realtime.ts`:
- Around line 56-70: The subscription handler in useReactiveRealtimeQuery can
pass undefined to replaceRealtimeReactiveValue and break reactivity when
snapshot.value?.data is missing; update the handler (inside
useReactiveRealtimeQuery after snapshot.value = store.snapshot) to guard: if
snapshot.value?.data is defined then call replaceRealtimeReactiveValue(current,
snapshot.value.data as RealtimeResultFor<TDefinition>) and assign its result to
current, otherwise leave current unchanged (but still update snapshot ref) so
reactive arrays/objects are not replaced with undefined; reference
getRealtimeQueryStore, createRealtimeReactiveValue, replaceRealtimeReactiveValue
and ensure onScopeDispose/unsubscribe behavior remains the same.

In `@packages/adapter-sveltekit/src/realtime.ts`:
- Around line 98-116: The subscription callback in useReactiveRealtimeQuery may
call replaceRealtimeReactiveValue with undefined when store.snapshot?.data is
missing, which can break reactivity; update the callback in
useReactiveRealtimeQuery to guard against undefined snapshot/data (from
store.snapshot?.data) and only call replaceRealtimeReactiveValue with a defined
value (or fall back to the existing current value or an appropriate empty
default) so replaceRealtimeReactiveValue and createRealtimeReactiveValue never
receive undefined and current stays a valid reactive reference.

In `@packages/cli/src/project/scaffold.ts`:
- Around line 597-603: The current routes-writing loop (uses variables route,
routePath, writeTextFile, pathExists, createdFrameworkSetup) unconditionally
overwrites user-facing realtime files; change it to skip writing when routePath
targets user-visible realtime locations (e.g., paths under "app/holo/realtime"
or "src/routes/holo/realtime") unless the file is inside the managed generator
directory ".holo-js/generated"; implement this by checking routePath for those
user-facing prefixes and continuing (not calling writeTextFile) unless routePath
includes ".holo-js/generated", and apply the same guard to the other
routes-writing block that handles realtime files.

In `@packages/db/src/cache.ts`:
- Around line 140-149: notifyDatabaseDependencyInvalidationListeners currently
uses Promise.all([...listeners].map(listener => listener(event))) so a single
thrown error will reject the whole promise and prevent remaining listeners from
running; change this to invoke every listener and isolate their errors by
wrapping each call in a try/catch (or map to a promise that catches its own
error) so all listeners are notified regardless of failures, and ensure errors
are logged (use the project logger or console.error) rather than propagating;
keep references to getQueryCacheBridgeState().dependencyInvalidationListeners
and the listener variable so you modify that exact loop.

In `@packages/realtime/src/runtime.ts`:
- Around line 241-261: The authorize function currently casts auth to
RealtimeAuthState before calling the user-provided authorize callback, but auth
can be null (from resolveAuthForAccess) causing an unsafe assertion; remove the
cast and pass auth as-is (auth: RealtimeAuthState | null) into access.authorize,
and update the authorize callback type in contracts (e.g., RealtimeAccessObject
/ RealtimeAccess type definition) so its signature accepts auth:
RealtimeAuthState | null instead of RealtimeAuthState to make the type-safe
change consistent with normalizeAccess/resolveAuthForAccess behavior.
- Around line 356-371: The refreshSubscription function does not isolate errors
thrown by user-provided callbacks, causing failures in the onData or onError
handlers to reject the entire function and break the refresh mechanism for all
subscriptions via Promise.all. Wrap both the await subscription.options.onData()
call and the await subscription.options.onError() call in their own try-catch
blocks so that any errors thrown by user code are caught and logged locally
without propagating up to the caller, ensuring subscription refresh continues
for other subscriptions.

In `@packages/realtime/src/server.ts`:
- Around line 54-65: The current collectRealtimeFiles function swallows all
errors from readdir which hides real issues; change the error handling around
the await readdir(...) call so that only a missing-directory error (e.g.
error.code === 'ENOENT') returns an empty array, while other errors (permission,
I/O, etc.) are rethrown (or propagated) so they surface to the caller; keep the
rest of the function (filtering by entry.isFile(), resolve(...), and
realtimeFileExtensions) unchanged and reference readdir, collectRealtimeFiles,
and realtimeFileExtensions when locating the code to update.

In `@packages/realtime/tests/realtime.server.test.ts`:
- Around line 124-128: The test expectation is missing the title returned by the
createPost mutation handler; update the assertion in the test (the expect on
mutationResponse.json()) to match the handler's actual return value by including
the title property (e.g., { id: 2, title: 'Second' }) so the result from
createPost (which returns { id: 2, title: args.title }) matches the expected
object.

---

Nitpick comments:
In `@apps/docs/docs/realtime/index.md`:
- Line 122: Update the docs text around the import example `import { listPosts }
from '~/server/realtime/posts'` to state that the `~/` alias is provided by the
project scaffold (via the emitted tsconfig paths mapping `~/*` and `@/*` to
`./*`) rather than being Nuxt-only, and add a short note that runtime
module/bundler resolution must honor TypeScript `paths` (i.e., users should
verify their framework/bundler respects TS path mappings). Also reference the
scaffold generator modules (workspace-renderers and runtime) as the origin of
the mapping so readers know the CLI emits the alias.

In `@packages/adapter-nuxt/src/runtime/composables/realtime.ts`:
- Around line 56-70: The shallowRef named snapshot in useReactiveRealtimeQuery
is unnecessary because its value is only used inside the store.subscribe
callback; remove the snapshot declaration and use store.snapshot?.data directly
when initializing and updating current (i.e., set current =
createRealtimeReactiveValue(store.snapshot?.data) and inside the subscribe
callback call replaceRealtimeReactiveValue(current, store.snapshot?.data as
RealtimeResultFor<TDefinition>)); keep the existing unsubscribe =
store.subscribe(...) logic, store.connect(), and onScopeDispose(unsubscribe),
and ensure TypeScript casts/types around store.snapshot?.data match the original
RealtimeResultFor<TDefinition>.

In `@packages/adapter-nuxt/src/runtime/server/routes/realtime-mutation.post.ts`:
- Around line 4-17: Extract the duplicated resolveRequest logic into a shared
utility function h3EventToRequest(event: H3Event, includeBody: boolean = true)
that builds Headers from getHeaders(event), uses getRequestURL(event),
conditionally adds body via readRawBody(event) when includeBody is true, and
returns a new Request; export this utility (e.g., in
src/runtime/server/utils/request-adapter.ts) and replace the local
resolveRequest implementations in realtime-mutation.post.ts,
realtime-query.post.ts (use default includeBody) and realtime-stream.get.ts
(pass includeBody = false), updating imports to use h3EventToRequest.

In `@packages/cli/src/project/scaffold.ts`:
- Around line 168-171: The current string replacement only matches the exact
literal '<body>{children}</body>' so layouts with attributes, whitespace or
newlines are missed; update the .replace usages that target
'<body>{children}</body>' to use a regex that matches <body> tags with optional
attributes/whitespace (e.g. /<body[^>]*>\s*{children}\s*<\/body>/s) and replace
the whole match with '<body><HoloRealtime>{children}</HoloRealtime></body>'
(apply the same change to the second replace occurrence as well) so the
HoloRealtime provider is injected robustly.

In `@packages/realtime/src/client.ts`:
- Around line 183-191: The connect() method currently swallows errors from the
initial transport.query<TResult>(name, args) call via an empty rejection
handler; update the promise handling in connect() to surface failures by
replacing the empty rejection handler with logic that logs the error (using the
module's logger) and/or sets an error state on the store (e.g., call a setError
or emitError helper) so consumers can inspect why no snapshot arrived; ensure
setSnapshot still runs on success and keep unsubscribe assignment to
transport.subscribe<TResult>(...) unchanged.

In `@packages/realtime/src/runtime.ts`:
- Around line 95-105: The factory createRealtimeDatabaseContext currently uses
type assertions for the table and model return values; replace those casts by
explicitly typing the returned object to match RealtimeDatabaseContext (or use
the TypeScript "satisfies RealtimeDatabaseContext" operator) so the compiler
verifies shapes instead of silencing mismatches. Concretely, annotate the object
returned from createRealtimeDatabaseContext as RealtimeDatabaseContext (or
append "satisfies RealtimeDatabaseContext"), remove the "as ReturnType<...>"
casts on the table implementation that constructs new TableQueryBuilder and on
the model call to connection.model(...), and adjust the table method signature
to return the exact TableQueryBuilder-derived type expected by
RealtimeDatabaseContext so changes to TableQueryBuilder or connection.model will
surface type errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 61bc97b9-61ae-498b-819c-d365ff991f81

📥 Commits

Reviewing files that changed from the base of the PR and between fe0dd64 and 465f109.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • packages/cli/src/generated/workspaceCatalog.ts is excluded by !**/generated/**
📒 Files selected for processing (63)
  • .changeset/config.json
  • .changeset/realtime-queries.md
  • apps/docs/docs/.vitepress/config.mts
  • apps/docs/docs/index.md
  • apps/docs/docs/installation.md
  • apps/docs/docs/realtime/index.md
  • package.json
  • packages/adapter-next/package.json
  • packages/adapter-next/src/react-shim.d.ts
  • packages/adapter-next/src/realtime.ts
  • packages/adapter-next/tests/client.test.ts
  • packages/adapter-next/tests/package.test.ts
  • packages/adapter-next/tsconfig.json
  • packages/adapter-next/tsup.config.ts
  • packages/adapter-next/vitest.config.ts
  • packages/adapter-nuxt/package.json
  • packages/adapter-nuxt/src/module.ts
  • packages/adapter-nuxt/src/nuxt-kit-shim.d.ts
  • packages/adapter-nuxt/src/runtime/composables/realtime.ts
  • packages/adapter-nuxt/src/runtime/plugins/realtime.client.ts
  • packages/adapter-nuxt/src/runtime/server/routes/realtime-mutation.post.ts
  • packages/adapter-nuxt/src/runtime/server/routes/realtime-query.post.ts
  • packages/adapter-nuxt/src/runtime/server/routes/realtime-stream.get.ts
  • packages/adapter-nuxt/tests/client.test.ts
  • packages/adapter-nuxt/tests/package.test.ts
  • packages/adapter-nuxt/tests/realtime-route.test.ts
  • packages/adapter-nuxt/tsconfig.json
  • packages/adapter-nuxt/vitest.config.ts
  • packages/adapter-sveltekit/package.json
  • packages/adapter-sveltekit/src/client.ts
  • packages/adapter-sveltekit/src/realtime.ts
  • packages/adapter-sveltekit/tests/client.test.ts
  • packages/adapter-sveltekit/tests/package.test.ts
  • packages/adapter-sveltekit/tsconfig.json
  • packages/adapter-sveltekit/tsup.config.ts
  • packages/adapter-sveltekit/vitest.config.ts
  • packages/cli/src/cli.ts
  • packages/cli/src/parsing.ts
  • packages/cli/src/project/scaffold.ts
  • packages/cli/src/project/scaffold/dependencies.ts
  • packages/cli/src/project/scaffold/framework-renderers.ts
  • packages/cli/src/project/scaffold/framework.ts
  • packages/cli/src/project/shared.ts
  • packages/cli/tests/cli.test.ts
  • packages/db/src/cache.ts
  • packages/db/src/index.ts
  • packages/db/src/query/TableQueryBuilder.ts
  • packages/realtime/package.json
  • packages/realtime/src/client.ts
  • packages/realtime/src/contracts.ts
  • packages/realtime/src/index.ts
  • packages/realtime/src/runtime.ts
  • packages/realtime/src/server.ts
  • packages/realtime/tests/docs-smoke.test.ts
  • packages/realtime/tests/realtime.client.test.ts
  • packages/realtime/tests/realtime.server.test.ts
  • packages/realtime/tests/realtime.test.ts
  • packages/realtime/tests/realtime.type.test.ts
  • packages/realtime/tsconfig.json
  • packages/realtime/tsconfig.type-tests.json
  • packages/realtime/tsup.config.ts
  • packages/realtime/vitest.config.ts
  • vitest.workspace.ts
👮 Files not reviewed due to content moderation or server errors (1)
  • packages/cli/tests/cli.test.ts

Comment thread apps/docs/docs/realtime/index.md Outdated
Comment thread packages/adapter-next/src/realtime.ts
Comment thread packages/adapter-nuxt/src/runtime/composables/realtime.ts
Comment thread packages/adapter-sveltekit/src/realtime.ts
Comment thread packages/cli/src/project/scaffold.ts
Comment thread packages/db/src/cache.ts
Comment thread packages/realtime/src/runtime.ts
Comment thread packages/realtime/src/runtime.ts
Comment thread packages/realtime/src/server.ts
Comment thread packages/realtime/tests/realtime.server.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
packages/realtime/src/runtime.ts (1)

399-409: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clean up the subscription when the initial onData fails.

Line 408 registers the subscription before Line 409 awaits the first callback. If that callback throws, subscribeRealtimeQuery() rejects but the entry stays in state.subscriptions, so later invalidations keep refreshing a subscription the caller never received a handle to unsubscribe.

Proposed fix
   const subscription: ActiveSubscription<TDefinition> = {
     id: `subscription.${state.nextSubscriptionId}`,
     definition,
     args: await resolveArgs(definition, input),
     options,
     dependencies: result.dependencies,
     version: 1,
     current: snapshot,
   }
   state.subscriptions.set(subscription.id, subscription as ActiveSubscription<RealtimeQueryDefinitionMetadata>)
-  await options.onData?.(snapshot)
+  try {
+    await options.onData?.(snapshot)
+  } catch (error) {
+    state.subscriptions.delete(subscription.id)
+    throw error
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/realtime/src/runtime.ts` around lines 399 - 409, The subscription is
added to state.subscriptions before awaiting the initial onData callback, so if
options.onData throws the subscription remains registered; update
subscribeRealtimeQuery() to wrap the initial await options.onData?.(snapshot) in
a try/catch and on error remove the entry
(state.subscriptions.delete(subscription.id)) before rethrowing the error so the
failed subscription is not left in state.subscriptions; reference the created
subscription object (subscription.id), the state.subscriptions map, and the
options.onData call to locate where to add the cleanup.
packages/realtime/src/server.ts (1)

32-32: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Skip declaration files during realtime module discovery.

The current filter only checks the last extension, so *.d.ts, *.d.mts, and *.d.cts are treated as executable realtime modules. If one of those files exists under the realtime root, resolveRealtimeDefinition() will try to import it and fail before it reaches the real definition file.

Proposed fix
 const realtimeFileExtensions = new Set(['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'])
+const realtimeDeclarationPattern = /\.d\.(ts|mts|cts)$/

   return entries
     .filter(entry => entry.isFile())
     .map(entry => resolve(root, entry.parentPath, entry.name))
-    .filter(filePath => realtimeFileExtensions.has(filePath.slice(filePath.lastIndexOf('.'))))
+    .filter(filePath => !realtimeDeclarationPattern.test(filePath))
+    .filter(filePath => realtimeFileExtensions.has(filePath.slice(filePath.lastIndexOf('.'))))
     .sort((left, right) => left.localeCompare(right))

Also applies to: 57-60

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/realtime/src/server.ts` at line 32, The discovery currently treats
declaration files like "*.d.ts" as runnable because it only checks the last
extension via realtimeFileExtensions; update the realtime module filtering used
by resolveRealtimeDefinition (and the related discovery code that uses
realtimeFileExtensions) to exclude TypeScript declaration patterns by adding a
check that the filepath does not match /\.d\.(ts|mts|cts)$/ (or generally
/\.d\.[^.]+$/) in addition to the existing extension-set membership; keep
realtimeFileExtensions as the allowed runtime extensions but ensure the
discovery predicate returns false for any path that matches the declaration-file
regex so *.d.ts, *.d.mts, and *.d.cts are skipped.
packages/realtime/tests/realtime.test.ts (1)

258-263: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Restore the console.error mock after this test.

Line 540 replaces a global, but the shared afterEach never restores mocks. That leaves console.error stubbed for later tests/files, which can hide failures and leak call history across the suite.

Proposed fix
 afterEach(() => {
+  vi.restoreAllMocks()
   resetRealtimeClientRuntime()
   resetRealtimeRuntime()
   resetDatabaseDependencyInvalidationListeners()
   resetDB()
 })

Also applies to: 540-540

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/realtime/tests/realtime.test.ts` around lines 258 - 263, The shared
afterEach block currently calls resetRealtimeClientRuntime(),
resetRealtimeRuntime(), resetDatabaseDependencyInvalidationListeners(), and
resetDB() but does not restore a mocked console.error; update this afterEach to
restore the console.error mock after tests that stub it (the stub created in the
test file around the console.error mock), e.g. call console.error.mockRestore()
or invoke jest.restoreAllMocks() in the afterEach so that any console.error mock
is removed and does not leak into later tests; keep the
resetRealtimeClientRuntime(), resetRealtimeRuntime(),
resetDatabaseDependencyInvalidationListeners(), and resetDB() calls intact.
packages/cli/src/project/registry-svelte.ts (1)

677-708: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

realtimeEnabled never turns on for the realtime directory.

pathExists() here is backed by readFileIfPresent(), so pathExists(resolve(projectRoot, 'server/realtime')) returns false for a directory. That means ensureNextManagedRoutes() stops regenerating .holo-js/generated/next/realtime-definitions.ts even when realtime definition files exist.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/project/registry-svelte.ts` around lines 677 - 708,
ensureNextManagedRoutes sets realtimeEnabled using
pathExists(resolve(projectRoot, 'server/realtime')) which returns false for
directories because pathExists delegates to readFileIfPresent; change realtime
detection in ensureNextManagedRoutes to check for a directory (e.g., use a
directory-aware helper or fs.stat/fs.promises.stat to test isDirectory) or check
for a known file inside server/realtime (like an index or pattern) so
realtimeEnabled becomes true when the server/realtime directory and its
definition files exist; update the realtimeEnabled assignment and any helper
calls (pathExists/readFileIfPresent) accordingly.
packages/cli/src/project/scaffold.ts (1)

591-605: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scaffolding ignores config.paths.realtime.

installRealtimeIntoProject() loads the project config but still creates and scans only server/realtime. If a project already customized the realtime root, this installer writes a second unused directory and generates Next imports from the wrong location.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/project/scaffold.ts` around lines 591 - 605, The scaffolder
calls loadProjectConfig but ignores the returned config and hardcodes
realtimeRoot to resolve(projectRoot, 'server/realtime'); capture the project
config from loadProjectConfig (e.g., const projectConfig = await
loadProjectConfig(...)) and compute realtimeRoot from
projectConfig.paths.realtime if present (fallback to resolve(projectRoot,
'server/realtime')); use that computed realtimeRoot for pathExists
(realtimeDirectoryExists), mkdir(realtimeRoot,...), and pass it into any
rendering helpers that need the realtime path (for example instead of
renderNextRealtimeDefinitions(projectRoot) call the renderer with the resolved
realtimeRoot or otherwise update renderNextRealtimeDefinitions to accept the
realtime root so generated imports point to the correct location).
🧹 Nitpick comments (2)
packages/adapter-nuxt/src/realtime-definition-transform.ts (1)

1-343: ⚖️ Poor tradeoff

Consider extracting shared transform logic.

This file duplicates ~340 lines of parsing and transform logic from packages/adapter-next/src/realtime-definition-transform.ts, differing only in the adapter-specific import path (line 272). While adapter isolation is valuable, consider whether a shared @holo-js/realtime/transform utility could reduce maintenance burden without coupling the adapters.

This is optional; the current duplication is acceptable if adapters are meant to remain fully independent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/adapter-nuxt/src/realtime-definition-transform.ts` around lines 1 -
343, The file duplicates the parsing/transform logic from adapter-next; extract
the shared functions (isIdentifierBoundary, skipString, skipLineComment,
skipBlockComment, findRealtimePropertyValueEnd, findClosingBrace,
applyReplacements, extractObjectPropertyValue, collectRealtimeDefinitionExports,
stripRealtimeServerHandlers, Replacement and RealtimeDefinitionExport types)
into a new shared module (e.g. a `@holo-js/realtime/transform` utility) and import
them here, keeping only adapter-specific bits (the adapter import path and
createRealtimeClientDefinitionModule which uses collectRealtimeDefinitionExports
and the adapter import). Update createRealtimeClientDefinitionModule to call the
shared helpers and only inject the adapter-specific import string; ensure
exported symbols (createRealtimeClientDefinitionModule and
stripRealtimeServerHandlers) still exist or are re-exported as needed.
packages/adapter-next/src/realtime-definition-transform.ts (1)

19-36: 💤 Low value

Consider template literal and regex literal edge cases.

The skipString function treats backticks identically to quotes, which doesn't account for template literal expressions like `hello ${world}`. Additionally, regex literals (e.g., /\//) could be misinterpreted as line comments at lines 63-65.

While these edge cases are unlikely in typical realtime definition syntax (where name is usually a plain string and handler is stripped), consider adding a note or TODO to track this limitation for future robustness.

Also applies to: 58-71

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/adapter-next/src/realtime-definition-transform.ts` around lines 19 -
36, The skipString function currently treats backticks and other delimiters like
quotes the same, which fails to handle template-literal expressions (`${...}`)
and may mis-handle regex literals; update skipString to detect
backtick-delimited template literals and skip over embedded `${...}` expression
blocks (properly balancing braces and respecting escapes) and, where applicable,
add handling or detection to avoid treating regex literals as line comments, or
if you prefer not to implement full parsing now, add a clear TODO comment in
skipString noting these limitations (template expressions and regex literals)
and reference future work to parse `${...}` and distinguish regex literals.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/blog-next/server/realtime/posts.ts`:
- Around line 32-38: The code trims the title after validation so inputs like " 
" can pass the pre-trim .min(3) check and be saved empty; fix by
normalizing/triming the incoming title before validating and updating: compute a
trimmedTitle = args.title.trim(), run the existing validation logic against
trimmedTitle (or enforce trimmedTitle.length >= 3), then use trimmedTitle when
calling uniqueSlug(Post, trimmedTitle, { ignore: args.id }) and when passing
title to post.update; keep the authorize/Post.findOrFail flow unchanged.

In `@apps/blog-nuxt/server/realtime/posts.ts`:
- Around line 32-38: The title is trimmed after validation so whitespace-only
titles can bypass the .min(3) check; update the flow to revalidate the
normalized title before persisting by either trimming before schema validation
or running the validator again on the trimmed value: compute const title =
args.title.trim() first, then validate it against your title schema (or call the
same validation used at Line 28) before calling Post.findOrFail, authorize, and
post.update(...), and ensure uniqueSlug(Post, title, { ignore: args.id }) uses
that revalidated title.

In `@apps/blog-sveltekit/server/realtime/posts.ts`:
- Around line 26-33: The schema currently validates the raw args.title then the
handler trims it, allowing inputs like "   " or " a " to pass; update the args
schema so the title is trimmed before validation (e.g., use
field.string().transform(s => s.trim()) or an equivalent sanitizer on the title
field in the args: schema) and adjust the handler (handler, title variable) to
use the already-trimmed args.title (remove the separate title =
args.title.trim()) before calling Post.findOrFail.

In `@packages/adapter-nuxt/src/runtime/composables/realtime.ts`:
- Around line 32-41: The function replaceReactiveArrayObject should be removed
because it mutates an array into an object (creating a hybrid that breaks Vue
reactivity); delete the replaceReactiveArrayObject implementation and update any
call sites that currently create reactive([]) and then call
replaceReactiveArrayObject to instead create/assign an appropriately typed
reactive object (e.g., use reactive({}) or replace the whole reactive reference)
so you never repurpose an array into an object; reference
replaceReactiveArrayObject and the places that initialize reactive([])/handle
the first snapshot to make the change.
- Around line 44-46: The code returns reactive([]) as TValue when value ===
undefined which assumes an array result and causes downstream mixing with
replaceReactiveArrayObject; instead, when value === undefined return undefined
(preserve the pending state) rather than reactive([]) and adjust the consumer
logic that currently expects an array—specifically update the code that calls
replaceReactiveArrayObject and the branch around the later check (the area
referencing TValue and the line with reactive([])) so it explicitly handles
undefined results (e.g., treat undefined as pending before converting snapshots)
rather than forcing an empty array.

In `@packages/adapter-sveltekit/src/realtime-definition-transform.ts`:
- Around line 48-96: The function findRealtimePropertyValueEnd fails to handle
regex literals, so add logic to detect and skip regex literals (like /.../)
similar to how strings and comments are skipped: when you see '/' and it's not
the start of '//' or '/*', determine if it is a regex start (use a simple
heuristic: check previous non-whitespace token char to allow regex starts such
as '(', '[', '{', ',', ':', '=', '!', '?', 'return', '=>', or start-of-value)
and if so call a new skipRegex(source, cursor) helper that advances past escapes
and character classes until the closing '/' (handling flags), then continue
scanning; reference findRealtimePropertyValueEnd, skipString, skipLineComment,
skipBlockComment, and add skipRegex to ensure braces/brackets/parenDepth aren’t
corrupted by regex contents.

In `@packages/broadcast/src/worker.ts`:
- Around line 1230-1236: The call to
socket.realtimeSubscriptions.get(request.id)?.unsubscribe() is unguarded and a
thrown error can abort teardown; wrap the unsubscribe() invocation in a
try/catch so errors are caught and do not prevent downstream cleanup: call
unsubscribe() inside try, on error log it (or handle it) but always proceed to
socket.realtimeSubscriptions.delete(request.id) and sendRealtimeMessage(...
'holo:realtime:unsubscribed', { id: request.id }), and apply the same guarded
pattern where unsubscribe() is called during disconnect cleanup (the block
around lines 1758-1761) so teardown always completes even if unsubscribe()
throws.
- Around line 1264-1277: The code creates a new subscription via
realtime.subscribe and stores it in socket.realtimeSubscriptions under
request.id but does not teardown any existing subscription with the same id,
causing orphaned listeners; before assigning the new subscription (the variable
subscription) check socket.realtimeSubscriptions for an existing handle (e.g.,
prevSub), call its teardown method (unsubscribe/close/stop — use whatever method
the subscribe returns) and remove it, then store the new subscription and
proceed to use sendRealtimeMessage/sendRealtimeError as before; update the logic
around realtime.subscribe and socket.realtimeSubscriptions.set(request.id,
subscription) to ensure previous subscriptions are properly unsubscribed/closed
before overwriting.

In `@packages/cli/src/broadcast.ts`:
- Around line 158-168: The current loadRealtimeServerModule function swallows
any MODULE_NOT_FOUND/ERR_MODULE_NOT_FOUND errors which can hide broken
transitive imports; change it so only a module-not-found that explicitly
mentions the resolved import specifier for '`@holo-js/realtime/server`' is treated
as "not installed" and returns null, otherwise rethrow the error so transitive
import failures surface. Use the same resolved specifier value produced by
resolveProjectPackageImportSpecifier(projectRoot, '`@holo-js/realtime/server`')
(and reference createRealtimeWorkerBindings which already validated
'`@holo-js/realtime`' in the manifest) to match the error message before returning
null. Ensure other module-not-found or any other errors are rethrown.
- Around line 220-257: The current withRealtimeRequest mutates the singleton
runtime via runtime.setAuthRequestAccessors, causing cross-request auth leakage;
instead stop mutating the shared runtime and pass request-local auth accessors
directly to the realtime calls. Remove calls to runtime.setAuthRequestAccessors
in withRealtimeRequest and change the callers (query/mutate/subscribe) to create
the per-request accessors with
createRealtimeAuthRequestAccessors(context.headers) and supply them as an
explicit argument to realtime.executeRealtimeQuery,
realtime.executeRealtimeMutation and realtime.subscribeRealtimeQuery (or create
a small per-request wrapper around those functions that captures the accessors)
so that auth is request-scoped without touching the shared runtime instance.

In `@packages/cli/src/project/registry-svelte.ts`:
- Around line 665-675: The code in renderNextRealtimeDefinitions currently
hard-codes 'server/realtime' when calling collectRealtimeDefinitionFiles; change
it to use the project's configured realtime path (config.paths.realtime)
instead: load or read the project config for projectRoot (so
renderNextRealtimeDefinitions can derive config.paths.realtime), pass
resolve(projectRoot, config.paths.realtime) into collectRealtimeDefinitionFiles,
and ensure the same replacement is applied to the adjacent block referenced
(lines ~677-708) that also assumes 'server/realtime'; keep the rest of the flow
(building importPaths and calling renderNextGeneratedRealtimeDefinitions)
unchanged.

In `@packages/cli/src/project/scaffold.ts`:
- Around line 156-174: injectSvelteRealtimeVitePlugin currently only handles one
exact import and plugin literal which causes silent no-ops for common
vite.config.ts shapes; update it to robustly detect (1) an existing import from
'`@holojs/adapter-sveltekit/vite`' and add the holoSvelteKitRealtime import if
missing, and (2) a plugins array (even multiline, with spreads or other entries)
and inject holoSvelteKitRealtime() at the start of that array rather than only
replacing the exact string "plugins: [sveltekit()]"; implement these changes by
using tolerant regexes to find imports of '`@sveltejs/kit/vite`' and any existing
adapter import, add the import when absent, and parse/replace the plugins array
(matching "plugins\s*:\s*\[([\s\S]*?)\]") to prepend "holoSvelteKitRealtime(),"
while preserving formatting and different quote styles and returning undefined
only if holoSvelteKitRealtime is already present.

In `@packages/db/src/model/serialize.ts`:
- Line 13: The mapped type for materialization currently forces readonly on
every property ("{ readonly [K in keyof TValue]:
MaterializeSerializedValue<TValue[K]> }"), which unintentionally makes
serialized outputs immutable; change that mapped branch to not add readonly
(e.g. "{ [K in keyof TValue]: MaterializeSerializedValue<TValue[K]> }") so
MaterializeSerializedValue/TValue preserve original mutability instead of
coercing readonly.

In `@packages/db/tests/query-cache.test.ts`:
- Around line 310-330: The test creates a spy with vi.spyOn(console, 'error')
assigned to consoleError but never restores it; add consoleError.mockRestore()
(or call vi.restoreAllMocks()) after the "notifies remaining database dependency
listeners when one listener fails" test to restore console.error and avoid
leaking the mock into other tests—locate the consoleError variable and add the
mock restore at the end of that test (or in the test teardown that resets
DB/query-cache state).

In `@packages/realtime/src/client.ts`:
- Around line 566-572: The code currently sets connected = true immediately in
connect(), before transport.query() and transport.subscribe() complete; change
this so the store only flips connected to true after subscription startup is
established (i.e., after subscribe() has successfully started), and ensure error
paths revert connected to false and call warnRealtimeOnce with the transport
error; to avoid newer live snapshots being overwritten by a slower initial
query, add a monotonic startup token or version counter (e.g., startupId)
captured before calling transport.query and transport.subscribe, pass that token
into setSnapshot callbacks and have setSnapshot ignore any snapshot whose token
is older than the latest seen token (or track a boolean "seenLiveSnapshot" and
drop query snapshots if a live snapshot with higher token arrived), and ensure
unsubscribe is still assigned and cleaned up on any failure so the store is not
left wedged (update references to connected, transport.query<TResult>,
transport.subscribe<TResult>, setSnapshot, warnRealtimeOnce, and unsubscribe
accordingly).

In `@packages/realtime/src/definition.ts`:
- Around line 23-27: nextDefinitionName currently uses a shared counter
(getDefinitionState().nextDefinitionId) so fallback names vary with module
evaluation order; replace this unstable behavior by requiring explicit
definition names for transport-backed definitions: change
nextDefinitionName(kind: ...) to accept a mandatory name parameter (or throw if
name is missing) and update callers to pass an explicit, stable name;
alternatively, if you need automatic fallback, compute a deterministic name from
a stable source identifier passed into the API (e.g., a sourceId/file+position
string) and derive the fallback via a stable hash function instead of
state.nextDefinitionId—update references to nextDefinitionName and any call
sites that relied on implicit naming accordingly.

In `@packages/realtime/src/index.ts`:
- Around line 95-109: The server branch of the callable `definition` returns a
thenable from `importRealtimeRuntime().then(...).then(...)` but the exposed type
is `RealtimeResultFor<...>` (synchronous); fix by aligning the type contract:
add overloads for `definition` (or change its signature) so that when executed
in server/SSR it returns a Promise of the realtime result (e.g.
Promise<SerializeModels<...>> or Promise<RealtimeResultFor<...>>) and when
client-side it returns the synchronous `RealtimeResultFor<...>` from
`useRealtimeQuery`; update the function signature(s) for `definition`, adjust
casts around `RealtimeQueryDefinition` and ensure callers expect `Promise` when
`shouldExecuteOnServer()`/server environment, or alternatively remove
auto-execution on server and expose a separate async helper that calls
`importRealtimeRuntime().executeRealtimeQuery`.

---

Outside diff comments:
In `@packages/cli/src/project/registry-svelte.ts`:
- Around line 677-708: ensureNextManagedRoutes sets realtimeEnabled using
pathExists(resolve(projectRoot, 'server/realtime')) which returns false for
directories because pathExists delegates to readFileIfPresent; change realtime
detection in ensureNextManagedRoutes to check for a directory (e.g., use a
directory-aware helper or fs.stat/fs.promises.stat to test isDirectory) or check
for a known file inside server/realtime (like an index or pattern) so
realtimeEnabled becomes true when the server/realtime directory and its
definition files exist; update the realtimeEnabled assignment and any helper
calls (pathExists/readFileIfPresent) accordingly.

In `@packages/cli/src/project/scaffold.ts`:
- Around line 591-605: The scaffolder calls loadProjectConfig but ignores the
returned config and hardcodes realtimeRoot to resolve(projectRoot,
'server/realtime'); capture the project config from loadProjectConfig (e.g.,
const projectConfig = await loadProjectConfig(...)) and compute realtimeRoot
from projectConfig.paths.realtime if present (fallback to resolve(projectRoot,
'server/realtime')); use that computed realtimeRoot for pathExists
(realtimeDirectoryExists), mkdir(realtimeRoot,...), and pass it into any
rendering helpers that need the realtime path (for example instead of
renderNextRealtimeDefinitions(projectRoot) call the renderer with the resolved
realtimeRoot or otherwise update renderNextRealtimeDefinitions to accept the
realtime root so generated imports point to the correct location).

In `@packages/realtime/src/runtime.ts`:
- Around line 399-409: The subscription is added to state.subscriptions before
awaiting the initial onData callback, so if options.onData throws the
subscription remains registered; update subscribeRealtimeQuery() to wrap the
initial await options.onData?.(snapshot) in a try/catch and on error remove the
entry (state.subscriptions.delete(subscription.id)) before rethrowing the error
so the failed subscription is not left in state.subscriptions; reference the
created subscription object (subscription.id), the state.subscriptions map, and
the options.onData call to locate where to add the cleanup.

In `@packages/realtime/src/server.ts`:
- Line 32: The discovery currently treats declaration files like "*.d.ts" as
runnable because it only checks the last extension via realtimeFileExtensions;
update the realtime module filtering used by resolveRealtimeDefinition (and the
related discovery code that uses realtimeFileExtensions) to exclude TypeScript
declaration patterns by adding a check that the filepath does not match
/\.d\.(ts|mts|cts)$/ (or generally /\.d\.[^.]+$/) in addition to the existing
extension-set membership; keep realtimeFileExtensions as the allowed runtime
extensions but ensure the discovery predicate returns false for any path that
matches the declaration-file regex so *.d.ts, *.d.mts, and *.d.cts are skipped.

In `@packages/realtime/tests/realtime.test.ts`:
- Around line 258-263: The shared afterEach block currently calls
resetRealtimeClientRuntime(), resetRealtimeRuntime(),
resetDatabaseDependencyInvalidationListeners(), and resetDB() but does not
restore a mocked console.error; update this afterEach to restore the
console.error mock after tests that stub it (the stub created in the test file
around the console.error mock), e.g. call console.error.mockRestore() or invoke
jest.restoreAllMocks() in the afterEach so that any console.error mock is
removed and does not leak into later tests; keep the
resetRealtimeClientRuntime(), resetRealtimeRuntime(),
resetDatabaseDependencyInvalidationListeners(), and resetDB() calls intact.

---

Nitpick comments:
In `@packages/adapter-next/src/realtime-definition-transform.ts`:
- Around line 19-36: The skipString function currently treats backticks and
other delimiters like quotes the same, which fails to handle template-literal
expressions (`${...}`) and may mis-handle regex literals; update skipString to
detect backtick-delimited template literals and skip over embedded `${...}`
expression blocks (properly balancing braces and respecting escapes) and, where
applicable, add handling or detection to avoid treating regex literals as line
comments, or if you prefer not to implement full parsing now, add a clear TODO
comment in skipString noting these limitations (template expressions and regex
literals) and reference future work to parse `${...}` and distinguish regex
literals.

In `@packages/adapter-nuxt/src/realtime-definition-transform.ts`:
- Around line 1-343: The file duplicates the parsing/transform logic from
adapter-next; extract the shared functions (isIdentifierBoundary, skipString,
skipLineComment, skipBlockComment, findRealtimePropertyValueEnd,
findClosingBrace, applyReplacements, extractObjectPropertyValue,
collectRealtimeDefinitionExports, stripRealtimeServerHandlers, Replacement and
RealtimeDefinitionExport types) into a new shared module (e.g. a
`@holo-js/realtime/transform` utility) and import them here, keeping only
adapter-specific bits (the adapter import path and
createRealtimeClientDefinitionModule which uses collectRealtimeDefinitionExports
and the adapter import). Update createRealtimeClientDefinitionModule to call the
shared helpers and only inject the adapter-specific import string; ensure
exported symbols (createRealtimeClientDefinitionModule and
stripRealtimeServerHandlers) still exist or are re-exported as needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6ea009c4-a1c9-41e7-b3e7-4eb6cdbb63d6

📥 Commits

Reviewing files that changed from the base of the PR and between 465f109 and 9e91f41.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (77)
  • apps/blog-next/app/admin/posts/page.tsx
  • apps/blog-next/app/admin/posts/realtime/page.tsx
  • apps/blog-next/app/admin/posts/realtime/realtime-posts-demo.tsx
  • apps/blog-next/package.json
  • apps/blog-next/server/realtime/posts.ts
  • apps/blog-next/tests/run.mjs
  • apps/blog-nuxt/app/pages/admin/posts/index.vue
  • apps/blog-nuxt/app/pages/admin/posts/realtime.vue
  • apps/blog-nuxt/package.json
  • apps/blog-nuxt/server/realtime/posts.ts
  • apps/blog-nuxt/tests/run.mjs
  • apps/blog-sveltekit/package.json
  • apps/blog-sveltekit/server/realtime/posts.ts
  • apps/blog-sveltekit/src/routes/admin/posts/+page.svelte
  • apps/blog-sveltekit/src/routes/admin/posts/realtime/+page.svelte
  • apps/blog-sveltekit/tests/run.mjs
  • apps/blog-sveltekit/vite.config.ts
  • apps/docs/docs/realtime/index.md
  • packages/adapter-next/src/config.ts
  • packages/adapter-next/src/realtime-definition-loader.ts
  • packages/adapter-next/src/realtime-definition-transform.ts
  • packages/adapter-next/src/realtime.ts
  • packages/adapter-next/src/runtime.ts
  • packages/adapter-next/tests/client.test.ts
  • packages/adapter-next/tsup.config.ts
  • packages/adapter-nuxt/src/module.ts
  • packages/adapter-nuxt/src/nuxt-kit-shim.d.ts
  • packages/adapter-nuxt/src/realtime-definition-transform.ts
  • packages/adapter-nuxt/src/runtime/composables/realtime.ts
  • packages/adapter-nuxt/src/runtime/plugins/realtime.client.ts
  • packages/adapter-nuxt/src/runtime/plugins/realtime.ts
  • packages/adapter-nuxt/src/runtime/shims.d.ts
  • packages/adapter-nuxt/tests/client.test.ts
  • packages/adapter-sveltekit/package.json
  • packages/adapter-sveltekit/src/index.ts
  • packages/adapter-sveltekit/src/realtime-definition-transform.ts
  • packages/adapter-sveltekit/src/realtime.ts
  • packages/adapter-sveltekit/src/vite.ts
  • packages/adapter-sveltekit/tests/client.test.ts
  • packages/adapter-sveltekit/tests/package.test.ts
  • packages/adapter-sveltekit/tsup.config.ts
  • packages/broadcast/src/contracts.ts
  • packages/broadcast/src/index.ts
  • packages/broadcast/src/worker.ts
  • packages/broadcast/tests/worker.test.ts
  • packages/cli/src/broadcast.ts
  • packages/cli/src/cli.ts
  • packages/cli/src/dev.ts
  • packages/cli/src/project/registry-svelte.ts
  • packages/cli/src/project/scaffold.ts
  • packages/cli/src/project/scaffold/framework-renderers.ts
  • packages/cli/tests/cli.test.ts
  • packages/db/src/cache.ts
  • packages/db/src/index.ts
  • packages/db/src/model/index.ts
  • packages/db/src/model/serialize.ts
  • packages/db/src/model/slug.ts
  • packages/db/src/model/types.ts
  • packages/db/tests/query-cache.test.ts
  • packages/db/tests/type-system.test.ts
  • packages/media/src/defineMediaModel.ts
  • packages/media/tests/media.test.ts
  • packages/realtime/src/client.ts
  • packages/realtime/src/contracts.ts
  • packages/realtime/src/definition.ts
  • packages/realtime/src/index.ts
  • packages/realtime/src/runtime.ts
  • packages/realtime/src/server.ts
  • packages/realtime/tests/realtime.client.test.ts
  • packages/realtime/tests/realtime.declaration.test.ts
  • packages/realtime/tests/realtime.test.ts
  • packages/realtime/tests/realtime.type.test.ts
  • packages/realtime/tsup.config.ts
  • packages/validation/src/contracts.ts
  • tests/example-app-auth-flow.mjs
  • tests/example-app-broadcast-browser-flow.mjs
  • tests/example-app-realtime-browser-flow.mjs
💤 Files with no reviewable changes (1)
  • packages/validation/src/contracts.ts
✅ Files skipped from review due to trivial changes (3)
  • apps/blog-next/app/admin/posts/realtime/page.tsx
  • packages/adapter-next/src/realtime-definition-loader.ts
  • apps/docs/docs/realtime/index.md
🚧 Files skipped from review as they are similar to previous changes (13)
  • packages/adapter-nuxt/src/nuxt-kit-shim.d.ts
  • packages/realtime/tsup.config.ts
  • packages/adapter-nuxt/src/runtime/plugins/realtime.client.ts
  • packages/adapter-next/src/realtime.ts
  • packages/adapter-next/tsup.config.ts
  • packages/adapter-sveltekit/tsup.config.ts
  • packages/adapter-sveltekit/tests/package.test.ts
  • packages/realtime/src/contracts.ts
  • packages/adapter-sveltekit/src/realtime.ts
  • packages/adapter-sveltekit/package.json
  • packages/cli/tests/cli.test.ts
  • packages/cli/src/cli.ts
  • packages/db/src/cache.ts

Comment on lines +32 to +38
const title = args.title.trim()
const post = await Post.findOrFail(args.id)
await authorize('update', post)
await post.update({
title,
slug: await uniqueSlug(Post, title, { ignore: args.id }),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Trimmed title can bypass minimum-length validation and persist invalid values.

Line 32 trims after validation in Line 28. A value like " " can satisfy .min(3) pre-trim and then be saved as an empty title/slug at Lines 35-38. Validate the normalized value before update.

Suggested fix
   handler: async ({ args }) => {
     const title = args.title.trim()
+    if (title.length < 3) {
+      throw new Error('Title must be at least 3 characters after trimming.')
+    }
     const post = await Post.findOrFail(args.id)
     await authorize('update', post)
     await post.update({
       title,
       slug: await uniqueSlug(Post, title, { ignore: args.id }),
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const title = args.title.trim()
const post = await Post.findOrFail(args.id)
await authorize('update', post)
await post.update({
title,
slug: await uniqueSlug(Post, title, { ignore: args.id }),
})
const title = args.title.trim()
if (title.length < 3) {
throw new Error('Title must be at least 3 characters after trimming.')
}
const post = await Post.findOrFail(args.id)
await authorize('update', post)
await post.update({
title,
slug: await uniqueSlug(Post, title, { ignore: args.id }),
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blog-next/server/realtime/posts.ts` around lines 32 - 38, The code trims
the title after validation so inputs like "   " can pass the pre-trim .min(3)
check and be saved empty; fix by normalizing/triming the incoming title before
validating and updating: compute a trimmedTitle = args.title.trim(), run the
existing validation logic against trimmedTitle (or enforce trimmedTitle.length
>= 3), then use trimmedTitle when calling uniqueSlug(Post, trimmedTitle, {
ignore: args.id }) and when passing title to post.update; keep the
authorize/Post.findOrFail flow unchanged.

Comment on lines +32 to +38
const title = args.title.trim()
const post = await Post.findOrFail(args.id)
await authorize('update', post)
await post.update({
title,
slug: await uniqueSlug(Post, title, { ignore: args.id }),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalized title is not revalidated before persistence.

Line 32 trims the input after schema checks on Line 28. Whitespace-only values can pass .min(3) before trim and still be written as empty/invalid title and slug at Lines 35-38. Revalidate after trimming.

Suggested fix
   handler: async ({ args }) => {
     const title = args.title.trim()
+    if (title.length < 3) {
+      throw new Error('Title must be at least 3 characters after trimming.')
+    }
     const post = await Post.findOrFail(args.id)
     await authorize('update', post)
     await post.update({
       title,
       slug: await uniqueSlug(Post, title, { ignore: args.id }),
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const title = args.title.trim()
const post = await Post.findOrFail(args.id)
await authorize('update', post)
await post.update({
title,
slug: await uniqueSlug(Post, title, { ignore: args.id }),
})
const title = args.title.trim()
if (title.length < 3) {
throw new Error('Title must be at least 3 characters after trimming.')
}
const post = await Post.findOrFail(args.id)
await authorize('update', post)
await post.update({
title,
slug: await uniqueSlug(Post, title, { ignore: args.id }),
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blog-nuxt/server/realtime/posts.ts` around lines 32 - 38, The title is
trimmed after validation so whitespace-only titles can bypass the .min(3) check;
update the flow to revalidate the normalized title before persisting by either
trimming before schema validation or running the validator again on the trimmed
value: compute const title = args.title.trim() first, then validate it against
your title schema (or call the same validation used at Line 28) before calling
Post.findOrFail, authorize, and post.update(...), and ensure uniqueSlug(Post,
title, { ignore: args.id }) uses that revalidated title.

Comment on lines +26 to +33
args: schema({
id: field.number().integer(),
title: field.string().required('Title is required.').min(3),
}),
access: 'authenticated',
handler: async ({ args }) => {
const title = args.title.trim()
const post = await Post.findOrFail(args.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Trim happens after schema validation, allowing invalid titles to be persisted.

Line 28 validates the untrimmed string, then Line 32 trims it. Inputs like " " or " a " can pass validation and still write an empty/too-short title.

[suggest fix]

Patch
   handler: async ({ args }) => {
     const title = args.title.trim()
+    if (title.length < 3) {
+      throw new Error('Title must be at least 3 characters.')
+    }
     const post = await Post.findOrFail(args.id)
     await authorize('update', post)
     await post.update({
       title,
       slug: await uniqueSlug(Post, title, { ignore: args.id }),
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
args: schema({
id: field.number().integer(),
title: field.string().required('Title is required.').min(3),
}),
access: 'authenticated',
handler: async ({ args }) => {
const title = args.title.trim()
const post = await Post.findOrFail(args.id)
handler: async ({ args }) => {
const title = args.title.trim()
if (title.length < 3) {
throw new Error('Title must be at least 3 characters.')
}
const post = await Post.findOrFail(args.id)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blog-sveltekit/server/realtime/posts.ts` around lines 26 - 33, The
schema currently validates the raw args.title then the handler trims it,
allowing inputs like "   " or " a " to pass; update the args schema so the title
is trimmed before validation (e.g., use field.string().transform(s => s.trim())
or an equivalent sanitizer on the title field in the args: schema) and adjust
the handler (handler, title variable) to use the already-trimmed args.title
(remove the separate title = args.title.trim()) before calling Post.findOrFail.

Comment thread packages/adapter-nuxt/src/runtime/composables/realtime.ts Outdated
Comment on lines +44 to +46
if (value === undefined) {
return reactive([]) as TValue
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Initializing undefined as an empty array causes type mismatches.

When a query's initial snapshot is undefined, this returns reactive([]), assuming the result will be an array. If the actual query returns an object, the code later calls replaceReactiveArrayObject (lines 65–68) to force the array into an object shape, creating a hybrid structure.

Returning a reactive empty array for undefined values is only safe if the query result type is guaranteed to be an array. For object-typed queries, this leads to the type-mismatch logic at line 65.

♻️ Proposed fix to avoid type assumption

Return undefined when value is undefined, and let the consumer handle the pending state:

 function createRealtimeReactiveValue<TValue>(value: TValue): TValue {
-  if (value === undefined) {
-    return reactive([]) as TValue
-  }
-
   if (Array.isArray(value)) {
     return reactive([...value]) as TValue
   }

   if (isPlainObject(value)) {
     return reactive({ ...value }) as TValue
   }

   return value
 }

Then update line 83 to handle the undefined case:

-let current = createRealtimeReactiveValue(store.snapshot?.data) as RealtimeResultFor<TDefinition>
+let current = (store.snapshot?.data !== undefined
+  ? createRealtimeReactiveValue(store.snapshot.data)
+  : undefined) as RealtimeResultFor<TDefinition>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/adapter-nuxt/src/runtime/composables/realtime.ts` around lines 44 -
46, The code returns reactive([]) as TValue when value === undefined which
assumes an array result and causes downstream mixing with
replaceReactiveArrayObject; instead, when value === undefined return undefined
(preserve the pending state) rather than reactive([]) and adjust the consumer
logic that currently expects an array—specifically update the code that calls
replaceReactiveArrayObject and the branch around the later check (the area
referencing TValue and the line with reactive([])) so it explicitly handles
undefined results (e.g., treat undefined as pending before converting snapshots)
rather than forcing an empty array.

Comment thread packages/db/src/model/serialize.ts Outdated
Comment thread packages/db/tests/query-cache.test.ts
Comment thread packages/realtime/src/client.ts Outdated
Comment on lines +23 to +27
export function nextDefinitionName(kind: 'query' | 'mutation'): string {
const state = getDefinitionState()
state.nextDefinitionId += 1
return `realtime.${kind}.${state.nextDefinitionId}`
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Fallback definition names are not stable across bundles.

nextDefinitionName() depends on module evaluation order, so unnamed definitions can get different names in the browser and server runtimes. Those names are part of the transport/store contract, which means an unrelated import-order change can make the client call a definition name the server never registered. Please derive fallback names from a stable source identifier or require explicit names for transport-backed definitions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/realtime/src/definition.ts` around lines 23 - 27, nextDefinitionName
currently uses a shared counter (getDefinitionState().nextDefinitionId) so
fallback names vary with module evaluation order; replace this unstable behavior
by requiring explicit definition names for transport-backed definitions: change
nextDefinitionName(kind: ...) to accept a mandatory name parameter (or throw if
name is missing) and update callers to pass an explicit, stable name;
alternatively, if you need automatic fallback, compute a deterministic name from
a stable source identifier passed into the API (e.g., a sourceId/file+position
string) and derive the fallback via a stable hash function instead of
state.nextDefinitionId—update references to nextDefinitionName and any call
sites that relied on implicit naming accordingly.

Comment on lines +95 to +109
const definition = ((args?: RealtimeArgsForSchema<TSchema>) => {
const normalizedArgs = (args ?? {}) as RealtimeArgsFor<RealtimeQueryDefinition<TName, TSchema, TAccess, SerializeModels<Awaited<TResult>>>>
if (shouldExecuteOnServer()) {
return importRealtimeRuntime()
.then(({ executeRealtimeQuery }) => executeRealtimeQuery(
definition as unknown as RealtimeQueryDefinition<TName, TSchema, TAccess, SerializeModels<Awaited<TResult>>>,
normalizedArgs,
))
.then(result => result.data) as RealtimeResultFor<RealtimeQueryDefinition<TName, TSchema, TAccess, SerializeModels<Awaited<TResult>>>>
}

return useRealtimeQuery(
definition as unknown as RealtimeQueryDefinition,
normalizedArgs,
) as RealtimeResultFor<RealtimeQueryDefinition<TName, TSchema, TAccess, SerializeModels<Awaited<TResult>>>>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Server-side query() calls return a Promise behind a synchronous type.

The server branch returns importRealtimeRuntime().then(...).then(...), but this callable is exposed as RealtimeResultFor<...>, not Promise<...>. In SSR/Node code that means TypeScript allows direct data access while runtime returns a thenable instead. Please align the contract here: either make server-executed queries explicitly async in the type system, or stop auto-executing them from the callable and require a separate async server helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/realtime/src/index.ts` around lines 95 - 109, The server branch of
the callable `definition` returns a thenable from
`importRealtimeRuntime().then(...).then(...)` but the exposed type is
`RealtimeResultFor<...>` (synchronous); fix by aligning the type contract: add
overloads for `definition` (or change its signature) so that when executed in
server/SSR it returns a Promise of the realtime result (e.g.
Promise<SerializeModels<...>> or Promise<RealtimeResultFor<...>>) and when
client-side it returns the synchronous `RealtimeResultFor<...>` from
`useRealtimeQuery`; update the function signature(s) for `definition`, adjust
casts around `RealtimeQueryDefinition` and ensure callers expect `Promise` when
`shouldExecuteOnServer()`/server environment, or alternatively remove
auto-execution on server and expose a separate async helper that calls
`importRealtimeRuntime().executeRealtimeQuery`.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/adapter-sveltekit/src/realtime-definition-transform.ts`:
- Around line 143-145: findClosingBrace() can prematurely count braces inside
regex literals because it only skips strings/comments; update it to reuse the
same regex-skipping logic used by findRealtimePropertyValueEnd(): when
encountering '/' and isRegexStart(source, cursor) call cursor =
skipRegex(source, cursor) and continue. Modify findClosingBrace() (used by
collectRealtimeDefinitionExports()) to import/ reference isRegexStart and
skipRegex so regex literals are skipped the same way as in
findRealtimePropertyValueEnd().

In `@packages/adapter-sveltekit/tests/realtime-definition-transform.test.ts`:
- Around line 5-17: The test fixture’s regex is too benign; update the source in
the test so the regex literal is adversarial (e.g., contains delimiter
characters or an unbalanced brace such as a literal "}" or a "/" inside the
regex) so a scanner that doesn’t treat regex literals specially will break
findRealtimePropertyValueEnd; adjust the source used by
stripRealtimeServerHandlers (in this test) to use that adversarial regex and
tighten the assertion to directly check that stripRealtimeServerHandlers(source)
replaces the handler (referencing stripRealtimeServerHandlers and
findRealtimePropertyValueEnd) by asserting the output contains "handler:
undefined," and does not contain the original regex literal or handler body.

In `@packages/cli/src/project/scaffold.ts`:
- Around line 186-191: The current injectSvelteRealtimeVitePlugin() only
rewrites inline arrays and returns undefined when the config uses a plugins
variable (e.g., const plugins = [...]; defineConfig({ plugins })), causing
installRealtimeIntoProject() to silently report success; update
injectSvelteRealtimeVitePlugin (the withImport/withPlugin logic) to detect
non-inline plugin usage (look for patterns like defineConfig({ plugins }),
object shorthand "plugins" or "plugins," without a bracketed array) and fail
fast by returning an explicit error or throwing so installRealtimeIntoProject()
can surface failure instead of proceeding; ensure you reference
injectSvelteRealtimeVitePlugin, withImport, withPlugin and adjust the caller
behavior in installRealtimeIntoProject to treat a non-string/undefined result as
an error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 269fba7b-a67e-455e-ab12-b2d45169e969

📥 Commits

Reviewing files that changed from the base of the PR and between 9e91f41 and a222f15.

📒 Files selected for processing (18)
  • packages/adapter-nuxt/src/runtime/composables/realtime.ts
  • packages/adapter-sveltekit/src/realtime-definition-transform.ts
  • packages/adapter-sveltekit/tests/realtime-definition-transform.test.ts
  • packages/broadcast/src/worker.ts
  • packages/broadcast/tests/worker.test.ts
  • packages/cli/src/broadcast.ts
  • packages/cli/src/project/registry-svelte.ts
  • packages/cli/src/project/scaffold.ts
  • packages/core/src/portable/holo.ts
  • packages/db/src/model/serialize.ts
  • packages/db/tests/query-cache.test.ts
  • packages/realtime/src/client.ts
  • packages/realtime/src/contracts.ts
  • packages/realtime/src/index.ts
  • packages/realtime/src/runtime.ts
  • packages/realtime/tests/realtime.client.test.ts
  • packages/validation/src/contracts-support.ts
  • packages/validation/tests/contracts.test.ts
💤 Files with no reviewable changes (1)
  • packages/realtime/tests/realtime.client.test.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/db/tests/query-cache.test.ts
  • packages/adapter-nuxt/src/runtime/composables/realtime.ts
  • packages/db/src/model/serialize.ts
  • packages/realtime/src/contracts.ts
  • packages/cli/src/project/registry-svelte.ts
  • packages/realtime/src/runtime.ts
  • packages/realtime/src/index.ts
  • packages/realtime/src/client.ts

Comment thread packages/adapter-sveltekit/src/realtime-definition-transform.ts
Comment thread packages/adapter-sveltekit/tests/realtime-definition-transform.test.ts Outdated
Comment thread packages/cli/src/project/scaffold.ts Outdated
@cobraprojects
cobraprojects merged commit 35abc66 into main Jun 7, 2026
1 check passed
@cobraprojects
cobraprojects deleted the realtime-package branch June 21, 2026 10:31
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