Skip to content

feat: add @karnak19/pbkit-realtime plugin for typed realtime subscriptions - #27

Merged
Karnak19 merged 8 commits into
mainfrom
feat/pbkit-realtime
Jun 1, 2026
Merged

feat: add @karnak19/pbkit-realtime plugin for typed realtime subscriptions#27
Karnak19 merged 8 commits into
mainfrom
feat/pbkit-realtime

Conversation

@Karnak19

@Karnak19 Karnak19 commented May 6, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #23

Adds @karnak19/pbkit-realtime — a new plugin that generates typed realtime subscription helpers from the PocketBase schema, using the built-in SSE system.

What it generates

realtime.gen.ts with:

  • RealtimeAction type ("create" | "update" | "delete")
  • RealtimeEvent<T> interface
  • subscribeTo{Collection}(callback, options?) per non-excluded collection
    • Typed callback with RealtimeEvent<{Collection}Record>
    • Optional filter (PocketBase filter string) and id (specific record)
    • Returns unsubscribe function () => Promise<void>
  • Respects collections config (isCollectionExcluded)

Files changed

  • New package: packages/pbkit-realtime/ — plugin implementation + 17 tests
  • Docs: apps/docs/src/content/docs/plugins/realtime.md — full plugin page
  • Skill: skills/pbkit/SKILL.md — updated with realtime section, tags, agent workflow
  • Playground: apps/playground/pbkit.config.ts — added realtimePlugin
  • Lockfile: bun.lock — workspace registration

Related

@Karnak19

Karnak19 commented May 6, 2026

Copy link
Copy Markdown
Owner Author

/oc review

@opencode-agent

opencode-agent Bot commented May 6, 2026

Copy link
Copy Markdown

PR Review: @karnak19/pbkit-realtime

Summary

New plugin that generates typed SSE subscription helpers per PocketBase collection. Implementation follows existing plugin patterns (pbkit-zod, pbkit-tanstack) closely. Code is clean and docs are thorough, but there are several issues to address.


Critical

  1. Filter injection vulnerability (generate.ts:33, realtime.md:125) — Docs show string interpolation in filters (article = "${articleId}"), which is an injection vector. Should encourage PocketBase's parameterized placeholders ({:param}) instead.

Major

  1. Unused singularize variable (generate.ts:21) — const s = pascalCase(singularize(col.name)) is computed but never used. Dead code.

  2. Duplicated utility functionspascalCase, singularize, lowerFirst are copy-pasted from pbkit-tanstack. Should be extracted to a shared utility.

  3. Unsound as casts (generate.ts:34) — e.action as RealtimeAction and e.record as ${recordType} suppress type errors rather than fixing them. Consider using PocketBase SDK's typed RecordSubscription or adding a runtime guard.

  4. Snapshot ordering fragility (realtime.test.ts.snap) — Snapshot is insertion-order-dependent. If parseJson ever reorders collections, the snapshot breaks (though individual toContain assertions are order-agnostic).

Minor

  1. singularize false positives (generate.ts:15) — statusstatu, canvascanva, busbu. Currently unused in output but broken if used later.

  2. No edge-case tests — Missing tests for empty schema, collection names with separators (e.g. my-cool-things).

  3. TypeScript ^6.0.0 devDependency (package.json:44) — Doesn't exist yet. Codebase-wide issue though.

Suggestions

  • Extract shared string utils to @karnak19/pbkit exports (all 3 plugins duplicate them)
  • Consider generating subscribeToXxxRecord singular aliases
  • Consider an async iterable API as a future enhancement (for await)

Verdict: Approve with changes

The implementation is solid and follows conventions well. The filter injection docs issue and unused dead code should be fixed before merge. Duplicated utilities should be cleaned up in this PR or a fast-follow.

New%20session%20-%202026-05-06T12%3A27%3A58.244Z
opencode session  |  github run

@Karnak19

Karnak19 commented May 6, 2026

Copy link
Copy Markdown
Owner Author

/oc review

@opencode-agent

opencode-agent Bot commented May 6, 2026

Copy link
Copy Markdown

PR Review: @karnak19/pbkit-realtime

Summary

New plugin generating typed SSE subscription helpers per PocketBase collection. Follows existing plugin patterns closely. Code is clean, tests are thorough, docs are well-written. A few issues to address.


Issues

