itty-sockets 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: kwhitley
4
+ open_collective: kevinrwhitley
5
+ # patreon: # Replace with a single Patreon username
6
+ # ko_fi: # Replace with a single Ko-fi username
7
+ # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8
+ # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9
+ # liberapay: # Replace with a single Liberapay username
10
+ # issuehunt: # Replace with a single IssueHunt username
11
+ # otechie: # Replace with a single Otechie username
12
+ # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
@@ -0,0 +1,25 @@
1
+ on: [push, pull_request]
2
+
3
+ name: test
4
+
5
+ jobs:
6
+ build:
7
+ name: Build
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v3
11
+ with:
12
+ fetch-depth: 0
13
+
14
+ - uses: oven-sh/setup-bun@v2.0.1
15
+
16
+ - name: Clean workspace and run tests
17
+ run: |
18
+ git clean -xfd # Remove all untracked files, including stale tests
19
+ bun install
20
+ bun test --coverage --coverage-reporter=text --coverage-reporter=lcov
21
+
22
+ - name: Coveralls
23
+ uses: coverallsapp/github-action@master
24
+ with:
25
+ github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,19 @@
1
+ name: lint
2
+
3
+ on:
4
+ push:
5
+ branches: [v0.x]
6
+ pull_request:
7
+ branches: [v0.x]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v2
15
+ - uses: oven-sh/setup-bun@v2.0.1
16
+ - name: install dependencies
17
+ run: bun install
18
+ - name: lint
19
+ run: bun run lint
@@ -0,0 +1,19 @@
1
+ name: verify
2
+
3
+ on:
4
+ push:
5
+ branches: [v0.x]
6
+ pull_request:
7
+ branches: [v0.x]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v2
15
+ - uses: oven-sh/setup-bun@v2.0.1
16
+ - name: Install dependencies
17
+ run: bun install
18
+ - name: Build
19
+ run: bun run build
package/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ ## Changelog
2
+ CAUTION: Pre v1.0.0, this should be considered an alpha release, with minor updates allowing for breaking changes to the interface.
3
+ #### v0.7.0
4
+ - added: .on('*', listener) to respond to *any* messages (e.g. normal, custom, join, leave, error, etc)
5
+ #### v0.6.0
6
+ - added: .on('custom-type', listener) support, keying off payload.type
7
+ - added: .on(filterFunction, listener) support to route listeners based on custom payload rules
8
+ - added: .on('message', listener) still works, catching *all* user-sent messages
9
+ - added: full TypeScript generics support for .on and .send functions
10
+ - added: for convenience, we now destructure the message payload into the top-level event (before the event props)
11
+ - breaking: removed Date casting of event.date, now left as numeric timestamp
12
+ #### v0.5.0
13
+ - BREAKING: removed base (url) option - instead, simply pass a full wss:// path as the channelId to use an external compatible server.
14
+ #### v0.4.0
15
+ - added: base (url) option
16
+ #### v0.3.1
17
+ - fixes: module export
18
+ - removes extra NPM files
19
+ #### v0.3.0
20
+ - breaking: every event has multiple listeners (previously only on('message') allowed multiple)
21
+ - added: join/leave/error event types
22
+ - added: .remove('event-name', listener) to remove listeners
23
+ #### v0.2.3
24
+ - fix type hinting on listeners
25
+ - improve type hinting on send/push
26
+ #### v0.2.0
27
+ - alpha release 2
28
+ #### v0.1.0
29
+ - alpha release 1
package/README.md CHANGED
@@ -18,32 +18,19 @@
18
18
 
19
19
  ---
20
20
 
