Skip to content

Commit 714792d

Browse files
authored
fix: Improve SSE handling and add documentation (#3672)
1 parent ecbca62 commit 714792d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3208
-5925
lines changed

package-lock.json

Lines changed: 2815 additions & 3470 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/feathers/src/client/sse.test.ts

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ describe('SSE client', function () {
99

1010
let server: any
1111
let app: Application<TestServiceTypes>
12-
let client1: Application<TestServiceTypes>
13-
let client2: Application<TestServiceTypes>
1412

1513
beforeAll(async () => {
1614
app = getApp()
@@ -32,19 +30,6 @@ describe('SSE client', function () {
3230
})
3331

3432
server = await createTestServer(port, app)
35-
36-
client1 = feathers<TestServiceTypes>().configure(
37-
fetchClient(fetch, {
38-
baseUrl: url,
39-
sse: 'sse'
40-
})
41-
)
42-
client2 = feathers<TestServiceTypes>().configure(
43-
fetchClient(fetch, {
44-
baseUrl: url,
45-
sse: 'sse'
46-
})
47-
)
4833
})
4934

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

5439
it('should stream basic SSE between clients, can abort sse', async () => {
5540
const events: Todo[] = []
41+
const client1 = feathers<TestServiceTypes>().configure(
42+
fetchClient(fetch, {
43+
baseUrl: url,
44+
sse: 'sse'
45+
})
46+
)
5647

57-
client1.service('sse').emit('start')
58-
59-
const controller = await new Promise<AbortController>((resolve) => {
48+
const connectedPromise = new Promise<AbortController>((resolve) => {
6049
client1.service('sse').once('connected', (data: AbortController) => resolve(data))
6150
})
6251

52+
await client1.setup()
53+
54+
const controller = await connectedPromise
55+
6356
client1.service('todos').on('created', (data: Todo) => {
6457
events.push(data)
6558
})
6659

60+
const client2 = feathers<TestServiceTypes>().configure(
61+
fetchClient(fetch, {
62+
baseUrl: url,
63+
sse: 'sse'
64+
})
65+
)
66+
6767
await client2.service('todos').create({ text: 'todo 1', complete: true })
6868
await Promise.all([
6969
client2.service('todos').create({ text: 'todo 2', complete: false }),
@@ -82,31 +82,57 @@ describe('SSE client', function () {
8282
})
8383

8484
it('emits AbortController on successful connection', async () => {
85-
const params = {
86-
query: { message: 'testing' }
87-
}
88-
89-
client1.service('sse').emit('start', params)
85+
const client = feathers<TestServiceTypes>().configure(
86+
fetchClient(fetch, {
87+
baseUrl: url,
88+
sse: {
89+
path: 'sse',
90+
params: { query: { message: 'testing' } }
91+
}
92+
})
93+
)
9094

91-
const controller = await new Promise<AbortController>((resolve) => {
92-
client1.service('sse').once('connected', (data: AbortController) => resolve(data))
95+
const connectedPromise = new Promise<AbortController>((resolve) => {
96+
client.service('sse').once('connected', (data: AbortController) => resolve(data))
9397
})
9498

99+
await client.setup()
100+
101+
const controller = await connectedPromise
102+
95103
controller.abort()
96104
expect(controller.signal.aborted).toBe(true)
97105
})
98106

99107
it('only receive events for their channels', async () => {
100108
const events: Todo[] = []
109+
const client1 = feathers<TestServiceTypes>().configure(
110+
fetchClient(fetch, {
111+
baseUrl: url,
112+
sse: {
113+
path: 'sse',
114+
params: { query: { channel: 'client' } }
115+
}
116+
})
117+
)
118+
const client2 = feathers<TestServiceTypes>().configure(
119+
fetchClient(fetch, {
120+
baseUrl: url,
121+
sse: {
122+
path: 'sse',
123+
params: { query: { channel: 'client' } }
124+
}
125+
})
126+
)
101127

102-
client1.service('sse').emit('start', { query: { channel: 'client' } })
103-
client2.service('sse').emit('start', { query: { channel: 'client' } })
104-
105-
await Promise.all([
128+
const connected = Promise.all([
106129
new Promise((resolve) => client1.service('sse').once('connected', resolve)),
107130
new Promise((resolve) => client2.service('sse').once('connected', resolve))
108131
])
109132

133+
await Promise.all([client1.setup(), client2.setup()])
134+
await connected
135+
110136
client1.service('todos').on('created', (todo: Todo) => events.push(todo))
111137
client2.service('todos').on('created', (todo: Todo) => events.push(todo))
112138

@@ -144,12 +170,14 @@ describe('SSE client', function () {
144170
}
145171
})
146172
)
147-
reconnectClient.service('sse').emit('start')
148173

149-
await new Promise<AbortController>((resolve) => {
174+
const connectedPromise = new Promise<AbortController>((resolve) => {
150175
reconnectClient.service('sse').once('connected', (data: AbortController) => resolve(data))
151176
})
152177

178+
await reconnectClient.setup()
179+
await connectedPromise
180+
153181
const disconnectEvent = new Promise<Error>((resolve) => {
154182
reconnectClient.service('sse').once('disconnected', (error: Error) => resolve(error))
155183
})

packages/feathers/src/client/sse.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { Application, Params } from '../declarations.js'
1+
import { Application, HookContext, NextFunction, Params } from '../declarations.js'
22

33
export interface SseClientOptions {
44
path: string
55
reconnectionDelay?: number
66
reconnectionDelayMax?: number
7+
params?: Params
78
}
89

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

2223
export function sseClient(options: SseClientOptions) {
2324
return (client: Application) => {
24-
const { path, reconnectionDelay = 1000, reconnectionDelayMax = 5000 } = options
25+
const {
26+
path,
27+
reconnectionDelay = 1000,
28+
reconnectionDelayMax = 5000,
29+
params: defaultParams = {}
30+
} = options
2531
const sseService = client.service(path)
2632

2733
let attempt = 0
@@ -94,8 +100,13 @@ export function sseClient(options: SseClientOptions) {
94100
})
95101
}
96102

97-
sseService.on('start', (params: Params = {}) => {
98-
connect(params)
103+
client.hooks({
104+
setup: [
105+
async (_context: HookContext, next: NextFunction) => {
106+
await next()
107+
connect(defaultParams)
108+
}
109+
]
99110
})
100111
}
101112
}

website/.nuxtrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
setups.@nuxt/test-utils="3.23.0"
1+
setups.@nuxt/test-utils="4.0.0"

website/app/app.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ useSeoMeta({
6666
<!-- Global Search Modal -->
6767
<DocsSearchModal
6868
v-model="isSearchOpen"
69-
:collections="['guides', 'api', 'cookbook', 'help', 'ecosystem']"
69+
:collections="['guides', 'api', 'help']"
7070
search-label="Feathers Docs"
71-
:popular-paths="['/guides', '/api', '/cookbook', '/help']"
71+
:popular-paths="['/guides', '/api', '/help']"
7272
/>
7373
</template>

website/app/components/DocsSidebar.vue

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ const sidebarRef = ref<HTMLElement | null>(null)
77
function getMenuName(path: string) {
88
if (path.startsWith('/guides')) return 'guides'
99
if (path.startsWith('/api')) return 'api'
10-
if (path.startsWith('/cookbook')) return 'cookbook'
1110
if (path.startsWith('/help')) return 'help'
1211
return 'guides' // default
1312
}
@@ -21,9 +20,6 @@ const { data: guidesMenu } = await useAsyncData('menu-guides', () =>
2120
const { data: apiMenu } = await useAsyncData('menu-api', () =>
2221
queryCollection('menus').where('stem', '==', 'menus/api').first()
2322
)
24-
const { data: cookbookMenu } = await useAsyncData('menu-cookbook', () =>
25-
queryCollection('menus').where('stem', '==', 'menus/cookbook').first()
26-
)
2723
const { data: helpMenu } = await useAsyncData('menu-help', () =>
2824
queryCollection('menus').where('stem', '==', 'menus/help').first()
2925
)
@@ -32,8 +28,6 @@ const currentMenu = computed(() => {
3228
switch (menuName.value) {
3329
case 'api':
3430
return apiMenu.value
35-
case 'cookbook':
36-
return cookbookMenu.value
3731
case 'help':
3832
return helpMenu.value
3933
default:

website/app/components/Features.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
<Text class="opacity-70">
4444
Built for TypeScript, Feathers provides the structure to create complex applications but is
4545
flexible enough to not be in the way. With
46-
<Link is="NuxtLink" to="/ecosystem/">a large ecosystem of plugins</Link> you can include exactly
46+
a large ecosystem of plugins you can include exactly
4747
what you need. No more, no less.
4848
</Text>
4949
</CardBody>

website/app/components/TopNav.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ const { openSearch } = useGlobalSearch()
1515
<Flex row items-center class="gap-6 rounded-box bg-base-100/10 p-3 sm:px-12">
1616
<NuxtLink to="/guides">Guides</NuxtLink>
1717
<NuxtLink to="/api">API</NuxtLink>
18-
<NuxtLink to="/cookbook">Cookbook</NuxtLink>
1918
<NuxtLink to="/help">Help</NuxtLink>
2019
</Flex>
2120
</NavbarCenter>

website/app/pages/cookbook/[...slug].vue

Lines changed: 0 additions & 26 deletions
This file was deleted.

website/content.config.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,9 @@ export default defineContentConfig({
2323
type: 'page',
2424
source: 'api/**/*.md'
2525
}),
26-
cookbook: defineCollection({
27-
type: 'page',
28-
source: 'cookbook/**/*.md'
29-
}),
3026
help: defineCollection({
3127
type: 'page',
3228
source: 'help/**/*.md'
3329
}),
34-
ecosystem: defineCollection({
35-
type: 'page',
36-
source: 'ecosystem/**/*.md'
37-
})
3830
}
3931
})

0 commit comments

Comments
 (0)