qwik-core-development

Use when modifying or reviewing Qwik core package code under packages/qwik, especially reactive primitives, signals, VNodes, cursor behavior, QRLs, optimizer-facing runtime behavior, or Qwik core tests.

Install
npx skills add 'https://github.com/QwikDev/qwik/tree/main/.ruler/skills/qwik-core-development'
Download bundle ↓
main · 638f809Scanned 2026-09-17

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

references/core-notes.md

references/core-notes.mdBrowse 2 files
View on GitHub
← Back to SKILL.md

Qwik Core Notes

Use this reference only after loading qwik-core-development and only for core runtime work that needs more detail than the skill body. Keep it current when source changes make a note stale.

Maintainer Bias From History

Recent core work by Varixo and Wout tends to:

  • isolate the broken invariant before changing code;
  • keep producer and consumer protocol changes together;
  • add focused regression tests beside the changed behavior;
  • preserve compatibility paths deliberately and test them;
  • extract small semantic helpers when they make ownership or ordering clearer;
  • leave comments only for non-obvious runtime encodings, lifecycle ordering, or compatibility constraints.

Use that style for core work. Avoid broad rewrites, unexplained fallbacks, and temporary debug code.

Source Map

packages/qwik/src/core/
  client/                     client render, VNode materialization, cursor/diff integration
  reactive-primitives/         signals, async signals, stores, subscriptions
  shared/qrl/                  QRL classes and helpers
  shared/serdes/               serialization, inflation, root refs
  shared/vnode/                VNode structures
  shared/cursor/               cursor walking and DOM update primitives
  ssr/                         server-side JSX rendering
  tests/                       core feature specs

Closest tests are usually in the same subtree and named *.unit.ts(x) or *.spec.ts(x).

AsyncSignal Current Model

Current API and implementation facts:

  • The async engine (AsyncJob, loading/error, cleanup) lives in ComputedSignalImpl; AsyncSignalImpl only parses options and sets AsyncSignalFlags.ASYNC_MODE | CTX_ARG.
  • All compute fns receive the ComputeCtx argument (track, previous, info, cleanup, abortSignal); sync computeds allocate an AsyncJob per compute and run the previous job's cleanups before recomputing. CTX_ARG signals (createAsyncQrl/useResource$) track only via the explicit ctx.track(); computeds auto-track synchronous reads via a dedicated invoke context, but that context is lost after the first await — later reads must use ctx.track().
  • A computed whose fn returns a promise lazily switches on ASYNC_MODE (loading state stays declared until then) and then has the full AsyncSignal API. The clientOnly option sets ASYNC_MODE at construction, since SSR can never resolve such a signal synchronously. Sync compute throws stay in sync mode but still land in .error; reading .value rethrows until a recompute or explicit value set clears it. Thrown promises must keep propagating for retry, never be captured as errors.
  • Serialization keys off ASYNC_MODE, not instanceof: async-mode computeds round-trip as TypeIds.AsyncSignal and resume as AsyncSignalImpl instances whose serialized flags (no CTX_ARG) preserve auto-track semantics. Runtime checks must use flags, not class identity.
  • createAsyncSignal() passes the full AsyncSignalOptions object to the constructor.
  • Signals have no built-in polling or expiration. Polling lives in usePoll(signal, expires) from @qwik.dev/utils: an interval-based visible task (document-idle) that calls invalidate() and resets on the pending flip. There is no expires/poll/interval option or property.
  • invalidate() keeps the previous value readable while recomputing; clear() drops the value first so readers suspend (the old allowStale: false behavior, now explicit and one-shot).
  • clientOnly skips server computation and computes on first client read.
  • eagerCleanup schedules cleanup after subscribers drop to zero.

When changing AsyncSignal behavior, inspect:

  • packages/qwik/src/core/reactive-primitives/impl/async-signal-impl.ts
  • packages/qwik/src/core/reactive-primitives/types.ts
  • packages/qwik/src/core/reactive-primitives/signal.public.ts
  • packages/qwik/src/core/reactive-primitives/cleanup.ts
  • packages/qwik/src/core/shared/serdes/serialize.ts
  • packages/qwik/src/core/shared/serdes/inflate.ts
  • closest async signal unit/spec tests