21
- ### The last WebSocket client you'll need : 512 bytes, all-in.
21
+ ### Say goodbye to WebSocket boilerplate.
22
+
23
+ Your own wrapper is bigger, I promise.
24
+
25
+ Or [optionally] go a step further and use the integrated [itty.ws](https://itty.ws) connection to send messages (zero-config, zero-tracking, 100% free).
22
26
 
23
27
  ## Features ✨
24
- 1. **DX perks** - JSON-in/out, queued messages, easy-reconnect, chainable everything.
25
- 1. **Works with any JSON-based WebSocket server**
26
- 1. **Powerful Routing**
27
- - Listen for all messages
28
- ```ts
29
- .on('message', data => console.log(data))
30
- ```
31
- - Only specific types
32
- ```ts
33
- // matches any data with { type: 'chat' }
34
- .on('chat', ({ text }) => console.log(text))
35
- ```
36
- - or fully custom filters
37
- ```ts
38
- // matches only messages where data.value > 20
39
- .on(
40
- ({ value }) => value > 20), // filter function
41
- ({ value }) => console.log(value),
42
- )
43
- ```
44
- 1. **Type-safe message handling**
45
- 1. **Tiny footprint** - 512 bytes, all-in.
46
- 1. **Optional usage with free/public/zero-config [itty.ws](https://itty.ws) service**
28
+ 1. **DX perks** - JSON-in/out, queued messages, easy-reconnect, chainable everything
29
+ 1. **Works with *any* JSON-based WebSocket server** - it's just a raw WebSocket wrapper, after all
30
+ 1. **Powerful routing** - easily handle your own message formats
31
+ 1. **Type-safe message handling** - so your app knows what to expect
32
+ 1. **No socket server needed** - Use [itty.ws](https://itty.ws) public channels to get started even faster
33
+ 1. **Tiny** - under 500 bytes total
47
34
 
48
35
  ## Basic Example
49
36
  ```ts
@@ -85,7 +72,7 @@ import { connect } from 'itty-sockets'
85
72
  **Option 2: Just copy this snippet:**
86
73
  <!-- BEGIN SNIPPET -->
87
74
  ```ts
88
- let connect=(e,s={})=>{let p,a=0,n=[],t=[],o={},l=()=>(p||(p=new WebSocket((/^wss?:/.test(e)?e:"wss://itty.ws/c/"+e)+"?"+new URLSearchParams(s)),p.onmessage=(e,s=JSON.parse(e.data),p=s?.message,a={...null==p?.[0]&&p,...s})=>{o[s?.type??p?.type]?.map(e=>e(a)),s?.type||o.message?.map(e=>e(a)),t.map(([e,s])=>e(a)&&s(a))},p.onopen=()=>(n.splice(0).map(e=>p?.send(e)),o.open?.map(e=>e()),a&&p?.close()),p.onclose=()=>(a=0,p=null,o.close?.map(e=>e()))),m),m=new Proxy(l,{get:(e,s)=>({open:l,close:()=>(1==p?.readyState?p.close():a=1,m),push:(e,s)=>(a=1,m.send(e,s)),send:(e,s)=>(e=JSON.stringify(e),e=s?""+s+""+e:e,1==p?.readyState?(p.send(e),m):(n.push(e),l())),on:(e,s)=>(s&&(e?.[0]?(o[e]??=[]).push(s):t.push([e,s])),l()),remove:(e,s,p=o[e],a=p?.indexOf(s)??-1)=>(~a&&p?.splice(a,1),l())}[s])});return m};
75
+ let connect=(e,s={})=>{let a,t,n=[],p={},o=()=>(a||(a=new WebSocket((/^wss?:/.test(e)?e:"wss://itty.ws/c/"+e)+"?"+new URLSearchParams(s)),a.onmessage=(e,s=JSON.parse(e.data),a=s?.message,t={...null==a?.[0]&&a,...s})=>[t.type,s.type?0:"message","*"].map(e=>p[e]?.map(e=>e(t))),a.onopen=()=>(n.splice(0).map(e=>a.send(e)),p.open?.map(e=>e(t)),t&&a?.close()),a.onclose=()=>(t=a=null,p.close?.map(e=>e(t)))),l),l={open:o,send:(e,s)=>(e=(s?`${s}`:"")+JSON.stringify(e),1&a?.readyState?a.send(e):n.push(e),o()),on:(e,s)=>((p[e?.[0]?e:"*"]??=[]).push(e?.[0]?s:a=>e?.(a)&&s(a)),o()),remove:(e,s)=>(p[e]=p[e]?.filter(e=>e!=s),l),close:()=>(1&a?.readyState?a.close():t=1,l),push:(e,s)=>(t=1,l.send(e,s))};return l};
89
76
  ```
90
77
  <!-- END SNIPPET -->
91
78
  *Note: This will lose TypeScript support.*
@@ -101,13 +88,14 @@ connect('wss://example.com')
101
88
  .on('message', console.log)
102
89
 
103
90
  // and just { type: 'chat' }
104
- .on('chat',
91
+ .on('chat',
105
92
  ({ user, text }) => console.log(`${user} says: ${text}`)
106
93
  )
107
94
  ```
108
95
 
109
96
  Now let's assume the following 2 messages are sent:
110
97
  ```json
98
+ // message 1
111
99
  {
112
100
  "type": "chat",
113
101
  "user": "Kevin",
@@ -116,6 +104,7 @@ Now let's assume the following 2 messages are sent:
116
104
  ```
117
105
 
118
106
  ```json
107
+ // message 2
119
108
  {
120
109
  "date": 1754659171196,
121
110
  "items": [1, 2, 3],
@@ -123,12 +112,13 @@ Now let's assume the following 2 messages are sent:
123
112
  ```
124
113
 
125
114
  This will output the following to the console:
126
- ```
115
+ ```js
116
+ // message 1
127
117
  { type: "chat", user: "Kevin", text: "Hey!" }
118
+ "Kevin says: Hey!"
128
119
 
120
+ // message 2
129
121
  { date: 1754659171196, items: [1, 2, 3] }
130
-
131
- "Kevin says: Hey!"
132
122
  ```
133
123
 
134
124
  ## Example 3 - Reconnection
package/bun.lockb ADDED
Binary file
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "itty-sockets",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "WebSockets : simplified and minified.",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./connect.mjs",
9
9
  "types": "./connect.d.ts",
10
- "require": "./connect.cjs"
10
+ "require": "./connect.js"
11
11
  }
12
12
  },
13
13
  "scripts": {
@@ -0,0 +1,570 @@
1
+ import { describe, afterAll, expect, it, mock } from 'bun:test'
2
+ import { connect, type IttySocket, type UseItty } from './connect'
3
+
4
+ type TestLeaf = (args: {
5
+ channel: IttySocket<UseItty>,
6
+ resolve: () => void,
7
+ spy: () => void,
8
+ getChannel: (options?: any) => IttySocket<UseItty>
9
+ }) => void
10
+
11
+ type TestTree = {
12
+ [key: string]: TestTree | TestLeaf
13
+ }
14
+
15
+ type ChatMessage = {
16
+ type: 'chat',
17
+ user: string,
18
+ text: string,
19
+ }
20
+
21
+ const EXPOSED_METHODS = ['send', 'push', 'on', 'remove', 'close', 'open']
22
+ const OPEN_CHANNELS: IttySocket[] = []
23
+
24
+ const tests: TestTree = {
25
+ 'NAMED EXPORTS': {
26
+ 'import { connect } from "itty-sockets"': {
27
+ 'is a function': () => expect(typeof connect).toBe('function'),
28
+ },
29
+ },
30
+ 'connect(id, options?)': {
31
+ 'constructs correct WebSocket URL': () => {
32
+ const originalWebSocket = global.WebSocket
33
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
34
+ const mockFn = mock((url: string) => ({}))
35
+
36
+ // @ts-ignore
37
+ global.WebSocket = mockFn
38
+
39
+ connect('my-channel', { a: 'b', c: 'd' } as any).open()
40
+ expect(mockFn.mock.calls[0][0]).toBe('wss://itty.ws/c/my-channel?a=b&c=d')
41
+
42
+ connect('ws://custom.server/path').open()
43
+ expect(mockFn.mock.calls[1][0]).toBe('ws://custom.server/path?')
44
+
45
+ connect('ws://custom.server/path', { echo: true, alias: 'test-user' }).open()
46
+ expect(mockFn.mock.calls[2][0]).toBe('ws://custom.server/path?echo=true&alias=test-user')
47
+
48
+ global.WebSocket = originalWebSocket
49
+ },
50
+ 'exposes chainable method': EXPOSED_METHODS.reduce((acc, method) => {
51
+ acc[`.${method}()`] = ({ channel }) => {
52
+ expect(typeof channel[method]).toBe('function')
53
+ expect(channel[method]()).toBe(channel)
54
+ }
55
+ return acc
56
+ }, {} as Record<string, TestLeaf>),
57
+ 'OPTIONS': {
58
+ '{ echo: true }': {
59
+ 'sends messages back to sender': async ({ getChannel, resolve }) =>
60
+ getChannel({ echo: true })
61
+ .on('message', msg => {
62
+ expect(msg.message).toBe('test')
63
+ resolve()
64
+ })
65
+ .send('test'),
66
+ },
67
+ '{ alias: string }': {
68
+ 'sets alias for messages': async ({ getChannel, resolve }) =>
69
+ getChannel({ echo: true, alias: 'test-user' })
70
+ .on('message', msg => {
71
+ expect(msg.alias).toBe('test-user')
72
+ resolve()
73
+ })
74
+ .send('test'),
75
+ 'sets alias for join events if { announce: true } is set': async ({ getChannel, resolve }) =>
76
+ getChannel({ echo: true, alias: 'test-user', announce: true })
77
+ .on('join', ({ alias }) => {
78
+ expect(alias).toBe('test-user')
79
+ resolve()
80
+ }),
81
+ },
82
+ '{ as: string }': {
83
+ 'sets alias for messages': async ({ getChannel, resolve }) =>
84
+ getChannel({ echo: true, as: 'test-user' })
85
+ .on('message', msg => {
86
+ expect(msg.alias).toBe('test-user')
87
+ resolve()
88
+ })
89
+ .send('test')
90
+ },
91
+ '{ announce: true }': {
92
+ 'announces self to channel upon joining/leaving': async ({ getChannel, resolve }) =>
93
+ getChannel({ announce: true, as: 'test-user' })
94
+ .on('join', ({ alias }) => {
95
+ expect(alias).toBe('test-user')
96
+ resolve()
97
+ })
98
+ },
99
+ },
100
+ 'METHODS': {
101
+ '.open()': {
102
+ 'opens the socket': async ({ channel, resolve }) =>
103
+ channel
104
+ .on('open', resolve)
105
+ .open(),
106
+ 'calling multiple times is fine': async ({ channel, resolve }) =>
107
+ channel
108
+ .on('open', () => {
109
+ channel.open() // one more time for good measure
110
+ resolve()
111
+ })
112
+ .open(),
113
+ },
114
+ '.close()': {
115
+ 'closes the socket': async ({ channel, resolve }) =>
116
+ channel
117
+ .on('close', resolve)
118
+ .on('open', channel.close),
119
+ 'calling multiple times is fine': async ({ channel, resolve }) =>
120
+ channel
121
+ .on('close',resolve)
122
+ .on('open', () => {
123
+ channel.close()
124
+ channel.close()
125
+ }),
126
+ },
127
+ '.on(\'open\', listener)': {
128
+ 'registers an event listener that is called when the socket is opened': async ({ channel, resolve }) =>
129
+ channel
130
+ .on('open', resolve)
131
+ .open(),
132
+ 'allows multiple listeners': async ({ channel, resolve, spy }) =>
133
+ channel
134
+ .on('open', spy)
135
+ .on('open', () => {
136
+ expect(spy).toHaveBeenCalled()
137
+ resolve()
138
+ }),
139
+ },
140
+ '.on(\'close\', listener)': {
141
+ 'registers an event listener that is called when the socket is closed': async ({ channel, resolve }) =>
142
+ channel
143
+ .on('close', resolve)
144
+ .on('open', channel.close),
145
+ 'allows multiple listeners': async ({ channel, resolve, spy }) =>
146
+ channel
147
+ .on('close', spy)
148
+ .on('close', () => {
149
+ expect(spy).toHaveBeenCalled()
150
+ resolve()
151
+ })
152
+ .on('open', channel.close),
153
+ },
154
+ '.on(\'message\', listener)': {
155
+ 'registers an event listener that is called when a message is received': async ({ getChannel, resolve }) => {
156
+ getChannel({ echo: true })
157
+ .on('message', e => {
158
+ expect(e.message).toBe('test')
159
+ resolve()
160
+ })
161
+ .send('test')
162
+ },
163
+ 'allows multiple message listeners': async ({ getChannel, resolve, spy }) =>
164
+ getChannel({ echo: true })
165
+ .on('message', spy)
166
+ .on('message', (e) => {
167
+ expect(e.message).toBe('test')
168
+ expect(spy).toHaveBeenCalled()
169
+ resolve()
170
+ })
171
+ .send('test'),
172
+ 'receives message props on base message object': async ({ getChannel, resolve }) =>
173
+ getChannel({ echo: true })
174
+ .on<{ foo: string }>('message', (e) => {
175
+ expect(e.foo).toBe('bar')
176
+ expect(e.message.foo).toBe('bar')
177
+ resolve()
178
+ })
179
+ .send({ foo: 'bar' }),
180
+ 'message props do not override event base props': async ({ getChannel, resolve }, date = new Date()) =>
181
+ getChannel({ echo: true, alias: 'test-user' })
182
+ .on<{ foo: string }>('message', (e) => {
183
+ expect(e.foo).toBe('bar')
184
+ // confirm types are correct
185
+ expect(e.uid).toBeTypeOf('string')
186
+ expect(e.alias).toBeTypeOf('string')
187
+ expect(e.date).toBeTypeOf('number')
188
+ // confirm props are not overridden
189
+ expect(e.uid).not.toBe('foo')
190
+ expect(e.alias).not.toBe('bar')
191
+ expect(e.date).not.toBe(+date)
192
+ resolve()
193
+ })
194
+ .send({ foo: 'bar', uid: 'foo', alias: 'bar', date }),
195
+ 'base props not polluted by string messages': async ({ getChannel, resolve, spy }) =>
196
+ getChannel({ echo: true })
197
+ .on('message', spy)
198
+ .on('message', (e) => {
199
+ expect(e[0]).toBeUndefined() // "h" if polluted
200
+ expect(spy).toHaveBeenCalled()
201
+ resolve()
202
+ })
203
+ .send('hello'),
204
+ 'base props not polluted by array messages': async ({ getChannel, resolve, spy }) =>
205
+ getChannel({ echo: true })
206
+ .on('message', spy)
207
+ .on('message', (e) => {
208
+ expect(e[0]).toBeUndefined() // "1" if polluted
209
+ expect(spy).toHaveBeenCalled()
210
+ resolve()
211
+ })
212
+ .send([1, 2, 3]),
213
+ 'base props not polluted by numeric messages': async ({ getChannel, resolve, spy }) =>
214
+ getChannel({ echo: true })
215
+ .on('message', spy)
216
+ .on('message', (e) => {
217
+ expect(e[0]).toBeUndefined() // "?" if polluted
218
+ expect(spy).toHaveBeenCalled()
219
+ resolve()
220
+ })
221
+ .send(13),
222
+ },
223
+ '.on(\'join\', listener)': {
224
+ 'registers an event listener that is called when a user (or self) joins the channel': async ({ channel, resolve }) =>
225
+ channel
226
+ .on('join', e => {
227
+ expect(e.users).toBe(1)
228
+ resolve()
229
+ }),
230
+ 'does NOT include user details when { announce: true } is not set': async ({ channel, resolve }) =>
231
+ channel
232
+ .on('join', e => {
233
+ expect(e.uid).toBeUndefined()
234
+ expect(e.alias).toBeUndefined()
235
+ resolve()
236
+ }),
237
+ 'DOES include user details when { announce: true } is set': async ({ getChannel, resolve }) =>
238
+ getChannel({ announce: true, alias: 'test-user' })
239
+ .on('join', e => {
240
+ expect(e.uid).not.toBeUndefined()
241
+ expect(e.alias).toBe('test-user')
242
+ resolve()
243
+ })
244
+ },
245
+ '.on(\'error\', listener)': {
246
+ 'registers an event listener that is called when an error occurs': async ({ channel, resolve }) =>
247
+ channel
248
+ .on('error', e => {
249
+ expect(e.message).toContain('non-existent-user')
250
+ resolve()
251
+ })
252
+ .send('test', 'non-existent-user')
253
+ },
254
+ '.on(\'leave\', listener)': {
255
+ 'registers an event listener that is called when a user leaves the channel': async ({ channel, getChannel,resolve }) => {
256
+ channel
257
+ .on('leave', e => {
258
+ expect(e.users).toBe(1)
259
+ resolve()
260
+ })
261
+ .on('open', () => {
262
+ getChannel().push('test') // trigger a join + leave
263
+ })
264
+ },
265
+ 'does NOT include user details when { announce: true } is not set': async ({ channel, getChannel, resolve }) => {
266
+ channel
267
+ .on('leave', e => {
268
+ expect(e.uid).toBeUndefined()
269
+ expect(e.alias).toBeUndefined()
270
+ resolve()
271
+ })
272
+ .on('open', () => {
273
+ getChannel().push('test') // trigger a join + leave
274
+ })
275
+ },
276
+ 'DOES include user details when { announce: true } is set': async ({ getChannel, resolve }) => {
277
+ getChannel()
278
+ .on('leave', e => {
279
+ expect(e.uid).not.toBeUndefined()
280
+ expect(e.alias).toBe('test-user')
281
+ resolve()
282
+ })
283
+ .on('open', () => {
284
+ getChannel({ announce: true, alias: 'test-user' }).push('test') // trigger a join + leave
285
+ })
286
+ }
287
+ },
288
+ '.on(\'{custom-type}\', listener)': {
289
+ 'catches when message.type matches the custom type': async ({ getChannel, resolve }) => {
290
+ getChannel()
291
+ .on<{ user: string, text: string }>('chat', (e) => {
292
+ const { user, text } = e
293
+ expect(user).toBe('test-user')
294
+ expect(text).toBe('test')
295
+ expect(e.type).toBe('chat') // currently giving a TS error (incorrect)
296
+ expect(e.uid).toBeTypeOf('string')
297
+ expect(e.date).toBeTypeOf('number')
298
+ expect(e.user).toBe(e.message.user)
299
+ resolve()
300
+ })
301
+ .on('open', () => {
302
+ getChannel().send({ type: 'chat', user: 'test-user', text: 'test' })
303
+ })
304
+ },
305
+ 'will still trigger "message" listeners': async ({ getChannel, resolve, spy }) =>
306
+ getChannel({ echo: true })
307
+ .on('message', spy)
308
+ .on('chat', () => {
309
+ setTimeout(() => {
310
+ expect(spy).toHaveBeenCalled()
311
+ resolve()
312
+ }, 5)
313
+ })
314
+ .send({ type: 'chat', user: 'test-user', text: 'test' }),
315
+ 'will include custom payloads at top level and under e.message': async ({ getChannel, resolve, spy }) =>
316
+ getChannel({ echo: true })
317
+ .on('message', spy)
318
+ .on<ChatMessage>('chat', (e) => {
319
+ expect(e.type).toBe('chat')
320
+ expect(e.user).toBe('test-user')
321
+ expect(e.text).toBe('test')
322
+ expect(e.message.type).toBe('chat')
323
+ expect(e.message.user).toBe('test-user')
324
+ expect(e.message.text).toBe('test')
325
+ resolve()
326
+ })
327
+ .send({ type: 'chat', user: 'test-user', text: 'test' })
328
+ },
329
+ '.on(\'*\', listener)': {
330
+ 'catches both typed and untyped events': async ({ getChannel, resolve }) => {
331
+ const received: any[] = []
332
+ getChannel({ echo: true })
333
+ .on('*', (e) => {
334
+ received.push(e)
335
+ if (received.length === 2) {
336
+ expect(received[0].type).toBe('join')
337
+ expect(received[1].message).toBe('test')
338
+ resolve()
339
+ }
340
+ })
341
+ .send('test')
342
+ },
343
+ },
344
+ '.on(eventFilter, listener)': {
345
+ 'can accept a filter function as type': async ({ getChannel, resolve, spy }) =>
346
+ getChannel({ echo: true })
347
+ .on('message', spy)
348
+ .on(e => e.type === 'chat', () => {
349
+ setTimeout(() => {
350
+ expect(spy).toHaveBeenCalled()
351
+ resolve()
352
+ }, 5)
353
+ })
354
+ .send({ type: 'chat', user: 'test-user', text: 'test' }),
355
+ },
356
+ '.remove(\'open\', listener)': {
357
+ 'removes a listener (will not fire)': async ({ channel, resolve, spy }) =>
358
+ channel
359
+ .on('open', spy)
360
+ .on('open', () => {
361
+ expect(spy).not.toHaveBeenCalled()
362
+ resolve()
363
+ })
364
+ .remove('open', spy)
365
+ },
366
+ '.send(message, recipient?)': {
367
+ 'delivers a message to the channel': async ({ channel, getChannel, resolve }) => {
368
+ channel
369
+ .on('message', e => {
370
+ expect(e.message).toBe('test')
371
+ resolve()
372
+ })
373
+ .on('open', () => {
374
+ getChannel().send('test')
375
+ })
376
+ },
377
+ 'delivers a message to a recipient': async ({ channel, getChannel, resolve }) => {
378
+ channel
379
+ .on('join', ({ uid }) => {
380
+ channel.send('test', uid)
381
+ })
382
+
383
+ getChannel({ announce: true })
384
+ .on('message', (e) => {
385
+ expect(e.message).toBe('test')
386
+ resolve()
387
+ })
388
+ },
389
+ 'private messages are ONLY delivered to the recipient': async ({ channel, getChannel, resolve, spy }) =>
390
+ channel
391
+ .on('join', ({ uid, alias }) => {
392
+ if (alias === 'test-user') {
393
+ channel.send('test', uid)
394
+ }
395
+ })
396
+ .on('open', () => {
397
+ getChannel()
398
+ .on('message', spy)
399
+ .on('open', () => {
400
+ getChannel({ announce: true, alias: 'test-user' })
401
+ .on('message', (e) => {
402
+ expect(e.message).toBe('test')
403
+ expect(spy).not.toHaveBeenCalled()
404
+ resolve()
405
+ })
406
+ })
407
+ }),
408
+ 'will send an error if the recipient does not exist': async ({ channel, resolve }) =>
409
+ channel
410
+ .on('error', e => {
411
+ expect(e.message).toContain('non-existent-user')
412
+ resolve()
413
+ })
414
+ .send('test', 'non-existent-user'),
415
+ },
416
+ '.push(message, recipient?)': {
417
+ 'sends a message to the channel': async ({ channel, getChannel, resolve }) => {
418
+ channel
419
+ .on('message', e => {
420
+ expect(e.message).toBe('test')
421
+ resolve()
422
+ })
423
+ .on('open', () => {
424
+ getChannel().push('test')
425
+ })
426
+ },
427
+ 'closes after sending a message': async ({ channel, resolve }) =>
428
+ channel
429
+ .on('close', () => {
430
+ resolve()
431
+ })
432
+ .push('test')
433
+ },
434
+ },
435
+ 'MISC BEHAVIOR': {
436
+ 'messages are queued and delivered upon connection': async ({ getChannel, resolve }) => {
437
+ const messages = ['first', 'second', 'third']
438
+ const received: string[] = []
439
+
440
+ getChannel({ echo: true })
441
+ .on('message', e => {
442
+ received.push(e.message)
443
+
444
+ if (received.length === messages.length) {
445
+ expect(received).toEqual(messages)
446
+ resolve()
447
+ }
448
+ })
449
+ .send(messages[0])
450
+ .send(messages[1])
451
+ .send(messages[2])
452
+ },
453
+ 'connection is opened when registering a listener': async ({ channel, resolve }) => channel.on('open', resolve),
454
+ },
455
+ 'EVENT PAYLOADS': {
456
+ 'join': {
457
+ 'default': async ({ channel, resolve }) =>
458
+ channel
459
+ .on('join', e => {
460
+ expect(e.users).toBe(1)
461
+ expect(e.uid).toBeUndefined()
462
+ expect(e.alias).toBeUndefined()
463
+ expect(e.date).toBeTypeOf('number')
464
+ resolve()
465
+ }),
466
+ 'with { announce: true }': async ({ getChannel, resolve }) =>
467
+ getChannel({ announce: true, as: 'test-user' })
468
+ .on('join', e => {
469
+ expect(e.users).toBe(1)
470
+ expect(e.uid).toBeTypeOf('string')
471
+ expect(e.alias).toBe('test-user')
472
+ expect(e.date).toBeTypeOf('number')
473
+ resolve()
474
+ })
475
+ },
476
+ 'leave': {
477
+ 'default': async ({ channel, getChannel, resolve }) => {
478
+ channel
479
+ .on('leave', e => {
480
+ expect(e.users).toBe(1)
481
+ expect(e.uid).toBeUndefined()
482
+ expect(e.alias).toBeUndefined()
483
+ expect(e.date).toBeTypeOf('number')
484
+ resolve()
485
+ })
486
+ .on('open', () => {
487
+ getChannel().push('test')
488
+ })
489
+ },
490
+ 'with { announce: true }': async ({ getChannel, resolve }) => {
491
+ getChannel()
492
+ .on('leave', e => {
493
+ expect(e.users).toBe(1)
494
+ expect(e.uid).toBeTypeOf('string')
495
+ expect(e.alias).toBe('test-user')
496
+ expect(e.date).toBeTypeOf('number')
497
+ resolve()
498
+ })
499
+ .on('open', () => {
500
+ getChannel({ announce: true, as: 'test-user' }).push('test')
501
+ })
502
+ }
503
+ },
504
+ 'message': {
505
+ 'default': async ({ getChannel, resolve }) =>
506
+ getChannel({ echo: true })
507
+ .on('message', e => {
508
+ expect(e.message).toBe('test')
509
+ expect(e.uid).toBeTypeOf('string')
510
+ expect(e.alias).toBeUndefined()
511
+ expect(e.date).toBeTypeOf('number')
512
+ resolve()
513
+ })
514
+ .send('test'),
515
+ 'with { alias: string } includes users alias in payload': async ({ getChannel, resolve }) =>
516
+ getChannel({ echo: true, alias: 'test-user' })
517
+ .on('message', e => {
518
+ expect(e.message).toBe('test')
519
+ expect(e.uid).toBeTypeOf('string')
520
+ expect(e.alias).toBe('test-user')
521
+ expect(e.date).toBeTypeOf('number')
522
+ resolve()
523
+ })
524
+ .send('test'),
525
+ },
526
+ }
527
+ }
528
+ }
529
+
530
+ // setup function for each test
531
+ const setup = () => {
532
+ const id = 'itty:itty-sockets:test-' + Math.random().toString(36).slice(2)
533
+ const getChannel = (options = {}): IttySocket<UseItty> => {
534
+ const channel = connect(id, options)
535
+ OPEN_CHANNELS.push(channel)
536
+ return channel
537
+ }
538
+
539
+ return {
540
+ getChannel,
541
+ channel: getChannel(id) as IttySocket<UseItty>,
542
+ spy: mock(() => {}),
543
+ }
544
+ }
545
+
546
+ // recursive test runner
547
+ const runTests = (tests: TestTree) => {
548
+ for (const [name, test] of Object.entries(tests)) {
549
+ if (typeof test === 'function') {
550
+ if (test.constructor.name === 'AsyncFunction') {
551
+ // @ts-ignore
552
+ it(name, () => new Promise(resolve => test({ ...setup(), resolve })))
553
+ } else {
554
+ // @ts-ignore
555
+ it(name, () => test({ ...setup() }))
556
+ }
557
+ } else {
558
+ describe(name, () => runTests(test))
559
+ }
560
+ }
561
+ }
562
+
563
+ // run the tests!
564
+ runTests(tests)
565
+
566
+ // close any open channels
567
+ afterAll(() => {
568
+ console.log(`closing ${OPEN_CHANNELS.length} channels`)
569
+ OPEN_CHANNELS.forEach(channel => channel.close())
570
+ })
package/src/connect.ts ADDED
@@ -0,0 +1,133 @@
1
+ type IttySocketEvent<BaseFormat> = BaseFormat extends UseItty
2
+ ? 'open' | 'close' | 'message' | 'join' | 'leave'
3
+ : 'open' | 'close' | 'message'
4
+
5
+ type Timestamp = { date: number }
6
+ type UserDetails = { uid: string, alias?: string }
7
+ type OptionalUserDetails = { uid?: string, alias?: string }
8
+
9
+ export type UseItty<MessageType = any> = {
10
+ message: MessageType
11
+ } & UserDetails & Timestamp
12
+
13
+ export type MessageEvent<MessageType = any> = {
14
+ message: MessageType
15
+ } & Timestamp & OptionalUserDetails
16
+
17
+ export type JoinEvent = {
18
+ type: 'join'
19
+ users: number
20
+ } & Timestamp & OptionalUserDetails
21
+
22
+ export type LeaveEvent = {
23
+ type: 'leave'
24
+ users: number
25
+ } & Timestamp & OptionalUserDetails
26
+
27
+ export type ErrorEvent = {
28
+ type: 'error'
29
+ message: string
30
+ } & Timestamp
31
+
32
+ export type IttySocketOptions = {
33
+ as?: string,
34
+ alias?: string,
35
+ echo?: true,
36
+ announce?: true,
37
+ }
38
+
39
+ export interface IttySocketConnect {
40
+ <BaseFormat = object>(
41
+ ...args: BaseFormat extends UseItty
42
+ ? [channelID: string, options?: IttySocketOptions]
43
+ : [url: string, queryParams?: any]
44
+ ): IttySocket<BaseFormat>
45
+ }
46
+
47
+ type UseIttyEvents<BaseFormat> = {
48
+ on(type: 'join', listener: (event: JoinEvent) => any): IttySocket<BaseFormat>
49
+ on(type: 'leave', listener: (event: LeaveEvent) => any): IttySocket<BaseFormat>
50
+ on(type: 'error', listener: (event: ErrorEvent) => any): IttySocket<BaseFormat>
51
+ }
52
+
53
+ type SendMessage<BaseFormat> = BaseFormat extends UseItty
54
+ ? <MessageFormat = any>(message: MessageFormat, uid?: string) => IttySocket<BaseFormat>
55
+ : <MessageFormat = any>(message: MessageFormat) => IttySocket<BaseFormat>
56
+
57
+ export type IttySocket<BaseFormat = object> = {
58
+ open: () => IttySocket<BaseFormat>
59
+ close: () => IttySocket<BaseFormat>
60
+ send: SendMessage<BaseFormat>
61
+ push: SendMessage<BaseFormat>
62
+ remove(type: IttySocketEvent<BaseFormat>, listener: () => any): IttySocket<BaseFormat>
63
+ remove(type: string, listener: () => any): IttySocket<BaseFormat>
64
+
65
+ // EVENTS
66
+ on(type: 'open', listener: () => any): IttySocket<BaseFormat>
67
+ on(type: 'close', listener: () => any): IttySocket<BaseFormat>
68
+ on<MessageFormat = BaseFormat>(type: 'message', listener: (event: BaseFormat & MessageFormat) => any): IttySocket<BaseFormat>
69
+ on<MessageFormat = BaseFormat>(type: string, listener: (event: BaseFormat & MessageFormat & { type: string }) => any): IttySocket<BaseFormat>
70
+ on<MessageFormat = BaseFormat>(type: (event?: any) => any, listener: (event: BaseFormat & MessageFormat & { type: string }) => any): IttySocket<BaseFormat>
71
+ } & (BaseFormat extends UseItty ? UseIttyEvents<BaseFormat> : object)
72
+
73
+ export let connect: IttySocketConnect = (channelId: string, options = {}) => {
74
+ let ws: WebSocket | null,
75
+ closeAfterSend: any,
76
+ queue: string[] = [],
77
+ events: Record<string, Array<(event?: any) => any>> = {}
78
+
79
+ let open = () => (
80
+ ws || (
81
+ // @ts-ignore - options will be cast as string regardless of what is passed
82
+ ws = new WebSocket((/^wss?:/.test(channelId) ? channelId : 'wss://itty.ws/c/' + channelId) + '?' + new URLSearchParams(options)),
83
+
84
+ ws.onmessage = (
85
+ event: any,
86
+ parsed = JSON.parse(event.data),
87
+ payload = parsed?.message,
88
+ eventPayload = {
89
+ ...(payload?.[0] == null && payload),
90
+ ...parsed,
91
+ },
92
+ ) =>
93
+ [eventPayload.type, parsed.type ? 0 : 'message', '*'].map(key =>
94
+ events[key]?.map(listener => listener(eventPayload))
95
+ ),
96
+
97
+ ws.onopen = () => (
98
+ queue.splice(0).map(m => ws!.send(m)),
99
+ events.open?.map(listener => listener(closeAfterSend)),
100
+ closeAfterSend && ws?.close()
101
+ ),
102
+
103
+ ws.onclose = () => (
104
+ closeAfterSend = ws = null,
105
+ events.close?.map(listener => listener(closeAfterSend))
106
+ )
107
+ ),
108
+ socket
109
+ )
110
+
111
+ let socket: any = {
112
+ open,
113
+ send: (message: any, recipient?: string) => (
114
+ message = (recipient ? `\x1F${recipient}\x1F` : '') + JSON.stringify(message),
115
+ // @ts-ignore
116
+ ws?.readyState & 1 ? ws!.send(message) : queue.push(message),
117
+ open()
118
+ ),
119
+ on: (type: any, listener: (e?: any) => any) => (
120
+ (events[type?.[0] ? type : '*'] ??= []).push(type?.[0] ? listener : (e: any) => type?.(e) && listener(e)),
121
+ open()
122
+ ),
123
+ remove: (type: any, listener: () => any) => (
124
+ events[type] = events[type]?.filter((l: any) => l != listener),
125
+ socket
126
+ ),
127
+ // @ts-ignore
128
+ close: () => (ws?.readyState & 1 ? ws!.close() : closeAfterSend = 1, socket),
129
+ push: (message: any, recipient?: string) => (closeAfterSend = 1, socket.send(message, recipient!)),
130
+ }
131
+
132
+ return socket
133
+ }
@@ -0,0 +1,183 @@
1
+ type IttySocketEvent<BaseFormat> = BaseFormat extends UseItty
2
+ ? 'open' | 'close' | 'message' | 'join' | 'leave'
3
+ : 'open' | 'close' | 'message'
4
+
5
+ type Timestamp = { date: number }
6
+ type UserDetails = { uid: string, alias?: string }
7
+ type OptionalUserDetails = { uid?: string, alias?: string }
8
+
9
+ export type UseItty<MessageType = any> = {
10
+ message: MessageType
11
+ } & UserDetails & Timestamp
12
+
13
+ export type MessageEvent<MessageType = any> = {
14
+ message: MessageType
15
+ } & Timestamp & OptionalUserDetails
16
+
17
+ export type JoinEvent = {
18
+ type: 'join'
19
+ users: number
20
+ } & Timestamp & OptionalUserDetails
21
+
22
+ export type LeaveEvent = {
23
+ type: 'leave'
24
+ users: number
25
+ } & Timestamp & OptionalUserDetails
26
+
27
+ export type ErrorEvent = {
28
+ type: 'error'
29
+ message: string
30
+ } & Timestamp
31
+
32
+ export type IttySocketOptions = {
33
+ as?: string,
34
+ alias?: string,
35
+ echo?: true,
36
+ announce?: true,
37
+ }
38
+
39
+ export interface IttySocketConnect {
40
+ <BaseFormat = object>(
41
+ ...args: BaseFormat extends UseItty
42
+ ? [channelID: string, options?: IttySocketOptions]
43
+ : [url: string, queryParams?: any]
44
+ ): IttySocket<BaseFormat>
45
+ }
46
+
47
+ type UseIttyEvents<BaseFormat> = {
48
+ on(type: 'join', listener: (event: JoinEvent) => any): IttySocket<BaseFormat>
49
+ on(type: 'leave', listener: (event: LeaveEvent) => any): IttySocket<BaseFormat>
50
+ on(type: 'error', listener: (event: ErrorEvent) => any): IttySocket<BaseFormat>
51
+ }
52
+
53
+ type SendMessage<BaseFormat> = BaseFormat extends UseItty
54
+ ? <MessageFormat = any>(message: MessageFormat, uid?: string) => IttySocket<BaseFormat>
55
+ : <MessageFormat = any>(message: MessageFormat) => IttySocket<BaseFormat>
56
+
57
+ export type IttySocket<BaseFormat = object> = {
58
+ open: () => IttySocket<BaseFormat>
59
+ close: () => IttySocket<BaseFormat>
60
+ send: SendMessage<BaseFormat>
61
+ push: SendMessage<BaseFormat>
62
+ remove(type: IttySocketEvent<BaseFormat>, listener: () => any): IttySocket<BaseFormat>
63
+ remove(type: string, listener: () => any): IttySocket<BaseFormat>
64
+
65
+ // EVENTS
66
+ on(type: 'open', listener: () => any): IttySocket<BaseFormat>
67
+ on(type: 'close', listener: () => any): IttySocket<BaseFormat>
68
+ on<MessageFormat = BaseFormat>(type: 'message', listener: (event: BaseFormat & MessageFormat) => any): IttySocket<BaseFormat>
69
+ on<MessageFormat = BaseFormat>(type: string, listener: (event: BaseFormat & MessageFormat & { type: string }) => any): IttySocket<BaseFormat>
70
+ on<MessageFormat = BaseFormat>(type: (event?: any) => any, listener: (event: BaseFormat & MessageFormat & { type: string }) => any): IttySocket<BaseFormat>
71
+ } & (BaseFormat extends UseItty ? UseIttyEvents<BaseFormat> : object)
72
+
73
+ export let connect: IttySocketConnect = (channelId: string, options = {}) => {
74
+ let ws: WebSocket | null,
75
+ closeAfterSend: any,
76
+ queue: string[] = [],
77
+ events: Record<string, Array<(event?: any) => any>> = {}
78
+
79
+ let open = () => {
80
+ if (ws) return socket
81
+
82
+ // @ts-ignore - options will be cast as string regardless of what is passed
83
+ ws = new WebSocket((channelId[3] < 'a' ? channelId : 'wss://itty.ws/c/' + channelId) + '?' + new URLSearchParams(options))
84
+
85
+ ws.onmessage = (
86
+ event: any,
87
+ parsed = JSON.parse(event.data),
88
+ payload = parsed?.message,
89
+ eventPayload = {
90
+ ...(payload?.[0] == null && payload),
91
+ ...parsed,
92
+ },
93
+ ) => (
94
+ events[eventPayload.type]?.map(listener => listener(eventPayload)),
95
+ parsed.type || events.message?.map(listener => listener(eventPayload)),
96
+ events['*']?.map(listener => listener(eventPayload))
97
+ )
98
+
99
+ ws.onopen = () => (
100
+ queue.splice(0).map(m => ws!.send(m)),
101
+ events.open?.map(listener => listener(closeAfterSend)),
102
+ closeAfterSend && ws?.close()
103
+ )
104
+
105
+ ws.onclose = () => (
106
+ closeAfterSend = ws = null,
107
+ events.close?.map(listener => listener(closeAfterSend))
108
+ )
109
+
110
+ return socket
111
+ }
112
+
113
+ let send = (message: any, recipient?: string) => (
114
+ message = (recipient ? `\x1F${recipient}\x1F` : '') + JSON.stringify(message),
115
+ ws?.readyState & 1 ? ws!.send(message) : queue.push(message),
116
+ open()
117
+ )
118
+
119
+ let socket: any = {
120
+ open,
121
+ send,
122
+ on: (type: any, listener: (e?: any) => any) => (
123
+ (events[type?.[0] ? type : '*'] ??= []).push(type?.[0] ? listener : (e: any) => type(e) && listener(e)),
124
+ open()
125
+ ),
126
+ remove: (type: any, listener: () => any) => (
127
+ events[type] = events[type]?.filter((l: any) => l != listener),
128
+ socket
129
+ ),
130
+ close: () => (ws?.readyState & 1 ? ws!.close() : closeAfterSend = 1, socket),
131
+ push: (message: any, recipient?: string) => (closeAfterSend = 1, send(message, recipient!)),
132
+ }
133
+
134
+ return socket
135
+ }
136
+
137
+ // type Chat = { type: 'chat', user: string, text: string }
138
+
139
+ // connect<UseItty>('doo')
140
+ // .on<Chat>('message', e => {
141
+ // e
142
+ // })
143
+ // .on<Chat>('chat', (e) => {
144
+ // e.text
145
+ // })
146
+ // .on<Chat>(v => v.type === 'chat', e => {
147
+ // e.texts
148
+ // })
149
+ // // .send() // test for (message) vs (message, recipient) based on BaseFormat type
150
+ // .remove('leave',
151
+
152
+
153
+ // GENERICS TESTING
154
+ // connect('test')
155
+ // .on('message', (e) => e.message.name)
156
+ // .on('close', () => {})
157
+ // .send(123)
158
+ // .on<{ x: string }>('message', (e) => parseInt(e.message.x))
159
+ // .send<{ foo: string }>({ foo: 'bar' })
160
+ // .on('join', e => e.users + 4)
161
+ // .on('leave', e => e.users - 4)
162
+ // .on('error', e => e.message)
163
+ // .on('message', e => e.message.whatever)
164
+ // .on('message', e => e.whatever)
165
+ // .on<{ foo: string }>('message', e => e.message.foo)
166
+ // .on<{ foo: string }>('message', e => e.foo)
167
+ // .on<{ foo: string }>('chat', e => e.foo)
168
+ // .on<{ foo: string }>('chat', e => e.type)
169
+ // .send({ $type: 'chat', foo: 'bar' })
170
+ // .on('*', e => e.message)
171
+
172
+ // .on<{ age: number }>('message', (e) => e.message.name) // ERROR
173
+ // .on<{ x: number }>('message', (e) => parseInt(e.message.x)) // ERROR
174
+ // .send<string>(123) // ERROR
175
+ // .send<{ foo: string }>(123) // ERROR
176
+ // .send<{ foo: string }>({ foo: 'foo', bar: 123 }) // ERROR
177
+ // .on('join', e => e.message) // ERROR
178
+ // .on<{ foo: string }>('join', e => e.users) // ERROR
179
+ // .on('leave', e => e.message) // ERROR
180
+ // .on('error', e => e.foo) // ERROR
181
+ // .on<{ foo: string }>('message', e => e.message.whatever) // ERROR
182
+ // .on<{ foo: string }>('chat', e => e.bar) // ERROR
183
+
package/tsconfig.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": true,
4
+ "allowSyntheticDefaultImports": true,
5
+ "baseUrl": "src",
6
+ "declaration": true,
7
+ "sourceMap": false,
8
+ "esModuleInterop": true,
9
+ "inlineSourceMap": false,
10
+ "lib": ["esnext", "es2015", "dom"],
11
+ "listEmittedFiles": false,
12
+ "listFiles": false,
13
+ "noFallthroughCasesInSwitch": true,
14
+ "pretty": true,
15
+ "rootDir": "src",
16
+ "skipLibCheck": true,
17
+ "strict": true,
18
+ "traceResolution": false,
19
+ "outDir": "",
20
+ "target": "esnext",
21
+ "module": "esnext"
22
+ },
23
+ "exclude": ["node_modules", "dist", "**/*.spec.ts"],
24
+ "include": ["src"]
25
+ }
package/connect.d.ts DELETED
@@ -1,61 +0,0 @@
1
- type IttySocketEvent<BaseFormat> = BaseFormat extends UseItty ? 'open' | 'close' | 'message' | 'join' | 'leave' : 'open' | 'close' | 'message';
2
- type Date = {
3
- date: Date;
4
- };
5
- type UserDetails = {
6
- uid: string;
7
- alias?: string;
8
- };
9
- type OptionalUserDetails = {
10
- uid?: string;
11
- alias?: string;
12
- };
13
- export type UseItty<MessageType = any> = {
14
- message: MessageType;
15
- } & UserDetails & Date;
16
- export type JoinEvent = {
17
- type: 'join';
18
- users: number;
19
- } & Date & OptionalUserDetails;
20
- export type LeaveEvent = {
21
- type: 'leave';
22
- users: number;
23
- } & Date & OptionalUserDetails;
24
- export type ErrorEvent = {
25
- type: 'error';
26
- message: string;
27
- } & Date;
28
- export type IttySocketOptions = {
29
- as?: string;
30
- alias?: string;
31
- echo?: true;
32
- announce?: true;
33
- };
34
- export interface IttySocketConnect {
35
- <BaseFormat = object>(...args: BaseFormat extends UseItty ? [channelID: string, options?: IttySocketOptions] : [url: string, queryParams?: any]): IttySocket<BaseFormat>;
36
- }
37
- type UseIttyEvents<BaseFormat> = {
38
- on(type: 'join', listener: (event: JoinEvent) => any): IttySocket<BaseFormat>;
39
- on(type: 'leave', listener: (event: LeaveEvent) => any): IttySocket<BaseFormat>;
40
- on(type: 'error', listener: (event: ErrorEvent) => any): IttySocket<BaseFormat>;
41
- };
42
- type SendMessage<BaseFormat> = BaseFormat extends UseItty ? <MessageFormat = any>(message: MessageFormat, uid?: string) => IttySocket<BaseFormat> : <MessageFormat = any>(message: MessageFormat) => IttySocket<BaseFormat>;
43
- export type IttySocket<BaseFormat = object> = {
44
- open: () => IttySocket<BaseFormat>;
45
- close: () => IttySocket<BaseFormat>;
46
- send: SendMessage<BaseFormat>;
47
- push: SendMessage<BaseFormat>;
48
- remove(type: IttySocketEvent<BaseFormat>, listener: () => any): IttySocket<BaseFormat>;
49
- remove(type: string, listener: () => any): IttySocket<BaseFormat>;
50
- on(type: 'open', listener: () => any): IttySocket<BaseFormat>;
51
- on(type: 'close', listener: () => any): IttySocket<BaseFormat>;
52
- on<MessageFormat = BaseFormat>(type: 'message', listener: (event: BaseFormat & MessageFormat) => any): IttySocket<BaseFormat>;
53
- on<MessageFormat = BaseFormat>(type: string, listener: (event: BaseFormat & MessageFormat & {
54
- type: string;
55
- }) => any): IttySocket<BaseFormat>;
56
- on<MessageFormat = BaseFormat>(type: (event?: any) => any, listener: (event: BaseFormat & MessageFormat & {
57
- type: string;
58
- }) => any): IttySocket<BaseFormat>;
59
- } & (BaseFormat extends UseItty ? UseIttyEvents<BaseFormat> : object);
60
- export declare let connect: IttySocketConnect;
61
- export {};
package/connect.js DELETED
@@ -1 +0,0 @@
1
- "use strict";exports.connect=(e,s={})=>{let n,t=0,p=[],a=[],o={},c=()=>(n||(n=new WebSocket((/^wss?:/.test(e)?e:"wss://itty.ws/c/"+e)+"?"+new URLSearchParams(s)),n.onmessage=(e,s=JSON.parse(e.data),n=s?.message,t={...null==n?.[0]&&n,...s})=>{o[s?.type??n?.type]?.map(e=>e(t)),s?.type||o.message?.map(e=>e(t)),a.map(([e,s])=>e(t)&&s(t))},n.onopen=()=>(p.splice(0).map(e=>n?.send(e)),o.open?.map(e=>e()),t&&n?.close()),n.onclose=()=>(t=0,n=null,o.close?.map(e=>e()))),l),l=new Proxy(c,{get:(e,s)=>({open:c,close:()=>(1==n?.readyState?n.close():t=1,l),push:(e,s)=>(t=1,l.send(e,s)),send:(e,s)=>(e=JSON.stringify(e),e=s?""+s+""+e:e,1==n?.readyState?(n.send(e),l):(p.push(e),c())),on:(e,s)=>(s&&(e?.[0]?(o[e]??=[]).push(s):a.push([e,s])),c()),remove:(e,s,n=o[e],t=n?.indexOf(s)??-1)=>(~t&&n?.splice(t,1),c())}[s])});return l};
package/connect.mjs DELETED
@@ -1 +0,0 @@
1
- let e=(e,s={})=>{let p,a=0,n=[],t=[],o={},l=()=>(p||(p=new WebSocket((/^wss?:/.test(e)?e:"wss://itty.ws/c/"+e)+"?"+new URLSearchParams(s)),p.onmessage=(e,s=JSON.parse(e.data),p=s?.message,a={...null==p?.[0]&&p,...s})=>{o[s?.type??p?.type]?.map(e=>e(a)),s?.type||o.message?.map(e=>e(a)),t.map(([e,s])=>e(a)&&s(a))},p.onopen=()=>(n.splice(0).map(e=>p?.send(e)),o.open?.map(e=>e()),a&&p?.close()),p.onclose=()=>(a=0,p=null,o.close?.map(e=>e()))),m),m=new Proxy(l,{get:(e,s)=>({open:l,close:()=>(1==p?.readyState?p.close():a=1,m),push:(e,s)=>(a=1,m.send(e,s)),send:(e,s)=>(e=JSON.stringify(e),e=s?""+s+""+e:e,1==p?.readyState?(p.send(e),m):(n.push(e),l())),on:(e,s)=>(s&&(e?.[0]?(o[e]??=[]).push(s):t.push([e,s])),l()),remove:(e,s,p=o[e],a=p?.indexOf(s)??-1)=>(~a&&p?.splice(a,1),l())}[s])});return m};export{e as connect};