Skip to content

Commit 49e681d

Browse files
authored
feat: middleware support (#226)
1 parent cc88de3 commit 49e681d

File tree

6 files changed

+168
-4
lines changed

6 files changed

+168
-4
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jobs:
2020

2121
- name: Install dependencies
2222
run: npm ci
23+
2324
- name: Run linters
2425
run: npm run lint:check
2526

@@ -37,6 +38,11 @@ jobs:
3738
- name: Run tests
3839
run: npm run test -- --coverage --ci --runInBand
3940

41+
- name: Upload code coverage
42+
uses: coverallsapp/github-action@master
43+
with:
44+
github-token: ${{ secrets.GITHUB_TOKEN }}
45+
4046
build:
4147
runs-on: ubuntu-latest
4248
steps:

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# OpenSea Stream API - JavaScript SDK
22

33
[![https://badges.frapsoft.com/os/mit/mit.svg?v=102](https://badges.frapsoft.com/os/mit/mit.svg?v=102)](https://opensource.org/licenses/MIT)
4+
[![Coverage Status](https://coveralls.io/repos/github/ProjectOpenSea/stream-js/badge.svg?branch=main)](https://coveralls.io/github/ProjectOpenSea/stream-js?branch=main)
45
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
56

67
A Javascript SDK for receiving updates from the OpenSea Stream API - pushed over websockets. We currently support the following event types on a per-collection basis:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opensea/stream-js",
3-
"version": "0.0.23",
3+
"version": "0.0.24",
44
"description": "An SDK to receive pushed updates from OpenSea over websocket",
55
"license": "MIT",
66
"author": "OpenSea Developers",

src/client.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,25 @@ import {
1515
TraitOfferEvent,
1616
Callback,
1717
LogLevel,
18-
Network
18+
Network,
19+
OnClientEvent
1920
} from './types';
2021
import { ENDPOINTS } from './constants';
2122

2223
export class OpenSeaStreamClient {
2324
private socket: Socket;
2425
private channels: Map<string, Channel>;
2526
private logLevel: LogLevel;
27+
private onEvent: OnClientEvent;
2628

2729
constructor({
2830
network = Network.MAINNET,
2931
token,
3032
apiUrl,
3133
connectOptions,
3234
logLevel = LogLevel.INFO,
33-
onError = (error) => this.error(error)
35+
onError = (error) => this.error(error),
36+
onEvent = () => true
3437
}: ClientConfig) {
3538
const endpoint = apiUrl || ENDPOINTS[network];
3639
const webTransportDefault =
@@ -44,6 +47,7 @@ export class OpenSeaStreamClient {
4447
this.socket.onError(onError);
4548
this.channels = new Map<string, Channel>();
4649
this.logLevel = logLevel;
50+
this.onEvent = onEvent;
4751
}
4852

4953
private debug(message: unknown) {
@@ -113,7 +117,14 @@ export class OpenSeaStreamClient {
113117
this.debug(`Fetching channel ${topic}`);
114118
const channel = this.getChannel(topic);
115119
this.debug(`Subscribing to ${eventType} events on ${topic}`);
116-
channel.on(eventType, callback);
120+
121+
const onClientEvent = this.onEvent;
122+
channel.on(eventType, (event) => {
123+
if (onClientEvent(collectionSlug, eventType, event)) {
124+
callback(event);
125+
}
126+
});
127+
117128
return () => {
118129
this.debug(`Unsubscribing from ${eventType} events on ${topic}`);
119130
channel.leave().receive('ok', () => {

src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import type { SocketConnectOption } from 'phoenix';
22

3+
export type OnClientEvent = <Payload>(
4+
collection: string,
5+
eventType: EventType,
6+
event: BaseStreamMessage<Payload>
7+
) => boolean;
8+
39
/**
410
* OpenSea Stream API configuration object
511
* @param token API key to use for API
@@ -8,6 +14,7 @@ import type { SocketConnectOption } from 'phoenix';
814
* @param connectOptions `SocketConnectOption` type to use to connect to the Stream API socket.
915
* @param onError a callback function to use whenever errors occur in the SDK.
1016
* @param logLevel `LogLevel` type to define the amount of logging the SDK should provide.
17+
* @param onEvent a callback function to use whenever an event is emmited in the SDK. Can be used to globally apply some logic, e.g emitting metric/logging etc. If the onEvent handler returns false, event will be filtered and the subscription callback won't be invoked.
1118
*/
1219
export type ClientConfig = {
1320
network?: Network;
@@ -16,6 +23,7 @@ export type ClientConfig = {
1623
connectOptions?: Partial<SocketConnectOption>;
1724
onError?: (error: unknown) => void;
1825
logLevel?: LogLevel;
26+
onEvent?: OnClientEvent;
1927
};
2028

2129
export enum Network {

tests/client.spec.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,141 @@ describe('event streams', () => {
130130
});
131131
});
132132
});
133+
134+
describe('middleware', () => {
135+
test('single', () => {
136+
const collectionSlug = 'c1';
137+
138+
const onClientEvent = jest.fn().mockImplementation(() => true);
139+
140+
streamClient = new OpenSeaStreamClient({
141+
token: 'test',
142+
apiUrl: 'ws://localhost:1234',
143+
connectOptions: { transport: WebSocket },
144+
onEvent: onClientEvent
145+
});
146+
147+
const socket = getSocket(streamClient);
148+
jest
149+
.spyOn(socket, 'endPointURL')
150+
.mockImplementation(() => 'ws://localhost:1234');
151+
152+
const onEvent = jest.fn();
153+
154+
const listingEvent = mockEvent(EventType.ITEM_LISTED, {});
155+
const saleEvent = mockEvent(EventType.ITEM_SOLD, {});
156+
157+
streamClient.onEvents(
158+
collectionSlug,
159+
[EventType.ITEM_LISTED, EventType.ITEM_SOLD],
160+
(event) => onEvent(event)
161+
);
162+
163+
server.send(
164+
encode({
165+
topic: collectionTopic(collectionSlug),
166+
event: EventType.ITEM_LISTED,
167+
payload: listingEvent
168+
})
169+
);
170+
171+
server.send(
172+
encode({
173+
topic: collectionTopic(collectionSlug),
174+
event: EventType.ITEM_SOLD,
175+
payload: saleEvent
176+
})
177+
);
178+
179+
expect(onClientEvent).nthCalledWith(
180+
1,
181+
collectionSlug,
182+
EventType.ITEM_LISTED,
183+
listingEvent
184+
);
185+
186+
expect(onClientEvent).nthCalledWith(
187+
2,
188+
collectionSlug,
189+
EventType.ITEM_SOLD,
190+
saleEvent
191+
);
192+
193+
expect(onEvent).nthCalledWith(1, listingEvent);
194+
expect(onEvent).nthCalledWith(2, saleEvent);
195+
196+
streamClient.disconnect();
197+
});
198+
199+
test('filter out events', () => {
200+
const collectionSlug = 'c1';
201+
202+
const onClientEvent = jest
203+
.fn()
204+
.mockImplementation(
205+
(_c, _e, event) => event.payload.chain === 'ethereum'
206+
);
207+
208+
streamClient = new OpenSeaStreamClient({
209+
token: 'test',
210+
apiUrl: 'ws://localhost:1234',
211+
connectOptions: { transport: WebSocket },
212+
onEvent: onClientEvent
213+
});
214+
215+
const socket = getSocket(streamClient);
216+
jest
217+
.spyOn(socket, 'endPointURL')
218+
.mockImplementation(() => 'ws://localhost:1234');
219+
220+
const onEvent = jest.fn();
221+
222+
const ethereumListing = mockEvent(EventType.ITEM_LISTED, {
223+
chain: 'ethereum'
224+
});
225+
const polygonListing = mockEvent(EventType.ITEM_LISTED, {
226+
chain: 'polygon'
227+
});
228+
229+
streamClient.onEvents(
230+
collectionSlug,
231+
[EventType.ITEM_LISTED, EventType.ITEM_SOLD],
232+
(event) => onEvent(event)
233+
);
234+
235+
server.send(
236+
encode({
237+
topic: collectionTopic(collectionSlug),
238+
event: EventType.ITEM_LISTED,
239+
payload: ethereumListing
240+
})
241+
);
242+
243+
server.send(
244+
encode({
245+
topic: collectionTopic(collectionSlug),
246+
event: EventType.ITEM_SOLD,
247+
payload: polygonListing
248+
})
249+
);
250+
251+
expect(onClientEvent).nthCalledWith(
252+
1,
253+
collectionSlug,
254+
EventType.ITEM_LISTED,
255+
ethereumListing
256+
);
257+
258+
expect(onClientEvent).nthCalledWith(
259+
2,
260+
collectionSlug,
261+
EventType.ITEM_SOLD,
262+
polygonListing
263+
);
264+
265+
expect(onEvent).toHaveBeenCalledTimes(1);
266+
expect(onEvent).toHaveBeenCalledWith(ethereumListing);
267+
268+
streamClient.disconnect();
269+
});
270+
});

0 commit comments

Comments
 (0)