Skip to content

Commit 1148707

Browse files
jasperboerhofclaude
andcommitted
Document broadcast config and fill in missing retrieveById row
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>
1 parent a3dfb12 commit 1148707

1 file changed

Lines changed: 69 additions & 14 deletions

File tree

docs/packages/adapter-store.md

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,59 @@ try {
181181

182182
The store automatically persists state to the provided storage service. When the page reloads, stored data is available immediately while `retrieveAll()` fetches fresh data from the API. This provides a fast initial render without loading spinners.
183183

184+
## Syncing External Updates
185+
186+
Some resources are updated outside of the store's own CRUD calls — by another user over a WebSocket, by a background job, by an in-process event emitter. The `broadcast` config slot is the single, narrow bridge for feeding those updates into the store without going through HTTP.
187+
188+
```typescript
189+
import type { AdapterStoreBroadcast } from "@script-development/fs-adapter-store";
190+
191+
const broadcast: AdapterStoreBroadcast<User> = {
192+
subscribe: ({ onUpdate, onDelete }) => {
193+
eventSource.on("user.updated", onUpdate);
194+
eventSource.on("user.deleted", onDelete);
195+
return () => {
196+
eventSource.off("user.updated", onUpdate);
197+
eventSource.off("user.deleted", onDelete);
198+
};
199+
},
200+
};
201+
202+
const usersStore = createAdapterStoreModule<User>({
203+
domainName: "users",
204+
adapter: resourceAdapter,
205+
httpService: http,
206+
storageService: storage,
207+
loadingService: loading,
208+
broadcast,
209+
});
210+
```
211+
212+
The store calls `subscribe` exactly once at construction and wires the handlers straight into its internal mutation path. `onUpdate(item)` replaces or inserts; `onDelete(id)` removes. Both update reactive state, refresh adapted views, and persist to storage — identical to what `update()` / `delete()` do after a successful HTTP call.
213+
214+
::: tip Why isn't there a public `setById` / `applyUpdate` method?
215+
By design. Exposing a raw mutation method would let any caller bypass HTTP, which is almost always a bug (you'd end up with stale server state). The `broadcast` contract forces the bridge to be declared explicitly at store construction, scoped to one event source per store.
216+
:::
217+
218+
### Lifecycle
219+
220+
The `subscribe` call happens once, when the store is created. The unsubscribe return is retained internally and never exposed. In practice stores live for the app's lifetime, so teardown isn't needed — but if your event source has its own lifecycle (e.g., a channel you join and leave), manage that _outside_ the store. The store only cares about incoming events, not which channel they came from.
221+
222+
A common pattern is a small in-process emitter as a middleman: your transport layer (WebSocket, SSE, channel service, whatever) joins and leaves connections as views mount/unmount, and forwards incoming payloads onto an emitter that the store subscribes to. The store stays agnostic of transport and lifecycle.
223+
224+
### The Contract
225+
226+
```typescript
227+
type AdapterStoreBroadcast<T> = {
228+
subscribe: (handlers: {
229+
onUpdate: (item: T) => void;
230+
onDelete: (id: number) => void;
231+
}) => () => void; // unsubscribe
232+
};
233+
```
234+
235+
That's it. Any event source that can emit "updated" and "deleted" events for your resource type can implement this.
236+
184237
## Custom New Types
185238

186239
By default, `generateNew()` creates an object with all fields except `id`. You can customize this with a third type parameter:
@@ -219,23 +272,25 @@ import { EntryNotFoundError, MissingResponseDataError } from "@script-developmen
219272

220273
### `createAdapterStoreModule(config)`
221274

222-
| Parameter | Type | Description |
223-
| ----------------------- | ----------------------------------------------- | -------------------------------------------- |
224-
| `config.domainName` | `string` | Resource endpoint name (e.g., `"users"`) |
225-
| `config.adapter` | `Adapter` | CRUD adapter factory (use `resourceAdapter`) |
226-
| `config.httpService` | `Pick<HttpService, "getRequest">` | HTTP service for fetching |
227-
| `config.storageService` | `Pick<StorageService, "get" \| "put">` | Storage for persistence |
228-
| `config.loadingService` | `Pick<LoadingService, "ensureLoadingFinished">` | Loading service for sync |
275+
| Parameter | Type | Description |
276+
| ----------------------- | ----------------------------------------------- | ----------------------------------------------------------- |
277+
| `config.domainName` | `string` | Resource endpoint name (e.g., `"users"`) |
278+
| `config.adapter` | `Adapter` | CRUD adapter factory (use `resourceAdapter`) |
279+
| `config.httpService` | `Pick<HttpService, "getRequest">` | HTTP service for fetching |
280+
| `config.storageService` | `Pick<StorageService, "get" \| "put">` | Storage for persistence |
281+
| `config.loadingService` | `Pick<LoadingService, "ensureLoadingFinished">` | Loading service for sync |
282+
| `config.broadcast?` | `AdapterStoreBroadcast<T>` | Optional external-event bridge for server-initiated updates |
229283

230284
### Store Module Methods
231285

232-
| Method | Returns | Description |
233-
| ------------------- | ----------------------------------- | -------------------------------------- |
234-
| `getAll` | `ComputedRef<Adapted[]>` | Reactive list of all adapted resources |
235-
| `getById(id)` | `ComputedRef<Adapted \| undefined>` | Reactive lookup by ID |
236-
| `getOrFailById(id)` | `Promise<Adapted>` | Wait for loading, throw if not found |
237-
| `generateNew()` | `NewAdapted` | Create a new unsaved resource |
238-
| `retrieveAll()` | `Promise<void>` | Fetch all from API and update state |
286+
| Method | Returns | Description |
287+
| ------------------- | ----------------------------------- | ------------------------------------------ |
288+
| `getAll` | `ComputedRef<Adapted[]>` | Reactive list of all adapted resources |
289+
| `getById(id)` | `ComputedRef<Adapted \| undefined>` | Reactive lookup by ID |
290+
| `getOrFailById(id)` | `Promise<Adapted>` | Wait for loading, throw if not found |
291+
| `generateNew()` | `NewAdapted` | Create a new unsaved resource |
292+
| `retrieveById(id)` | `Promise<void>` | Fetch a single resource from the API by id |
293+
| `retrieveAll()` | `Promise<void>` | Fetch all from API and update state |
239294

240295
### Adapted Properties
241296

0 commit comments

Comments
 (0)