MEDIUM — Duplicated pascalCase utility (generate.ts:5-10)
The identical pascalCase function is copy-pasted across all three plugins (pbkit-zod, pbkit-tanstack, pbkit-realtime). Should be extracted to the core @karnak19/pbkit package. This is a maintenance burden — fix in one place, miss in others.

MEDIUM — Unsafe filter interpolation in docs (realtime.md:124)
The React example shows { filter: article = "${articleId}" } — direct string interpolation into a PocketBase filter. While line 140 adds a warning note, the primary example users will copy-paste demonstrates the unsafe pattern. Should use PocketBase's parameterized syntax ({:param}) in the example itself, not just warn about it after.

LOW — Hardcoded client.gen import path (generate.ts:43)
import { client } from "./client.gen" is hardcoded. The ctx object provides configurable typesImport and sdkImport, but there's no clientImport. Consistent with how other plugins work today, but fragile if the client output path ever becomes configurable.

LOW — as casts suppress type errors (generate.ts:26)
e.action as RealtimeAction and e.record as ${recordType} bypass type checking. Acceptable for codegen since PocketBase SSE is stable, but worth noting.

LOW — Non-deterministic import order in snapshot (realtime.test.ts.snap)
The snapshot's type import order (UsersRecord, CategoriesRecord, ArticlesRecord, CommentsRecord) depends on collection iteration order. Sorting alphabetically would make it deterministic.

LOW — Test ctx not typed as PluginContext (realtime.test.ts:7-11)
The test context object isn't explicitly typed. Adding satisfies PluginContext would catch future required-field additions.


Verdict: Approve with changes

The implementation is solid and well-tested. The filter interpolation in the docs example and the duplicated pascalCase should be fixed before merge. The client.gen hardcoding and as casts are acceptable for now.

New%20session%20-%202026-05-06T13%3A04%3A31.546Z
opencode session  |  github run

@Karnak19

Karnak19 commented May 6, 2026

Copy link
Copy Markdown
Owner Author

/oc review

@opencode-agent

opencode-agent Bot commented May 6, 2026

Copy link
Copy Markdown

PR Review: @karnak19/pbkit-realtime

Critical

Filter injection vulnerability (realtime.md:124) — The React example uses direct string interpolation into a PocketBase filter (article = "${articleId}"). If articleId comes from user input, this is exploitable. The example should use parameterized escaping or PocketBase's {:param} syntax instead of demonstrating the unsafe pattern first and warning after.

Major

  1. Return type mismatch (generate.ts:16) — Generated signature returns () => Promise<void> but client.collection().unsubscribe() returns void, not Promise<void>. Consumers writing await unsub() get a silent no-op.

  2. Missing pocketbase peer dependency (package.json) — Docs say PocketBase is a peer dependency but package.json declares none. Sibling packages (pbkit-tanstack, pbkit-zod) declare theirs.

  3. Hardcoded client.gen import path (generate.ts:36) — Uses "./client.gen" instead of deriving from ctx like tanstack's ctx.sdkImport. Breaks if output structure is customized.

Minor

  • No READMEpackage.json lists "README.md" in files but no README exists. npm publish will warn.
  • as casts (generate.ts:26) — Bypasses type checking rather than using typed RecordSubscription.
  • Snapshot fragility — Import order depends on collection iteration order; sorting alphabetically would be deterministic.
  • Test gaps — No tests for empty schema, auth collections, or edge-case names (foo__bar, names starting with numbers).

Verdict: Approve with changes

Core implementation is clean and follows existing patterns well. Filter injection docs, return type mismatch, and missing peer dependency should be fixed before merge.

New%20session%20-%202026-05-06T14%3A50%3A53.996Z
opencode session  |  github run

