Skip to content

Add broadcast subscription contract to fs-adapter-store#35

Merged
Goosterhof merged 3 commits into
mainfrom
feat/adapter-store-apply-external-updates
Apr 19, 2026
Merged

Add broadcast subscription contract to fs-adapter-store#35
Goosterhof merged 3 commits into
mainfrom
feat/adapter-store-apply-external-updates

Conversation

@jasperboerhof
Copy link
Copy Markdown
Contributor

@jasperboerhof jasperboerhof commented Apr 19, 2026

Summary

  • Adds an optional broadcast slot on AdapterStoreConfig accepting an AdapterStoreBroadcast<T> contract.
  • At store construction, the store calls broadcast.subscribe({ onUpdate, onDelete }) once; incoming events flow straight into the internal setById / deleteById.
  • Public store API gains zero new mutation methods.

Why not expose applyServerUpdate / applyServerDelete directly

Earlier iterations of this branch exposed those as public methods. Gerard flagged that this would inevitably get misused as a general HTTP-bypass shortcut — intent-revealing naming is a soft guardrail, not a hard one.

This design keeps the mutation path fully encapsulated: the only way in is via a broadcast source wired at construction. The handlers are never exposed on the returned store object.

How consuming apps use it

// domain-level broadcast binder (implements AdapterStoreBroadcast<Comment>)
const commentBroadcast: AdapterStoreBroadcast<Comment> = {
  subscribe: ({ onUpdate, onDelete }) => {
    emitter.on('comment.updated', onUpdate);
    emitter.on('comment.deleted', onDelete);
    return () => {
      emitter.off('comment.updated', onUpdate);
      emitter.off('comment.deleted', onDelete);
    };
  },
};

const commentStore = createAdapterStoreModule({
  domainName: 'comments',
  adapter: createCommentAdapter,
  httpService, storageService, loadingService,
  broadcast: commentBroadcast,
});

// Vue component — channel lifecycle only
onMounted(() => joinProjectChannel(projectId, {
  CommentUpdated: (p) => emitter.emit('comment.updated', p),
  CommentDeleted: (p) => emitter.emit('comment.deleted', p.id),
}));
onUnmounted(() => leaveProjectChannel(projectId));

Channel join/leave lifecycle stays in the component. The store just listens on whatever source the broadcast contract wraps.

Version bump

0.1.2 → 0.1.3 — additive, non-breaking config option.

Test plan

  • npm run build
  • npm run typecheck
  • npm test — 90/90 pass
  • npm run test:coverage — 100%
  • npx stryker run (adapter-store) — 100% mutation score
  • npx oxfmt --check .
  • npm run lint
  • npm run lint:pkg

🤖 Generated with Claude Code

Exposes setById/deleteById as intent-revealing public methods so that
server-initiated events (e.g. WebSocket broadcasts from another client)
can update the store without an HTTP refetch. The naming is deliberate:
these are not general-purpose mutation hooks — they signal that the
source of truth is an external system emitting authoritative state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 19, 2026

Deploying fs-packages with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1148707
Status: ✅  Deploy successful!
Preview URL: https://cc1cb8ce.fs-packages.pages.dev
Branch Preview URL: https://feat-adapter-store-apply-ext.fs-packages.pages.dev

View logs

Addresses review feedback: exposing applyServerUpdate/applyServerDelete
as public store methods leaked a mutation surface that invited misuse
as an HTTP-bypass shortcut.

Drops both public methods. Adds an optional `broadcast` config slot
accepting an AdapterStoreBroadcast<T> contract. The store calls
`broadcast.subscribe({ onUpdate, onDelete })` exactly once at
construction, routing incoming events directly into the internal
setById/deleteById. Consumers never see the mutation handlers — they
can only bind a broadcast source at store creation time.

Channel lifecycle (join/leave per component) stays in the consuming
app; the broadcast contract is the narrow bridge between that layer
and the store.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jasperboerhof jasperboerhof changed the title Add applyServerUpdate/applyServerDelete to fs-adapter-store Add broadcast subscription contract to fs-adapter-store Apr 19, 2026
Adds a "Syncing External Updates" section covering the AdapterStoreBroadcast
contract — what it's for, the subscribe/unsubscribe shape, the rationale
for not exposing raw mutation methods, and the lifecycle split between
store (one-shot subscribe) and transport layer (connection join/leave).

Also backfills the retrieveById row in the Store Module Methods table,
which was missed when that method was added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Goosterhof Goosterhof merged commit 220a6fa into main Apr 19, 2026
2 checks passed
@Goosterhof Goosterhof deleted the feat/adapter-store-apply-external-updates branch April 19, 2026 14:14
Goosterhof added a commit that referenced this pull request Apr 23, 2026
…fix-sweep

fs-packages: re-normalize 10 publint git+ prefix regressions (PR #35 aftermath)
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.

2 participants