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
6,285 changes: 2,815 additions & 3,470 deletions package-lock.json

Large diffs are not rendered by default.

90 changes: 59 additions & 31 deletions packages/feathers/src/client/sse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ describe('SSE client', function () {

let server: any
let app: Application<TestServiceTypes>
let client1: Application<TestServiceTypes>
let client2: Application<TestServiceTypes>

beforeAll(async () => {
app = getApp()
Expand All @@ -32,19 +30,6 @@ describe('SSE client', function () {
})

server = await createTestServer(port, app)

client1 = feathers<TestServiceTypes>().configure(
fetchClient(fetch, {
baseUrl: url,
sse: 'sse'
})
)
client2 = feathers<TestServiceTypes>().configure(
fetchClient(fetch, {
baseUrl: url,
sse: 'sse'
})
)
})

afterAll(async () => {
Expand All @@ -53,17 +38,32 @@ describe('SSE client', function () {

it('should stream basic SSE between clients, can abort sse', async () => {
const events: Todo[] = []
const client1 = feathers<TestServiceTypes>().configure(
fetchClient(fetch, {
baseUrl: url,
sse: 'sse'
})
)

client1.service('sse').emit('start')

const controller = await new Promise<AbortController>((resolve) => {
const connectedPromise = new Promise<AbortController>((resolve) => {
client1.service('sse').once('connected', (data: AbortController) => resolve(data))
})

await client1.setup()

const controller = await connectedPromise

client1.service('todos').on('created', (data: Todo) => {
events.push(data)
})

const client2 = feathers<TestServiceTypes>().configure(
fetchClient(fetch, {
baseUrl: url,
sse: 'sse'
})
)

await client2.service('todos').create({ text: 'todo 1', complete: true })
await Promise.all([
client2.service('todos').create({ text: 'todo 2', complete: false }),
Expand All @@ -82,31 +82,57 @@ describe('SSE client', function () {
})

it('emits AbortController on successful connection', async () => {
const params = {
query: { message: 'testing' }
}

client1.service('sse').emit('start', params)
const client = feathers<TestServiceTypes>().configure(
fetchClient(fetch, {
baseUrl: url,
sse: {
path: 'sse',
params: { query: { message: 'testing' } }
}
})
)

const controller = await new Promise<AbortController>((resolve) => {
client1.service('sse').once('connected', (data: AbortController) => resolve(data))
const connectedPromise = new Promise<AbortController>((resolve) => {
client.service('sse').once('connected', (data: AbortController) => resolve(data))
})

await client.setup()

const controller = await connectedPromise

controller.abort()
expect(controller.signal.aborted).toBe(true)
})

it('only receive events for their channels', async () => {
const events: Todo[] = []
const client1 = feathers<TestServiceTypes>().configure(
fetchClient(fetch, {
baseUrl: url,
sse: {
path: 'sse',
params: { query: { channel: 'client' } }
}
})
)
const client2 = feathers<TestServiceTypes>().configure(
fetchClient(fetch, {
baseUrl: url,
sse: {
path: 'sse',
params: { query: { channel: 'client' } }
}
})
)

client1.service('sse').emit('start', { query: { channel: 'client' } })
client2.service('sse').emit('start', { query: { channel: 'client' } })

await Promise.all([
const connected = Promise.all([
new Promise((resolve) => client1.service('sse').once('connected', resolve)),
new Promise((resolve) => client2.service('sse').once('connected', resolve))
])

await Promise.all([client1.setup(), client2.setup()])
await connected

client1.service('todos').on('created', (todo: Todo) => events.push(todo))
client2.service('todos').on('created', (todo: Todo) => events.push(todo))

Expand Down Expand Up @@ -144,12 +170,14 @@ describe('SSE client', function () {
}
})
)
reconnectClient.service('sse').emit('start')

await new Promise<AbortController>((resolve) => {
const connectedPromise = new Promise<AbortController>((resolve) => {
reconnectClient.service('sse').once('connected', (data: AbortController) => resolve(data))
})

await reconnectClient.setup()
await connectedPromise

const disconnectEvent = new Promise<Error>((resolve) => {
reconnectClient.service('sse').once('disconnected', (error: Error) => resolve(error))
})
Expand Down
19 changes: 15 additions & 4 deletions packages/feathers/src/client/sse.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Application, Params } from '../declarations.js'
import { Application, HookContext, NextFunction, Params } from '../declarations.js'

export interface SseClientOptions {
path: string
reconnectionDelay?: number
reconnectionDelayMax?: number
params?: Params
}

export interface ReconnectingEvent {
Expand All @@ -21,7 +22,12 @@ function getDelay(attempt: number, reconnectionDelay: number, reconnectionDelayM

export function sseClient(options: SseClientOptions) {
return (client: Application) => {
const { path, reconnectionDelay = 1000, reconnectionDelayMax = 5000 } = options
const {
path,
reconnectionDelay = 1000,
reconnectionDelayMax = 5000,
params: defaultParams = {}
} = options
const sseService = client.service(path)

let attempt = 0
Expand Down Expand Up @@ -94,8 +100,13 @@ export function sseClient(options: SseClientOptions) {
})
}

sseService.on('start', (params: Params = {}) => {
connect(params)
client.hooks({
setup: [
async (_context: HookContext, next: NextFunction) => {
await next()
connect(defaultParams)
}
]
})
}
}
2 changes: 1 addition & 1 deletion website/.nuxtrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
setups.@nuxt/test-utils="3.23.0"
setups.@nuxt/test-utils="4.0.0"
4 changes: 2 additions & 2 deletions website/app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ useSeoMeta({
<!-- Global Search Modal -->
<DocsSearchModal
v-model="isSearchOpen"
:collections="['guides', 'api', 'cookbook', 'help', 'ecosystem']"
:collections="['guides', 'api', 'help']"
search-label="Feathers Docs"
:popular-paths="['/guides', '/api', '/cookbook', '/help']"
:popular-paths="['/guides', '/api', '/help']"
/>
</template>
6 changes: 0 additions & 6 deletions website/app/components/DocsSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const sidebarRef = ref<HTMLElement | null>(null)
function getMenuName(path: string) {
if (path.startsWith('/guides')) return 'guides'
if (path.startsWith('/api')) return 'api'
if (path.startsWith('/cookbook')) return 'cookbook'
if (path.startsWith('/help')) return 'help'
return 'guides' // default
}
Expand All @@ -21,9 +20,6 @@ const { data: guidesMenu } = await useAsyncData('menu-guides', () =>
const { data: apiMenu } = await useAsyncData('menu-api', () =>
queryCollection('menus').where('stem', '==', 'menus/api').first()
)
const { data: cookbookMenu } = await useAsyncData('menu-cookbook', () =>
queryCollection('menus').where('stem', '==', 'menus/cookbook').first()
)
const { data: helpMenu } = await useAsyncData('menu-help', () =>
queryCollection('menus').where('stem', '==', 'menus/help').first()
)
Expand All @@ -32,8 +28,6 @@ const currentMenu = computed(() => {
switch (menuName.value) {
case 'api':
return apiMenu.value
case 'cookbook':
return cookbookMenu.value
case 'help':
return helpMenu.value
default:
Expand Down
2 changes: 1 addition & 1 deletion website/app/components/Features.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
<Text class="opacity-70">
Built for TypeScript, Feathers provides the structure to create complex applications but is
flexible enough to not be in the way. With
<Link is="NuxtLink" to="/ecosystem/">a large ecosystem of plugins</Link> you can include exactly
a large ecosystem of plugins you can include exactly
what you need. No more, no less.
</Text>
</CardBody>
Expand Down
1 change: 0 additions & 1 deletion website/app/components/TopNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const { openSearch } = useGlobalSearch()
<Flex row items-center class="gap-6 rounded-box bg-base-100/10 p-3 sm:px-12">
<NuxtLink to="/guides">Guides</NuxtLink>
<NuxtLink to="/api">API</NuxtLink>
<NuxtLink to="/cookbook">Cookbook</NuxtLink>
<NuxtLink to="/help">Help</NuxtLink>
</Flex>
</NavbarCenter>
Expand Down
26 changes: 0 additions & 26 deletions website/app/pages/cookbook/[...slug].vue

This file was deleted.

8 changes: 0 additions & 8 deletions website/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,9 @@ export default defineContentConfig({
type: 'page',
source: 'api/**/*.md'
}),
cookbook: defineCollection({
type: 'page',
source: 'cookbook/**/*.md'
}),
help: defineCollection({
type: 'page',
source: 'help/**/*.md'
}),
ecosystem: defineCollection({
type: 'page',
source: 'ecosystem/**/*.md'
})
}
})
4 changes: 2 additions & 2 deletions website/content/api/application.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ app.configure(setupService)
## .setup([server])

`app.setup([server]) -> Promise<app>` is used to initialize all services by calling each [services .setup(app, path)](services#setupapp-path) method (if available).
It will also use the `server` instance passed (e.g. through `http.createServer`) to set up SocketIO (if enabled) and any other provider that might require the server instance. You can register [application setup hooks](./hooks#setup-and-teardown) to e.g. set up database connections and other things required to be initialized on startup in a certain order.
It will also use the `server` instance passed (e.g. through `http.createServer`) to set up any provider that might require the server instance. You can register [application setup hooks](./hooks#setup-and-teardown) to e.g. set up database connections and other things required to be initialized on startup in a certain order.

Normally `app.setup` will be called automatically when starting the application via [app.listen([port])](#listen-port) but there are cases (like in tests) when it can be called explicitly.

Expand All @@ -183,7 +183,7 @@ Normally `app.setup` will be called automatically when starting the application

`app.listen([port]) -> Promise<HTTPServer>` starts the application on the given port. It will set up all configured transports (if any) and then run [app.setup(server)](#setup-server) with the server object and then return the server object.

`listen` will only be available if a server side transport (REST or websocket) has been configured.
`listen` will only be available if a server side transport (HTTP) has been configured.

## .set(name, value)

Expand Down
14 changes: 7 additions & 7 deletions website/content/api/bun.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,14 @@ Bun.serve({

The `createHandler` returns a function with the signature `(request: Request) => Promise<Response>` which is the Web Standard used natively by Bun.

## With Socket.io
## With SSE

To use real-time functionality with Socket.io in Bun:
To use real-time functionality with Server-Sent Events in Bun, register the [SSE service](./http#sse-service) and set up [channels](./channels):

```ts
import { feathers } from 'feathers'
import { createHandler } from 'feathers/http'
import { Server } from 'socket.io'
import { socketio } from 'feathers/socketio'
import { SseService } from 'feathers/sse'

const app = feathers()

Expand All @@ -46,14 +45,15 @@ app.use('messages', {
}
})

app.configure(socketio())
// Register the SSE service for real-time events
app.use('sse', new SseService())

const handler = createHandler(app)

const server = Bun.serve({
Bun.serve({
port: 3030,
fetch: handler
})

await app.setup(server)
await app.setup()
```
4 changes: 2 additions & 2 deletions website/content/api/channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Channels

On a Feathers server with a real-time transport (like [Socket.io](./socketio)) configured, event channels determine which connected clients to send [real-time events](./events) to and how the sent data should look.
On a Feathers server with a real-time transport (like [SSE](./client/sse)) configured, event channels determine which connected clients to send [real-time events](./events) to and how the sent data should look.

This chapter describes:

Expand Down Expand Up @@ -86,7 +86,7 @@ export default function (app: any) {

## Connections

A connection is an object that represents a real-time connection. It is the same object as `socket.feathers` in a [Socket.io](./socketio#params) middleware. You can add any kind of information to it but most notably, when using [authentication](./authentication/service), it will contain the authenticated user. By default it is located in `connection.user` once the client has authenticated on the socket (usually by calling `app.authenticate()` on the [client](./client)).
A connection is an object that represents a real-time connection. You can add any kind of information to it but most notably, when using [authentication](./authentication/service), it will contain the authenticated user. By default it is located in `connection.user` once the client has authenticated (usually by calling `app.authenticate()` on the [client](./client)).

We can get access to the `connection` object by listening to `app.on('connection', connection => {})` or `app.on('login', (payload, { connection }) => {})`.

Expand Down
Loading
Loading