Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,55 @@
# Changelog

## 0.2.0-next.21

- **Does what `0.2.0-next.20` concluded was the real fix: stop replacing the
list.** Adds a live data layer that merges changed rows by id instead of
reloading whole lists from a server snapshot, which is what made rows pop out
and back in.

New `useEntity` hook (`bool-sdk/react`) — one call replaces the hand-wired
`useEffect` + `useState` + `load()` + change-subscription pattern that every
app was rebuilding, and getting subtly wrong:

```tsx
const todos = useEntity("todos", { sort: "-created_at" });
todos.data; todos.loading; todos.error;
await todos.create({ title }); // optimistic, rolls back on failure
await todos.update(id, { done: true });
await todos.remove(id);
```

It owns the four things the hand-rolled version got wrong:

- **Merges by id, never replaces.** A change applies as a delta, so a stale
snapshot can no longer overwrite rows the user already added. This is the
structural fix `next.20` identified; `next.19`'s reload-hold only shrank the
window.
- **Coalesces.** The doorbell trigger fires per row, so a 50-row bulk write
rang it 50 times and cost 50 full-list reloads. Pings inside a 50ms window
now batch into ONE keyed fetch.
- **Orders responses.** Full reloads carry a monotonic sequence; a stale
response landing after a newer one is dropped instead of rewinding the view.
- **Layers optimistic writes over committed state.** An in-flight write is an
overlay, so a concurrent reload can't wipe it, and a failure rolls back by
dropping the overlay.

Reconciling is as cheap as the ding allows: a `DELETE` applies with no fetch at
all; other ops fetch only the changed rows by id through the gateway (so
auth and telemetry still apply); a ding carrying the full row applies with no
fetch. A ding with no id (older platform trigger) degrades to one coalesced
full reload — still better than one reload per ping.

Filtered views stay correct on their own: a row that stops matching leaves the
view, one that starts matching arrives, evaluated client-side against the same
filter DSL `bool.entities` uses.

- `subscribeToChanges` payloads now carry `id` (and optionally `row`), matching
the platform's id-bearing doorbell. `BoolChangePayload` gained both fields;
existing `{table, op}` consumers are unaffected.
- Exports `LiveEntityStore`, `matchesFilter`, and `compareBySort` for non-React
use.

## 0.2.0-next.20

- **Reverts the reload-hold behavior added in `0.2.0-next.19`.** That release
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bool-sdk",
"version": "0.2.0-next.20",
"version": "0.2.0-next.21",
"description": "Client SDK for apps built on Bool — gateway data access, end-user auth, the AI battery, the React auth layer, and the local-dev CLI (link, types, deploy).",
"type": "module",
"main": "./dist/index.js",
Expand Down
14 changes: 13 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,19 @@ export type BoolAuth = {

/** A row-data-free change notification: some row in `table` saw `op`. Refetch
* whatever you derive from that table — the ping never carries the data. */
export type BoolChangePayload = { table?: string; op?: string };
export type BoolChangePayload = {
table?: string;
op?: string;
/** The changed row's id (present since the gateway's id-bearing doorbell;
* older triggers ping without it). Lets subscribers refetch just the changed
* rows instead of re-running their whole query. */
id?: string | null;
/** The full row, when the ding carries it. Today it never does — the public
* doorbell channel is deliberately row-data-free — but the private-channel
* variant (minted realtime token) will ship it, and the live layer already
* applies it directly when present. */
row?: Record<string, unknown>;
};

/** A JSON Schema describing the shape `bool.ai.generate` should return. Passed
* straight to the gateway, which validates the model's output against it. e.g.
Expand Down
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,11 @@ export {
type UpdateManyResult,
type ImportResult,
} from "./entities.js";
export {
LiveEntityStore,
matchesFilter,
compareBySort,
type EntityRow,
type LiveQueryOptions,
type LiveSnapshot,
} from "./live.js";
Loading
Loading