Skip to content

Commit 75f9637

Browse files
authored
KOL-27 | Added support for listening to notifications (#2)
* Added support for listening to notifications * Cleanup
1 parent 7dd9550 commit 75f9637

File tree

4 files changed

+145
-2
lines changed

4 files changed

+145
-2
lines changed

README.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Kolme client
22

3-
This is a library you can use to interact with kolme-powered applications.
3+
This is a library you can use to interact with kolme-powered applications. Things you can do:
44

5-
Things you can do:
5+
## Broadcast to your kolme app
66

77
```TypeScript
88
import { KolmeClient } from 'kolme-client'
@@ -17,3 +17,28 @@ const block = await client.broadcast(privateKey, [{
1717
}
1818
}])
1919
```
20+
21+
## Listen to notifications on the kolme's websocket endpoint
22+
23+
```TypeScript
24+
import { KolmeClient } from 'kolme-client'
25+
26+
const client = new KolmeClient('https://yourkolme.app')
27+
28+
const client.subscribeToNotifications(
29+
(message) => {
30+
// Do something interesting with the message
31+
},
32+
(socketState) => {
33+
// Get updates about the socket state - may be useful for React re-rendering for example
34+
}
35+
)
36+
37+
// Stop listening after a minute
38+
setTimeout(
39+
() => {
40+
client.unsubscribeFromNotifications()
41+
},
42+
60 * 1000
43+
)
44+
```

src/HttpApi.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as t from 'io-ts';
22
import { isRight } from 'fp-ts/Either';
33
import { PathReporter } from 'io-ts/lib/PathReporter';
44
import waitFor from './waitFor';
5+
import * as websocket from './websocket';
56

67
const processResponse = async <T>(response: Response, decoder: t.Decoder<unknown, T>) : Promise<t.Validation<T>> => {
78
if(!response.ok) {
@@ -156,6 +157,20 @@ export const waitForTx = async(base: string, txHash: string) : Promise<Block> =>
156157
throw new Error(`Kolme block with tx ${txHash} did not appear on the side chain after ${(after - before) / 1000} seconds`)
157158
}
158159

160+
export type SubscribeToNotificationsInputs = {
161+
onOpen: () => void
162+
onMessage: (message: MessageEvent) => void
163+
onClose: () => void
164+
onError: () => void
165+
}
166+
167+
export const subscribeToNotifications = (base: string, inputs: SubscribeToNotificationsInputs) => {
168+
return websocket.open({
169+
...inputs,
170+
endpoint: `${base}/notifications`,
171+
})
172+
}
173+
159174
export class WithBase {
160175
base: string;
161176

src/index.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { digest } from './crypto';
22
import * as secp from '@noble/secp256k1';
33
import { Buffer } from 'buffer'
44
import * as HttpApi from './HttpApi'
5+
import waitFor from './waitFor';
6+
7+
export const NOTIFICATIONS_RETRY_TIMEOUT = 3000
58

69
export const generatePrivateKey = () : Uint8Array => {
710
return secp.utils.randomPrivateKey()
@@ -39,11 +42,19 @@ export const broadcast = async (httpBase: string, signingKey: Uint8Array, messag
3942
return HttpApi.waitForTx(httpBase, result.txhash)
4043
}
4144

45+
export type WebSocketState =
46+
| { type: 'subscribed', socket: WebSocket }
47+
| { type: 'unsubscribed' }
48+
4249
export class KolmeClient {
4350
httpBase: string
51+
notificationsSocket: WebSocketState
52+
lastNotificationsErrorAt: number | undefined
53+
onNotificationsSocketStateChange: ((state: number) => void) | undefined
4454

4555
constructor(httpBase: string) {
4656
this.httpBase = httpBase;
57+
this.notificationsSocket = { type: 'unsubscribed' };
4758
}
4859

4960
generatePrivateKey() {
@@ -57,4 +68,80 @@ export class KolmeClient {
5768
async broadcast(signingKey: Uint8Array, messages: unknown[]) {
5869
return broadcast(this.httpBase, signingKey, messages)
5970
}
71+
72+
onNotificationsSocketOpen() {
73+
if(this.onNotificationsSocketStateChange && this.notificationsSocket.type === 'subscribed') {
74+
this.onNotificationsSocketStateChange(this.notificationsSocket.socket.readyState)
75+
}
76+
}
77+
78+
async onNotificationsSocketClosed(onMessage: (message: MessageEvent) => void) : Promise<boolean> {
79+
switch(this.notificationsSocket.type) {
80+
case 'subscribed': {
81+
if(this.onNotificationsSocketStateChange) {
82+
this.onNotificationsSocketStateChange(this.notificationsSocket.socket.readyState)
83+
}
84+
85+
// Make sure we're not re-trying too often
86+
if(this.lastNotificationsErrorAt) {
87+
const now = Date.now()
88+
const diff = now - this.lastNotificationsErrorAt
89+
90+
if(diff < NOTIFICATIONS_RETRY_TIMEOUT) {
91+
await waitFor(NOTIFICATIONS_RETRY_TIMEOUT - diff)
92+
}
93+
}
94+
95+
this.subscribeToNotifications(onMessage)
96+
return true
97+
}
98+
case 'unsubscribed': {
99+
return false;
100+
}
101+
}
102+
}
103+
104+
async subscribeToNotifications(onMessage: (message: MessageEvent) => void, onReadyStateChange? : (state: number) => void) : Promise<void> {
105+
const endpoint = this.httpBase.replace('https', 'wss')
106+
107+
if(onReadyStateChange) {
108+
this.onNotificationsSocketStateChange = onReadyStateChange
109+
}
110+
111+
return new Promise((resolve, reject) => {
112+
this.notificationsSocket = {
113+
type: 'subscribed',
114+
socket: HttpApi.subscribeToNotifications(endpoint, {
115+
onMessage,
116+
onOpen: () => {
117+
this.onNotificationsSocketOpen()
118+
resolve()
119+
},
120+
onClose: () => {
121+
this.onNotificationsSocketClosed(onMessage)
122+
},
123+
onError: () => {
124+
this.lastNotificationsErrorAt = Date.now()
125+
reject()
126+
},
127+
})
128+
}
129+
})
130+
}
131+
132+
unsubscribeFromNotifications(): boolean {
133+
this.onNotificationsSocketStateChange = undefined
134+
135+
switch(this.notificationsSocket.type) {
136+
case 'subscribed': {
137+
this.notificationsSocket.socket.close()
138+
this.notificationsSocket = { type: 'unsubscribed' }
139+
140+
return true
141+
}
142+
case 'unsubscribed': {
143+
return false
144+
}
145+
}
146+
}
60147
}

src/websocket.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export type OpenProps = {
2+
endpoint: string;
3+
onOpen: () => void
4+
onMessage: (message: MessageEvent) => void
5+
onClose: () => void
6+
}
7+
8+
export const open = ({ endpoint, onOpen, onClose, onMessage } : OpenProps) : WebSocket => {
9+
const ws = new WebSocket(endpoint);
10+
11+
ws.onopen = onOpen
12+
ws.onmessage = onMessage
13+
ws.onclose = onClose
14+
15+
return ws
16+
}

0 commit comments

Comments
 (0)