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
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added
- `SSEAdapter` interface and `AdapterMapping` type for building database/event source adapters (`reactive-swr/server`) (#WI-244)
- `channel.watch(adapter)` method to connect adapters to SSE channels with idempotent cleanup (#WI-246)
- `createPrismaAdapter(prisma, mapping)` adapter for automatic SSE emission from Prisma `$use()` middleware (#WI-247)
- `createMongoAdapter(collection, mapping)` adapter for MongoDB Change Streams with resume token support (#WI-248)
- `createPgAdapter(client, mapping)` adapter for PostgreSQL LISTEN/NOTIFY with SQL identifier quoting (#WI-249)
- `createEmitterAdapter(emitter, mapping)` adapter for bridging any `on`/`off`-compatible event emitter (#WI-250)
- Schema `resources` field in `defineSchema()` that auto-expands CRUD event triplets (`<resource>.created`, `<resource>.updated`, `<resource>.deleted`) (#WI-245)
- `ResourceDefinition` and `ResourceOperationDefinition` types for schema resource definitions (#WI-245)
- Subpath exports for individual adapters: `reactive-swr/server/adapters/prisma`, `reactive-swr/server/adapters/mongodb`, `reactive-swr/server/adapters/pg`, `reactive-swr/server/adapters/emitter` (#WI-251)
- Barrel export file for all adapters at `src/server/adapters/index.ts` (#WI-251)

### Fixed
- `SchemaResult` type now includes resource-expanded event keys (`.created`/`.updated`/`.deleted`) for proper TypeScript inference
- `defineSchema()` logs a `console.warn` when an explicit event overrides a resource-generated event
- MongoDB adapter `start()` is now properly async (awaits stream setup instead of fire-and-forget)
- MongoDB reconnect counter only resets on fresh `start()`, not on every event emission
- PostgreSQL adapter sets `started = true` after LISTEN queries succeed, not before
- PostgreSQL adapter always quotes identifiers to handle reserved keywords like `select`
- PostgreSQL adapter warns when client lacks `off()`/`removeListener()` for cleanup
- Prisma adapter propagates `$use()` registration errors and resets state on failure
- EventEmitter adapter defers `started = true` until all listeners are registered; `off()` calls wrapped in try/catch
- EventEmitter adapter resets `started = false` in `stop()` to allow restart
- `channel.watch()` clears stopped state on re-watch so the same adapter can be reused
- Channel error messages now include operation context (e.g., "Cannot connect: channel is closed")

### Changed
- `defineSchema()` now accepts an optional `resources` key alongside explicit event definitions; explicit definitions take precedence over generated resource events (#WI-245)
- `channel.close()` now stops all watched adapters in addition to closing client connections (#WI-246)
- Build script updated to compile individual adapter entry points for tree-shakeable imports (#WI-251)
- `tsconfig.emit.json` updated to include adapter source files for declaration generation (#WI-251)

## [0.1.0] - 2026-02-22

### Added
Expand Down
140 changes: 140 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,32 @@ Each event definition supports:
| `filter` | `(payload) => boolean` | Optional client-side filter |
| `transform` | `(payload) => payload` | Optional client-side transform |

#### Schema Resources

For CRUD-heavy applications, define resources instead of individual events. Each resource automatically expands into `.created`, `.updated`, and `.deleted` event definitions:

```typescript
const schema = defineSchema({
resources: {
orders: {
created: { key: '/api/orders', update: 'refetch' },
updated: { key: (p: { id: string }) => `/api/orders/${p.id}`, update: 'set' },
deleted: { key: '/api/orders', update: 'refetch' },
},
users: {
updated: { key: (p: { id: string }) => `/api/users/${p.id}` },
},
},
// Explicit events can coexist with resources
'notification.sent': {
key: '/api/notifications',
update: 'refetch',
},
})
```

This generates `orders.created`, `orders.updated`, `orders.deleted`, `users.created`, `users.updated`, `users.deleted`, and `notification.sent` as event types. Omitted operations (e.g., `users.created`) still get a default definition using the resource name as the key and `'set'` as the update strategy. Explicit event definitions always take precedence over generated resource events.

#### `createChannel()` (Server)

`createChannel()` provides a complete server-side SSE endpoint. It handles wire formatting, heartbeats, connection tracking, and cleanup. Import it from `reactive-swr/server`.
Expand Down Expand Up @@ -203,6 +229,120 @@ export async function POST(request: Request) {
channel.close() // Closes all connections, stops heartbeats
```

#### Database Adapters

Adapters bridge database change notifications to `channel.emit()`, completing the reactive pipeline: **data change -> adapter -> channel.emit() -> SSE -> SWR cache update**. Each adapter accepts a pre-configured client instance -- reactiveSWR never imports a database driver directly.

Connect an adapter with `channel.watch()`:

```typescript
// Sync adapters (Prisma, EventEmitter) return a cleanup function directly:
const cleanup = channel.watch(adapter)

// Async adapters (MongoDB, PostgreSQL) return a Promise<cleanup>:
const cleanup = await channel.watch(adapter)

// Later: cleanup() to stop the adapter
```

`channel.watch()` returns a cleanup function (or `Promise<cleanup>` for async adapters) that calls `adapter.stop()`. When `channel.close()` is called, all watched adapters are stopped automatically.

**Prisma adapter** -- intercepts create/update/delete via `$use()` middleware:

```typescript
import { createPrismaAdapter } from 'reactive-swr/server/adapters/prisma'

const adapter = createPrismaAdapter(prisma, {
Order: {
created: 'orders.created',
updated: 'orders.updated',
deleted: 'orders.deleted',
},
User: {
updated: 'users.updated',
},
})

channel.watch(adapter) // sync — Prisma uses $use() middleware, no async setup
```

Note: the Prisma adapter uses `$use()` middleware, which works with Prisma v4-v6. Prisma v7 removed `$use()` in favor of `$extends()` — a migration to Client Extensions is tracked in [#4](https://github.com/queso/reactiveSWR/issues/4). Register the adapter last to ensure it runs after other middleware.

**MongoDB adapter** -- listens to Change Streams with resume token support:

```typescript
import { createMongoAdapter } from 'reactive-swr/server/adapters/mongodb'

const adapter = createMongoAdapter(db.collection('orders'), {
insert: 'orders.created',
update: 'orders.updated',
replace: 'orders.updated',
delete: 'orders.deleted',
})

await channel.watch(adapter) // async — opens Change Stream
```

The adapter handles `invalidate` events by reopening the stream and persists resume tokens so reconnections pick up where they left off.

**PostgreSQL adapter** -- maps LISTEN/NOTIFY channels to SSE events:

```typescript
import { createPgAdapter } from 'reactive-swr/server/adapters/pg'

const adapter = createPgAdapter(pgClient, {
order_changes: 'orders.updated',
user_changes: 'users.updated',
})

await channel.watch(adapter) // async — issues LISTEN queries
```

NOTIFY payloads are parsed as JSON automatically. Note the PostgreSQL NOTIFY payload limit of 8000 bytes.

**EventEmitter adapter** -- bridges any `on`/`off`-compatible event source:

```typescript
import { createEmitterAdapter } from 'reactive-swr/server/adapters/emitter'

const adapter = createEmitterAdapter(myEventBus, {
'order:changed': 'orders.updated',
'user:changed': 'users.updated',
})

channel.watch(adapter)
```

Works with Node.js `EventEmitter`, Redis pub/sub clients, or any object with `on(event, listener)` and `off(event, listener)` methods.

All adapters are tree-shakeable -- importing one does not pull in the others. They can also be imported from the barrel export at `reactive-swr/server`:

```typescript
import {
createPrismaAdapter,
createMongoAdapter,
createPgAdapter,
createEmitterAdapter,
} from 'reactive-swr/server'
```

For third-party adapter authors, the `SSEAdapter` interface is exported from `reactive-swr/server`:

```typescript
import type { SSEAdapter } from 'reactive-swr/server'

function createMyAdapter(client: MyClient): SSEAdapter {
return {
start(emit) {
// Begin watching for changes, call emit(eventType, payload) when they occur
},
stop() {
// Clean up resources
},
}
}
```

#### SSEProvider `schema` Prop

Pass a schema to `SSEProvider` instead of manually writing `events` mappings:
Expand Down
Loading
Loading