AsyncSignal Invariants

  • A first unresolved read may throw the compute promise; tests should use retryOnPromise() when exercising first-read behavior.
  • .value, .loading, and .error have separate subscriber sets. Subscriber-sensitive logic must account for all three.
  • invalidate(info) records the latest info and increments the info version.
  • AbortError is cancellation, not a user-visible .error.
  • Reading .pending or .error triggers computation when needed; serialization must read the private $untrackedPending$/$untrackedError$ fields to avoid starting computes.
  • clientOnly resume rides on the state script's q-d:qidle _res QRL built from $eagerResume$; SSR must emit the state script whenever $eagerResume$ is non-empty, even if no roots were discovered yet (the QRL captures become roots during attribute serialization).
  • Timeout IDs must be cleared in invalidation, destroy, and reschedule paths.
  • Browser timers must not run during SSR. Current code uses isServer plus the test platform check.
  • Node timers that can keep the process alive should use .unref?.().

AsyncSignal Test Patterns

Use the current test helpers already present in nearby tests:

await withContainer(async () => {
  const signal = createAsyncQrl($(async () => 42)) as AsyncSignalImpl<number>;

  await retryOnPromise(() => {
    effect$(() => signal.value);
  });

  await signal.promise();
  expect(signal.value).toBe(42);
});

For mutable counters captured by $() closures, use an object ref:

const ref = { calls: 0 };
const signal = createAsyncQrl(
  $(async () => {
    ref.calls++;
    return ref.calls;
  })
);

Do not capture and mutate primitive let bindings from $() tests; optimizer serialization can turn the binding into a const-like captured value.

Serialization And Inflation

When a core value gains serialized state:

  1. Update the serializer and inflater together.
  2. Keep array positions or marker encodings documented in the code that owns them.
  3. Add a round-trip test in shared/serdes or the closest subsystem.
  4. Check SSR and client resume behavior when the value affects resumed or streamed state.

For AsyncSignal fields, inspect the serdes tests that deserialize async signals and verify concurrency, timeout, stale value, and error/loading state behavior.

VNode, Cursor, And Streaming

Core rendering changes often cross multiple boundaries:

  • SSR emits HTML, VNode data, event data, state, and sometimes streamed patches.
  • Client startup materializes VNodes lazily from DOM plus qwik/vnode data.
  • Cursor work must preserve render promise resolution and not orphan paused cursors.
  • Qwikloader changes need behavior tests because they run outside normal framework code.

When touching these areas:

  • trace the owner of each marker or ID from emitter to consumer;
  • keep numeric/string encodings deterministic;
  • test root and nested/container cases when a feature can appear in both;
  • include streaming or out-of-order cases when state can arrive after initial event listeners.

QRL And Optimizer-Facing Runtime

  • Use $-suffixed APIs and $() in tests when a QRL boundary is expected.
  • Avoid manual QRL construction unless nearby tests already use it for the same reason.
  • If runtime behavior relies on optimizer output, inspect the optimizer transform and snapshot too.
  • For event or JSX attribute changes, keep event-names, JSX runtime, qwikloader, and optimizer behavior aligned.

Focused Verification

Use the closest command first:

pnpm vitest run packages/qwik/src/core/reactive-primitives/impl/async-signal.unit.tsx
pnpm vitest run packages/qwik/src/core/shared/serdes/serdes.unit.ts
pnpm vitest run packages/qwik/src/qwikloader.behavior.unit.ts
pnpm build.core.dev
pnpm api.update

Use e2e only when unit/spec tests cannot cover the behavior, such as real browser event timing, streaming, navigation, or integration with fixture apps. For Qwik e2e, load qwik-e2e-verification.

Never use pnpm test.unit for agent verification in this repo.

ErrorBoundary (experimental errorBoundary)

Keep error state non-enumerable and out of serialized state. Store the raw throw and project it only at display sites; redaction is origin-based — the server display redacts in production unless the app returns an Error from transformError; the client display always shows the error as thrown.

Reset must re-render the component that authored projected children. Identify and retain that component as soon as SSR error teardown determines it; store its VNode reference only when the projection cut prevents the client owner walk. Re-key the highest wrapper below that author so normal diffing recreates emptied projected content without projection-wide scheduling.

Keep This Reference Fresh

Before finishing a core task, ask:

  1. Did current source contradict anything in this reference?
  2. Did the task teach a durable pattern that future core work should reuse?
  3. Is the lesson specific enough to belong here rather than in .ruler/AGENTS.md?

If yes, update this file in the same task when scope allows it. Prefer replacing stale text over appending another long lesson.

Referenced from SKILL.md