Skip to content

Commit 1b33054

Browse files
committed
Release v0.3.0
1 parent 0fac03c commit 1b33054

6 files changed

Lines changed: 119 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# @opensea/stream-js
2+
3+
## 0.3.0
4+
5+
### Minor Changes
6+
7+
- Add `version` field to `BaseStreamMessage` for out-of-order event resolution

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,41 @@ client.onItemCancelled('collection-slug', (event) => {
171171

172172
If you'd like to listen to an event from all collections use wildcard `*` for the `collectionSlug` parameter.
173173

174+
# Event Versioning
175+
176+
Every stream event includes a numeric `version` field that is **monotonically increasing per source entity**. Use it to handle out-of-order event delivery: when two events arrive for the same entity, the one with the higher `version` is the newer state.
177+
178+
## Version scale depends on event type
179+
180+
The backend emits whichever authoritative monotonic counter exists for the underlying entity, so the *scale* of `version` varies:
181+
182+
| Event types | `version` semantic |
183+
| --- | --- |
184+
| `item_listed`, `item_cancelled`, `item_received_offer`, `item_received_bid`, `collection_offer`, `trait_offer`, `order_invalidate`, `order_revalidate` | Order revision counter (small monotonic integer per order) |
185+
| `item_transferred`, `item_sold`, `item_metadata_updated` | Epoch milliseconds of the event's source timestamp |
186+
187+
Both representations are monotonic and sufficient for resolving out-of-order delivery, but the two scales are **not comparable to each other**. Never compare `version` across different event families or across unrelated entities — only compare versions for the same entity within the same event family.
188+
189+
## Usage
190+
191+
Scope your stored versions by event type (or by the source entity — order id for order-derived events, nft_id for item events):
192+
193+
```typescript
194+
const latestOrderVersion = new Map<string, number>();
195+
196+
client.onItemListed('*', (event) => {
197+
const orderHash = event.payload.order_hash;
198+
const prev = latestOrderVersion.get(orderHash) ?? 0;
199+
if (event.version > prev) {
200+
latestOrderVersion.set(orderHash, event.version);
201+
// Process — this is newer state for this order
202+
}
203+
// Otherwise discard — we already have newer state for this order
204+
});
205+
```
206+
207+
Multiple backend pods process events, DLQ replays can deliver older state, and normal Kafka processing can deliver events out of order. Comparing `version` within the same entity lets consumers always converge to the correct current state.
208+
174209
# Types
175210

176211
Types are included to make working with our event payload objects easier.

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.2.3",
3+
"version": "0.3.0",
44
"description": "A TypeScript SDK to receive pushed updates from OpenSea over websocket",
55
"license": "MIT",
66
"author": "OpenSea Developers",

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export type Payload = {
7070

7171
export type BaseStreamMessage<Payload> = {
7272
event_type: string
73+
version: number
7374
sent_at: string
7475
payload: Payload
7576
}

test/client.spec.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,78 @@ describe("event streams", () => {
133133
})
134134
})
135135

136+
describe("version", () => {
137+
test("passes custom version to callback", () => {
138+
const collectionSlug = "c1"
139+
140+
streamClient = new OpenSeaStreamClient({
141+
...clientOpts,
142+
connectOptions: { transport: WebSocket },
143+
})
144+
145+
const socket = getSocket(streamClient)
146+
vi.spyOn(socket, "endPointURL").mockImplementation(
147+
() => "ws://localhost:1234",
148+
)
149+
150+
const onEvent = vi.fn()
151+
streamClient.onEvents(collectionSlug, [EventType.ITEM_LISTED], event =>
152+
onEvent(event),
153+
)
154+
155+
const payload = mockEvent(
156+
EventType.ITEM_LISTED,
157+
{},
158+
{
159+
version: 1713300042000,
160+
},
161+
)
162+
163+
server.send(
164+
encode({
165+
topic: collectionTopic(collectionSlug),
166+
event: EventType.ITEM_LISTED,
167+
payload,
168+
}),
169+
)
170+
171+
expect(onEvent).toHaveBeenCalledWith(payload)
172+
expect(onEvent.mock.calls[0][0].version).toBe(1713300042000)
173+
})
174+
175+
test("includes default version in all events", () => {
176+
const collectionSlug = "c1"
177+
178+
streamClient = new OpenSeaStreamClient({
179+
...clientOpts,
180+
connectOptions: { transport: WebSocket },
181+
})
182+
183+
const socket = getSocket(streamClient)
184+
vi.spyOn(socket, "endPointURL").mockImplementation(
185+
() => "ws://localhost:1234",
186+
)
187+
188+
const onEvent = vi.fn()
189+
streamClient.onEvents(collectionSlug, [EventType.ITEM_LISTED], event =>
190+
onEvent(event),
191+
)
192+
193+
const payload = mockEvent(EventType.ITEM_LISTED, {})
194+
195+
server.send(
196+
encode({
197+
topic: collectionTopic(collectionSlug),
198+
event: EventType.ITEM_LISTED,
199+
payload,
200+
}),
201+
)
202+
203+
expect(onEvent).toHaveBeenCalledWith(payload)
204+
expect(onEvent.mock.calls[0][0].version).toBe(1713300000000)
205+
})
206+
})
207+
136208
describe("middleware", () => {
137209
test("single", () => {
138210
const collectionSlug = "c1"

test/helpers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,13 @@ export const encode = ({
3434
export const mockEvent = <Payload = unknown>(
3535
eventType: EventType,
3636
payload: Payload,
37+
overrides?: Partial<BaseStreamMessage<Payload>>,
3738
): BaseStreamMessage<Payload> => {
3839
return {
3940
event_type: eventType,
41+
version: 1713300000000,
4042
payload,
4143
sent_at: Date.now().toString(),
44+
...overrides,
4245
}
4346
}

0 commit comments

Comments
 (0)