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
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"hoist": true
},
"publish": {
"allowBranch": ["crow", "dove"],
"allowBranch": ["crow", "dove", "v6"],
"message": "chore(release): publish %s",
"conventionalCommits": true,
"createRelease": "github"
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"name": "@feathersjs/feathers",
"name": "feathers",
"private": true,
"homepage": "http://feathersjs.com",
"description": "The API and real-time application framework",
"homepage": "https://feathersjs.com",
"repository": {
"type": "git",
"url": "git://github.com/feathersjs/feathers.git"
Expand Down Expand Up @@ -42,6 +43,7 @@
"clean": "find . -name node_modules -exec rm -rf '{}' + && find . -name package-lock.json -exec rm -rf '{}' +",
"test:deno": "deno test --config deno/tsconfig.json deno/test.ts",
"test": "npm run lint && npm run compile && vitest run --coverage",
"vitest": "vitest run --coverage",
"dev": "vitest"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions packages/feathers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

Feathers is a lightweight web-framework for creating APIs and real-time applications using TypeScript or JavaScript.

Feathers can interact with any backend technology, supports many databases out of the box and works with any frontend technology like React, VueJS, Angular, React Native, Android or iOS.
Feathers supports Node, Deno, Bun and Cloudflareworkers and works with any frontend technology like React, VueJS, Angular, React Native, Android or iOS.

## Getting started

Expand All @@ -26,6 +26,6 @@ To learn more about Feathers visit the website at [feathersjs.com](http://feathe

## License

Copyright (c) 2024 [Feathers contributors](https://github.com/feathersjs/feathers/graphs/contributors)
Copyright (c) 2025 [Feathers contributors](https://github.com/feathersjs/feathers/graphs/contributors)

Licensed under the [MIT license](LICENSE).
30 changes: 21 additions & 9 deletions packages/feathers/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createServer } from 'node:http'
import { TestService } from './fixture.js'

import { feathers, Application, Params } from '../src/index.js'
import { createHandler } from '../src/http/index.js'
import { createHandler, SseService } from '../src/http/index.js'

export * from './client.js'
export * from './rest.js'
Expand All @@ -23,6 +23,7 @@ export class ResponseTestService {
const generator = async function* () {
for (let i = 1; i <= 5; i++) {
yield { message: `Hello ${id} ${i}` }
await new Promise((resolve) => setTimeout(resolve, 50))
}
}

Expand All @@ -41,26 +42,37 @@ export class ResponseTestService {
}
}

export type TestServiceTypes = { todos: TestService; test: ResponseTestService }
export type TestServiceTypes = {
todos: TestService
test: ResponseTestService
sse: SseService
}

export type TestApplication = Application<TestServiceTypes>

export const app: TestApplication = feathers()
export function getApp(): TestApplication {
const app: TestApplication = feathers()

app.use('todos', new TestService(), {
methods: ['find', 'get', 'create', 'update', 'patch', 'remove', 'customMethod']
})
app.use('test', new ResponseTestService())
app.use('todos', new TestService(), {
methods: ['find', 'get', 'create', 'update', 'patch', 'remove', 'customMethod']
})
app.use('test', new ResponseTestService())
app.use('sse', new SseService())

export function createTestServer(port: number) {
return app
}

export async function createTestServer(port: number, app: TestApplication) {
const handler = createHandler(app)
// You can create your Node server instance by using our adapter
const nodeServer = createServer(createServerAdapter(handler))

new Promise<void>((resolve) => {
await new Promise<void>((resolve) => {
// Then start listening on some port
nodeServer.listen(port, () => resolve())
})

await app.setup(nodeServer)

return nodeServer
}
6 changes: 3 additions & 3 deletions packages/feathers/package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "feathers",
"description": "A framework for real-time applications and REST API with JavaScript and TypeScript",
"description": "The API and real-time application framework",
"version": "5.0.34",
"homepage": "http://feathersjs.com",
"homepage": "https://feathersjs.com",
"repository": {
"type": "git",
"url": "git://github.com/feathersjs/feathers.git",
Expand All @@ -23,7 +23,7 @@
"./hooks": "./lib/hooks/index.js",
"./commons": "./lib/commons.js",
"./errors": "./lib/errors.js",
"./client": "./lib/client.js",
"./client": "./lib/client/index.js",
"./http": "./lib/http/index.js"
},
"author": {
Expand Down
75 changes: 74 additions & 1 deletion packages/feathers/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createDebug } from './debug.js'
import version from './version.js'
import { eventHook, eventMixin } from './events.js'
import { hookMixin } from './hooks.js'
import { wrapService, getServiceOptions, protectedMethods } from './service.js'
import { wrapService, getServiceOptions, protectedMethods, defaultServiceEvents } from './service.js'
import type {
FeathersApplication,
ServiceMixin,
Expand All @@ -20,8 +20,12 @@ import type {
} from './declarations.js'
import { enableHooks } from './hooks.js'
import { Router } from './router.js'
import { Channel } from './channel/base.js'
import { CombinedChannel } from './channel/combined.js'
import { channelServiceMixin, Event, Publisher, PUBLISHERS, ALL_EVENTS, CHANNELS } from './channel/mixin.js'

const debug = createDebug('@feathersjs/feathers')
const channelDebug = createDebug('@feathersjs/transport-commons/channels')

export class Feathers<Services, Settings>
extends EventEmitter
Expand All @@ -36,6 +40,10 @@ export class Feathers<Services, Settings>

protected registerHooks: (this: any, allHooks: any) => any

// Channel-related properties
public [CHANNELS]: { [key: string]: Channel } = {}
public [PUBLISHERS]: { [ALL_EVENTS]?: Publisher; [key: string]: Publisher } = {}

constructor() {
super()
this.registerHooks = enableHooks(this)
Expand All @@ -44,6 +52,67 @@ export class Feathers<Services, Settings>
})
}

get channels(): string[] {
return Object.keys(this[CHANNELS])
}

channel(...names: string[]): Channel {
channelDebug('Returning channels', names)

if (names.length === 0) {
throw new Error('app.channel needs at least one channel name')
}

if (names.length === 1) {
const [name] = names

if (Array.isArray(name)) {
return this.channel(...name)
}

if (!this[CHANNELS][name]) {
const channel = new Channel()

channel.once('empty', () => {
channel.removeAllListeners()
delete this[CHANNELS][name]
})

this[CHANNELS][name] = channel
}

return this[CHANNELS][name]
}

const channels = names.map((name) => this.channel(name))

return new CombinedChannel(channels)
}

publish(event: Event | Publisher, publisher?: Publisher): this {
return this.registerPublisher(event, publisher)
}

registerPublisher(event: Event | Publisher, publisher?: Publisher): this {
channelDebug('Registering publisher', event)

if (!publisher && typeof event === 'function') {
publisher = event
event = ALL_EVENTS
}

const { serviceEvents = defaultServiceEvents } = getServiceOptions(this) || {}

if (event !== ALL_EVENTS && !serviceEvents.includes(event as string)) {
throw new Error(`'${event.toString()}' is not a valid service event`)
}

const publishers = this[PUBLISHERS]
publishers[event as string] = publisher!

return this
}

get<L extends keyof Settings & string>(name: L): Settings[L] {
return this.settings[name]
}
Expand Down Expand Up @@ -202,6 +271,10 @@ export class Feathers<Services, Settings>

// Add all the mixins
this.mixins.forEach((fn) => fn.call(this, protoService, location, serviceOptions))

// Add channel publishing functionality to the service
channelServiceMixin(this as any)(protoService, location, serviceOptions)

this.routes.insert(path, routerParams)
this.routes.insert(`${path}/:__id`, routerParams)
this.services[location] = protoService
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EventEmitter } from 'events'
import { RealTimeConnection } from '../../declarations.js'
import { RealTimeConnection } from '../declarations.js'

export class Channel extends EventEmitter {
connections: RealTimeConnection[]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { describe, it, beforeEach } from 'vitest'
import assert from 'assert'
import { feathers, Application, RealTimeConnection } from '../index.js'
import { channels, keys } from './index.js'
import { Channel } from './channel/base.js'
import { CombinedChannel } from './channel/combined.js'

const { CHANNELS } = keys
import { Channel } from './base.js'
import { CombinedChannel } from './combined.js'
import { CHANNELS } from './mixin.js'

describe('app.channel', () => {
let app: Application

beforeEach(() => {
app = feathers().configure(channels())
app = feathers()
})

describe('base channels', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RealTimeConnection } from '../../declarations.js'
import { Channel } from './base'
import { RealTimeConnection } from '../declarations.js'
import { Channel } from './base.js'

function collectConnections(children: Channel[]) {
const mappings = new WeakMap<RealTimeConnection, any>()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { describe, it, beforeEach } from 'vitest'
import assert from 'assert'
import { feathers, Application, HookContext } from '../index.js'
import { channels } from './index.js'
import { Channel } from './channel/base.js'
import { CombinedChannel } from './channel/combined.js'
import { Channel } from './base.js'
import { CombinedChannel } from './combined.js'

class TestService {
events = ['foo']
Expand All @@ -17,7 +16,7 @@ describe('app.publish', () => {
let app: Application

beforeEach(() => {
app = feathers().configure(channels())
app = feathers()
})

it('throws an error if service does not send the event', () => {
Expand Down Expand Up @@ -52,7 +51,7 @@ describe('app.publish', () => {

try {
await app.service('test').create({ message: 'something' })
} catch (error: any) {
} catch (_error: any) {
assert.fail('Should never get here')
}
})
Expand Down
Loading
Loading