@Karnak19
Karnak19 force-pushed the feat/pbkit-realtime branch from ebbeec7 to 10f7cc4 Compare June 1, 2026 11:15

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review of @karnak19/pbkit-realtime plugin. The implementation is clean, tests are thorough, and the pascalCase extraction into core is a good improvement. Two issues to address:

  • Return type mismatch (generate.ts:16): Generated unsubscribe is typed () => void but PocketBase SDK's UnsubscribeFunc is () => Promise<void>. Consumers won't know to await the unsubscribe, risking unhandled rejections.
  • Missing peerDependencies (package.json): Sibling plugins (pbkit-tanstack, pbkit-zod) declare runtime peer deps; this package omits pocketbase.
  • Missing changeset: This PR adds a new published package and modifies existing packages (@karnak19/pbkit exports pascalCase, tanstack/zod import changes). A .changeset/*.md entry is needed for proper versioning.

Publish @karnak19/pbkit-realtime at 0.1.0 and bump @karnak19/pbkit (minor)
so the pascalCase export the plugin imports is available in a published
version. Set realtime version to 0.0.0 so the minor changeset lands 0.1.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two issues carried over from the previous review round — both still present in the current diff:

  • Return type mismatch (generate.ts:16): Generated unsubscribe function is typed () => void but PocketBase SDK returns () => Promise<void>, risking unhandled rejections. (Correctness)
  • Missing pocketbase peerDependency (package.json:39): Sibling plugins declare runtime deps of generated code as peerDependencies; this package does not. (Package hygiene)

Comment thread packages/pbkit-realtime/src/generate.ts Outdated
lines.push(`export async function ${fnName}(`);
lines.push(` callback: (event: RealtimeEvent<${recordType}>) => void,`);
lines.push(` options?: { filter?: string; id?: string },`);
lines.push(`): Promise<() => void> {`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Problem: The generated return type Promise<() => void> doesn't match the PocketBase SDK's actual return type. RecordService.subscribe() returns Promise<UnsubscribeFunc> where UnsubscribeFunc = () => Promise<void> (per the SDK source). The unsubscribe function is async — it calls unsubscribeByTopicAndListener which awaits submitSubscriptions().

Impact: TypeScript allows () => Promise<void> to be assigned to () => void (void return means "value ignored"), so the code compiles. But consumers see unsub() as synchronous and won't await it. If the unsubscribe network request fails, the resulting promise rejection is unhandled. The docs also demonstrate unsub() without await, reinforcing the wrong pattern.

Fix: Change the generated return type and update the docs/README accordingly:

lines.push(`): Promise<() => Promise<void>> {`);

Then in the docs and README, show await unsub() instead of unsub(). The React example's cleanup type should also change to (() => Promise<void>) | undefined.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 19adf11. Generated helpers now return Promise<() => Promise<void>> (PocketBase UnsubscribeFunc). Updated the snapshot, the Promise<() => void> assertion, and the docs/README/SKILL to await unsub(); the React example cleanup type is now (() => Promise<void>) | undefined.

"access": "public"
},
"license": "MIT",
"dependencies": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Problem: This package doesn't declare pocketbase as a peerDependency, even though the generated realtime.gen.ts code depends on it at runtime (via client.gen.ts which imports from pocketbase).

Impact: Both sibling plugins follow the peerDependency pattern — pbkit-tanstack declares @tanstack/query-core and pbkit-zod declares zod. Without it, npm install @karnak19/pbkit-realtime won't warn users if pocketbase is missing from their project. While pocketbase is typically already present (required by the core pbkit client), explicit declaration is the correct package hygiene.

Fix: Add a peerDependencies block consistent with the sibling packages:

"peerDependencies": {
  "pocketbase": ">=0.21.0"
}
``` <!-- ai-pr-review:inline -->

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 19adf11. Added "peerDependencies": { "pocketbase": ">=0.21.0" }, matching the pbkit-zod (zod) / pbkit-tanstack (@tanstack/query-core) pattern.

- Generated subscribe helpers now return Promise<() => Promise<void>>,
  matching PocketBase's UnsubscribeFunc (unsubscribe is async). Update
  snapshot, tests, docs, README, and SKILL to await unsub().
- Declare pocketbase as a peerDependency, consistent with pbkit-zod and
  pbkit-tanstack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Both previously raised issues (return type mismatch → Promise<() => Promise<void>>, and missing pocketbase peerDependency) are confirmed fixed in the current diff. The pascalCase utility has been properly extracted to @karnak19/pbkit/src/utils/ and shared across all three plugins. Changeset (.changeset/pbkit-realtime.md) covers both @karnak19/pbkit-realtime (minor) and @karnak19/pbkit (minor). Tests are thorough including edge cases. No new material issues found.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Karnak19
Karnak19 merged commit 4c89d3e into main Jun 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add @karnak19/pbkit-realtime plugin for typed realtime subscriptions

1 participant