@motiadev/stream-client-browser 0.2.1-beta.47
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.
- package/LICENSE +21 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/src/stream-group.d.ts +16 -0
- package/dist/src/stream-group.js +58 -0
- package/dist/src/stream-item.d.ts +16 -0
- package/dist/src/stream-item.js +46 -0
- package/dist/src/stream-subscription.d.ts +9 -0
- package/dist/src/stream-subscription.js +20 -0
- package/dist/src/stream.d.ts +13 -0
- package/dist/src/stream.js +20 -0
- package/dist/src/stream.types.d.ts +65 -0
- package/dist/src/stream.types.js +1 -0
- package/index.ts +4 -0
- package/package.json +21 -0
- package/src/stream-group.ts +68 -0
- package/src/stream-item.ts +56 -0
- package/src/stream-subscription.ts +30 -0
- package/src/stream.ts +26 -0
- package/src/stream.types.ts +20 -0
- package/tsconfig.json +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Motia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { StreamSubscription } from './stream-subscription';
|
|
2
|
+
import { GroupJoinMessage, Listener } from './stream.types';
|
|
3
|
+
export declare class StreamGroupSubscription<TData extends {
|
|
4
|
+
id: string;
|
|
5
|
+
}> extends StreamSubscription {
|
|
6
|
+
private readonly ws;
|
|
7
|
+
private readonly sub;
|
|
8
|
+
private onChangeListeners;
|
|
9
|
+
private listeners;
|
|
10
|
+
private state;
|
|
11
|
+
constructor(ws: WebSocket, sub: GroupJoinMessage);
|
|
12
|
+
close(): void;
|
|
13
|
+
addChangeListener(listener: Listener<TData[]>): void;
|
|
14
|
+
removeChangeListener(listener: Listener<TData[]>): void;
|
|
15
|
+
getState(): TData[];
|
|
16
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { StreamSubscription } from './stream-subscription';
|
|
2
|
+
export class StreamGroupSubscription extends StreamSubscription {
|
|
3
|
+
constructor(ws, sub) {
|
|
4
|
+
super();
|
|
5
|
+
this.ws = ws;
|
|
6
|
+
this.sub = sub;
|
|
7
|
+
this.onChangeListeners = new Set();
|
|
8
|
+
this.listeners = new Set();
|
|
9
|
+
this.state = [];
|
|
10
|
+
const message = { type: 'join', data: sub };
|
|
11
|
+
const listenerWrapper = (event) => {
|
|
12
|
+
const message = JSON.parse(event.data);
|
|
13
|
+
const isStreamName = message.streamName === this.sub.streamName;
|
|
14
|
+
const isGroupId = 'groupId' in message && message.groupId === this.sub.groupId;
|
|
15
|
+
if (isStreamName && isGroupId) {
|
|
16
|
+
if (message.event.type === 'sync') {
|
|
17
|
+
this.state = message.event.data;
|
|
18
|
+
}
|
|
19
|
+
else if (message.event.type === 'create') {
|
|
20
|
+
const id = message.event.data.id;
|
|
21
|
+
if (!this.state.find((item) => item.id === id)) {
|
|
22
|
+
this.state = [...this.state, message.event.data];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else if (message.event.type === 'update') {
|
|
26
|
+
const messageData = message.event.data;
|
|
27
|
+
const messageDataId = messageData.id;
|
|
28
|
+
this.state = this.state.map((item) => (item.id === messageDataId ? messageData : item));
|
|
29
|
+
}
|
|
30
|
+
else if (message.event.type === 'delete') {
|
|
31
|
+
const messageDataId = message.event.data.id;
|
|
32
|
+
this.state = this.state.filter((item) => item.id !== messageDataId);
|
|
33
|
+
}
|
|
34
|
+
else if (message.event.type === 'event') {
|
|
35
|
+
this.onEventReceived(message.event.event);
|
|
36
|
+
}
|
|
37
|
+
this.onChangeListeners.forEach((listener) => listener(this.state));
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
this.ws.addEventListener('message', listenerWrapper);
|
|
41
|
+
this.listeners.add(listenerWrapper);
|
|
42
|
+
ws.send(JSON.stringify(message));
|
|
43
|
+
}
|
|
44
|
+
close() {
|
|
45
|
+
const message = { type: 'leave', data: this.sub };
|
|
46
|
+
this.ws.send(JSON.stringify(message));
|
|
47
|
+
this.listeners.forEach((listener) => this.ws.removeEventListener('message', listener));
|
|
48
|
+
}
|
|
49
|
+
addChangeListener(listener) {
|
|
50
|
+
this.onChangeListeners.add(listener);
|
|
51
|
+
}
|
|
52
|
+
removeChangeListener(listener) {
|
|
53
|
+
this.onChangeListeners.delete(listener);
|
|
54
|
+
}
|
|
55
|
+
getState() {
|
|
56
|
+
return this.state;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { StreamSubscription } from './stream-subscription';
|
|
2
|
+
import { ItemJoinMessage, Listener } from './stream.types';
|
|
3
|
+
export declare class StreamItemSubscription<TData extends {
|
|
4
|
+
id: string;
|
|
5
|
+
}> extends StreamSubscription {
|
|
6
|
+
private readonly ws;
|
|
7
|
+
private readonly sub;
|
|
8
|
+
private onChangeListeners;
|
|
9
|
+
private listeners;
|
|
10
|
+
private state;
|
|
11
|
+
constructor(ws: WebSocket, sub: ItemJoinMessage);
|
|
12
|
+
close(): void;
|
|
13
|
+
addChangeListener(listener: Listener<TData>): void;
|
|
14
|
+
removeChangeListener(listener: Listener<TData>): void;
|
|
15
|
+
getState(): TData | null;
|
|
16
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { StreamSubscription } from './stream-subscription';
|
|
2
|
+
export class StreamItemSubscription extends StreamSubscription {
|
|
3
|
+
constructor(ws, sub) {
|
|
4
|
+
super();
|
|
5
|
+
this.ws = ws;
|
|
6
|
+
this.sub = sub;
|
|
7
|
+
this.onChangeListeners = new Set();
|
|
8
|
+
this.listeners = new Set();
|
|
9
|
+
this.state = null;
|
|
10
|
+
const message = { type: 'join', data: sub };
|
|
11
|
+
const listenerWrapper = (event) => {
|
|
12
|
+
const message = JSON.parse(event.data);
|
|
13
|
+
const isStreamName = message.streamName === this.sub.streamName;
|
|
14
|
+
const isId = 'id' in message && message.id === this.sub.id;
|
|
15
|
+
if (isStreamName && isId) {
|
|
16
|
+
if (message.event.type === 'sync' || message.event.type === 'create' || message.event.type === 'update') {
|
|
17
|
+
this.state = message.event.data;
|
|
18
|
+
}
|
|
19
|
+
else if (message.event.type === 'delete') {
|
|
20
|
+
this.state = null;
|
|
21
|
+
}
|
|
22
|
+
else if (message.event.type === 'event') {
|
|
23
|
+
this.onEventReceived(message.event.event);
|
|
24
|
+
}
|
|
25
|
+
this.onChangeListeners.forEach((listener) => listener(this.state));
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
this.ws.addEventListener('message', listenerWrapper);
|
|
29
|
+
this.listeners.add(listenerWrapper);
|
|
30
|
+
ws.send(JSON.stringify(message));
|
|
31
|
+
}
|
|
32
|
+
close() {
|
|
33
|
+
const message = { type: 'leave', data: this.sub };
|
|
34
|
+
this.ws.send(JSON.stringify(message));
|
|
35
|
+
this.listeners.forEach((listener) => this.ws.removeEventListener('message', listener));
|
|
36
|
+
}
|
|
37
|
+
addChangeListener(listener) {
|
|
38
|
+
this.onChangeListeners.add(listener);
|
|
39
|
+
}
|
|
40
|
+
removeChangeListener(listener) {
|
|
41
|
+
this.onChangeListeners.delete(listener);
|
|
42
|
+
}
|
|
43
|
+
getState() {
|
|
44
|
+
return this.state;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CustomEvent } from './stream.types';
|
|
2
|
+
type CustomEventListener = (event: any) => void;
|
|
3
|
+
export declare abstract class StreamSubscription {
|
|
4
|
+
private customEventListeners;
|
|
5
|
+
protected onEventReceived(event: CustomEvent): void;
|
|
6
|
+
onEvent(type: string, listener: CustomEventListener): void;
|
|
7
|
+
offEvent(type: string, listener: CustomEventListener): void;
|
|
8
|
+
}
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class StreamSubscription {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.customEventListeners = new Map();
|
|
4
|
+
}
|
|
5
|
+
onEventReceived(event) {
|
|
6
|
+
const customEventListeners = this.customEventListeners.get(event.type);
|
|
7
|
+
if (customEventListeners) {
|
|
8
|
+
const eventData = event.data;
|
|
9
|
+
customEventListeners.forEach((listener) => listener(eventData));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
onEvent(type, listener) {
|
|
13
|
+
const listeners = this.customEventListeners.get(type) || [];
|
|
14
|
+
this.customEventListeners.set(type, [...listeners, listener]);
|
|
15
|
+
}
|
|
16
|
+
offEvent(type, listener) {
|
|
17
|
+
const listeners = this.customEventListeners.get(type) || [];
|
|
18
|
+
this.customEventListeners.set(type, listeners.filter((l) => l !== listener));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { StreamGroupSubscription } from './stream-group';
|
|
2
|
+
import { StreamItemSubscription } from './stream-item';
|
|
3
|
+
export declare class Stream {
|
|
4
|
+
private ws;
|
|
5
|
+
constructor(address: string, onReady: () => void);
|
|
6
|
+
subscribeItem<TData extends {
|
|
7
|
+
id: string;
|
|
8
|
+
}>(streamName: string, id: string): StreamItemSubscription<TData>;
|
|
9
|
+
subscribeGroup<TData extends {
|
|
10
|
+
id: string;
|
|
11
|
+
}>(streamName: string, groupId: string): StreamGroupSubscription<TData>;
|
|
12
|
+
close(): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import { StreamGroupSubscription } from './stream-group';
|
|
3
|
+
import { StreamItemSubscription } from './stream-item';
|
|
4
|
+
export class Stream {
|
|
5
|
+
constructor(address, onReady) {
|
|
6
|
+
this.ws = new WebSocket(address);
|
|
7
|
+
this.ws.onopen = () => onReady();
|
|
8
|
+
}
|
|
9
|
+
subscribeItem(streamName, id) {
|
|
10
|
+
const subscriptionId = uuidv4();
|
|
11
|
+
return new StreamItemSubscription(this.ws, { streamName, id, subscriptionId });
|
|
12
|
+
}
|
|
13
|
+
subscribeGroup(streamName, groupId) {
|
|
14
|
+
const subscriptionId = uuidv4();
|
|
15
|
+
return new StreamGroupSubscription(this.ws, { streamName, groupId, subscriptionId });
|
|
16
|
+
}
|
|
17
|
+
close() {
|
|
18
|
+
this.ws.close();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export type BaseMessage = {
|
|
2
|
+
streamName: string;
|
|
3
|
+
} & ({
|
|
4
|
+
id: string;
|
|
5
|
+
} | {
|
|
6
|
+
groupId: string;
|
|
7
|
+
});
|
|
8
|
+
export type JoinMessage = BaseMessage & {
|
|
9
|
+
subscriptionId: string;
|
|
10
|
+
};
|
|
11
|
+
export type ItemJoinMessage = BaseMessage & {
|
|
12
|
+
id: string;
|
|
13
|
+
subscriptionId: string;
|
|
14
|
+
};
|
|
15
|
+
export type GroupJoinMessage = BaseMessage & {
|
|
16
|
+
groupId: string;
|
|
17
|
+
subscriptionId: string;
|
|
18
|
+
};
|
|
19
|
+
export type CustomEvent = {
|
|
20
|
+
type: string;
|
|
21
|
+
data: any;
|
|
22
|
+
};
|
|
23
|
+
export type StreamEvent<TData extends {
|
|
24
|
+
id: string;
|
|
25
|
+
}> = {
|
|
26
|
+
type: 'create';
|
|
27
|
+
data: TData;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'update';
|
|
30
|
+
data: TData;
|
|
31
|
+
} | {
|
|
32
|
+
type: 'delete';
|
|
33
|
+
data: TData;
|
|
34
|
+
} | {
|
|
35
|
+
type: 'event';
|
|
36
|
+
event: CustomEvent;
|
|
37
|
+
};
|
|
38
|
+
export type ItemStreamEvent<TData extends {
|
|
39
|
+
id: string;
|
|
40
|
+
}> = StreamEvent<TData> | {
|
|
41
|
+
type: 'sync';
|
|
42
|
+
data: TData;
|
|
43
|
+
};
|
|
44
|
+
export type GroupStreamEvent<TData extends {
|
|
45
|
+
id: string;
|
|
46
|
+
}> = StreamEvent<TData> | {
|
|
47
|
+
type: 'sync';
|
|
48
|
+
data: TData[];
|
|
49
|
+
};
|
|
50
|
+
export type ItemEventMessage<TData extends {
|
|
51
|
+
id: string;
|
|
52
|
+
}> = BaseMessage & {
|
|
53
|
+
event: ItemStreamEvent<TData>;
|
|
54
|
+
};
|
|
55
|
+
export type GroupEventMessage<TData extends {
|
|
56
|
+
id: string;
|
|
57
|
+
}> = BaseMessage & {
|
|
58
|
+
event: GroupStreamEvent<TData>;
|
|
59
|
+
};
|
|
60
|
+
export type Message = {
|
|
61
|
+
type: 'join' | 'leave';
|
|
62
|
+
data: JoinMessage;
|
|
63
|
+
};
|
|
64
|
+
export type Listener<TData> = (state: TData | null) => void;
|
|
65
|
+
export type CustomEventListener<TData> = (event: TData) => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@motiadev/stream-client-browser",
|
|
3
|
+
"description": "Motia Stream Client Package – Responsible for managing streams of data.",
|
|
4
|
+
"version": "0.2.1-beta.47",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"uuid": "^11.1.0"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/jest": "^29.5.14",
|
|
12
|
+
"@types/ws": "^8.18.1",
|
|
13
|
+
"jest": "^29.7.0",
|
|
14
|
+
"ts-jest": "^29.2.5",
|
|
15
|
+
"typescript": "^5.7.2"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "rm -rf dist && tsc",
|
|
19
|
+
"lint": "eslint --config ../../eslint.config.js"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { StreamSubscription } from './stream-subscription'
|
|
2
|
+
import { GroupEventMessage, GroupJoinMessage, Listener, Message } from './stream.types'
|
|
3
|
+
|
|
4
|
+
export class StreamGroupSubscription<TData extends { id: string }> extends StreamSubscription {
|
|
5
|
+
private onChangeListeners: Set<Listener<TData[]>> = new Set()
|
|
6
|
+
private listeners: Set<EventListener> = new Set()
|
|
7
|
+
|
|
8
|
+
private state: TData[] = []
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly ws: WebSocket,
|
|
12
|
+
private readonly sub: GroupJoinMessage,
|
|
13
|
+
) {
|
|
14
|
+
super()
|
|
15
|
+
|
|
16
|
+
const message: Message = { type: 'join', data: sub }
|
|
17
|
+
const listenerWrapper = (event: MessageEvent<string>) => {
|
|
18
|
+
const message: GroupEventMessage<TData> = JSON.parse(event.data)
|
|
19
|
+
const isStreamName = message.streamName === this.sub.streamName
|
|
20
|
+
const isGroupId = 'groupId' in message && message.groupId === this.sub.groupId
|
|
21
|
+
|
|
22
|
+
if (isStreamName && isGroupId) {
|
|
23
|
+
if (message.event.type === 'sync') {
|
|
24
|
+
this.state = message.event.data
|
|
25
|
+
} else if (message.event.type === 'create') {
|
|
26
|
+
const id = message.event.data.id
|
|
27
|
+
|
|
28
|
+
if (!this.state.find((item) => item.id === id)) {
|
|
29
|
+
this.state = [...this.state, message.event.data]
|
|
30
|
+
}
|
|
31
|
+
} else if (message.event.type === 'update') {
|
|
32
|
+
const messageData = message.event.data
|
|
33
|
+
const messageDataId = messageData.id
|
|
34
|
+
this.state = this.state.map((item) => (item.id === messageDataId ? messageData : item))
|
|
35
|
+
} else if (message.event.type === 'delete') {
|
|
36
|
+
const messageDataId = message.event.data.id
|
|
37
|
+
this.state = this.state.filter((item) => item.id !== messageDataId)
|
|
38
|
+
} else if (message.event.type === 'event') {
|
|
39
|
+
this.onEventReceived(message.event.event)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.onChangeListeners.forEach((listener) => listener(this.state))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
this.ws.addEventListener('message', listenerWrapper as EventListener)
|
|
46
|
+
this.listeners.add(listenerWrapper as EventListener)
|
|
47
|
+
|
|
48
|
+
ws.send(JSON.stringify(message))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
close() {
|
|
52
|
+
const message: Message = { type: 'leave', data: this.sub }
|
|
53
|
+
this.ws.send(JSON.stringify(message))
|
|
54
|
+
this.listeners.forEach((listener) => this.ws.removeEventListener('message', listener))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
addChangeListener(listener: Listener<TData[]>) {
|
|
58
|
+
this.onChangeListeners.add(listener)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
removeChangeListener(listener: Listener<TData[]>) {
|
|
62
|
+
this.onChangeListeners.delete(listener)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getState(): TData[] {
|
|
66
|
+
return this.state
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { StreamSubscription } from './stream-subscription'
|
|
2
|
+
import { ItemEventMessage, ItemJoinMessage, Listener, Message } from './stream.types'
|
|
3
|
+
|
|
4
|
+
export class StreamItemSubscription<TData extends { id: string }> extends StreamSubscription {
|
|
5
|
+
private onChangeListeners: Set<Listener<TData>> = new Set()
|
|
6
|
+
private listeners: Set<EventListener> = new Set()
|
|
7
|
+
private state: TData | null = null
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
private readonly ws: WebSocket,
|
|
11
|
+
private readonly sub: ItemJoinMessage,
|
|
12
|
+
) {
|
|
13
|
+
super()
|
|
14
|
+
|
|
15
|
+
const message: Message = { type: 'join', data: sub }
|
|
16
|
+
const listenerWrapper = (event: MessageEvent<string>) => {
|
|
17
|
+
const message: ItemEventMessage<TData> = JSON.parse(event.data)
|
|
18
|
+
const isStreamName = message.streamName === this.sub.streamName
|
|
19
|
+
const isId = 'id' in message && message.id === this.sub.id
|
|
20
|
+
|
|
21
|
+
if (isStreamName && isId) {
|
|
22
|
+
if (message.event.type === 'sync' || message.event.type === 'create' || message.event.type === 'update') {
|
|
23
|
+
this.state = message.event.data
|
|
24
|
+
} else if (message.event.type === 'delete') {
|
|
25
|
+
this.state = null
|
|
26
|
+
} else if (message.event.type === 'event') {
|
|
27
|
+
this.onEventReceived(message.event.event)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.onChangeListeners.forEach((listener) => listener(this.state))
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
this.ws.addEventListener('message', listenerWrapper as EventListener)
|
|
34
|
+
this.listeners.add(listenerWrapper as EventListener)
|
|
35
|
+
|
|
36
|
+
ws.send(JSON.stringify(message))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
close() {
|
|
40
|
+
const message: Message = { type: 'leave', data: this.sub }
|
|
41
|
+
this.ws.send(JSON.stringify(message))
|
|
42
|
+
this.listeners.forEach((listener) => this.ws.removeEventListener('message', listener))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
addChangeListener(listener: Listener<TData>) {
|
|
46
|
+
this.onChangeListeners.add(listener)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
removeChangeListener(listener: Listener<TData>) {
|
|
50
|
+
this.onChangeListeners.delete(listener)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getState(): TData | null {
|
|
54
|
+
return this.state
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { CustomEvent } from './stream.types'
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
type CustomEventListener = (event: any) => void
|
|
5
|
+
|
|
6
|
+
export abstract class StreamSubscription {
|
|
7
|
+
private customEventListeners: Map<string, CustomEventListener[]> = new Map()
|
|
8
|
+
|
|
9
|
+
protected onEventReceived(event: CustomEvent) {
|
|
10
|
+
const customEventListeners = this.customEventListeners.get(event.type)
|
|
11
|
+
|
|
12
|
+
if (customEventListeners) {
|
|
13
|
+
const eventData = event.data
|
|
14
|
+
customEventListeners.forEach((listener) => listener(eventData))
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
onEvent(type: string, listener: CustomEventListener) {
|
|
19
|
+
const listeners = this.customEventListeners.get(type) || []
|
|
20
|
+
this.customEventListeners.set(type, [...listeners, listener])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
offEvent(type: string, listener: CustomEventListener) {
|
|
24
|
+
const listeners = this.customEventListeners.get(type) || []
|
|
25
|
+
this.customEventListeners.set(
|
|
26
|
+
type,
|
|
27
|
+
listeners.filter((l) => l !== listener),
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/stream.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
2
|
+
import { StreamGroupSubscription } from './stream-group'
|
|
3
|
+
import { StreamItemSubscription } from './stream-item'
|
|
4
|
+
|
|
5
|
+
export class Stream {
|
|
6
|
+
private ws: WebSocket
|
|
7
|
+
|
|
8
|
+
constructor(address: string, onReady: () => void) {
|
|
9
|
+
this.ws = new WebSocket(address)
|
|
10
|
+
this.ws.onopen = () => onReady()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
subscribeItem<TData extends { id: string }>(streamName: string, id: string): StreamItemSubscription<TData> {
|
|
14
|
+
const subscriptionId = uuidv4()
|
|
15
|
+
return new StreamItemSubscription<TData>(this.ws, { streamName, id, subscriptionId })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
subscribeGroup<TData extends { id: string }>(streamName: string, groupId: string): StreamGroupSubscription<TData> {
|
|
19
|
+
const subscriptionId = uuidv4()
|
|
20
|
+
return new StreamGroupSubscription<TData>(this.ws, { streamName, groupId, subscriptionId })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
close() {
|
|
24
|
+
this.ws.close()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type BaseMessage = { streamName: string } & ({ id: string } | { groupId: string })
|
|
2
|
+
export type JoinMessage = BaseMessage & { subscriptionId: string }
|
|
3
|
+
export type ItemJoinMessage = BaseMessage & { id: string; subscriptionId: string }
|
|
4
|
+
export type GroupJoinMessage = BaseMessage & { groupId: string; subscriptionId: string }
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
export type CustomEvent = { type: string; data: any }
|
|
7
|
+
export type StreamEvent<TData extends { id: string }> =
|
|
8
|
+
| { type: 'create'; data: TData }
|
|
9
|
+
| { type: 'update'; data: TData }
|
|
10
|
+
| { type: 'delete'; data: TData }
|
|
11
|
+
| { type: 'event'; event: CustomEvent }
|
|
12
|
+
export type ItemStreamEvent<TData extends { id: string }> = StreamEvent<TData> | { type: 'sync'; data: TData }
|
|
13
|
+
export type GroupStreamEvent<TData extends { id: string }> = StreamEvent<TData> | { type: 'sync'; data: TData[] }
|
|
14
|
+
export type ItemEventMessage<TData extends { id: string }> = BaseMessage & { event: ItemStreamEvent<TData> }
|
|
15
|
+
export type GroupEventMessage<TData extends { id: string }> = BaseMessage & { event: GroupStreamEvent<TData> }
|
|
16
|
+
|
|
17
|
+
export type Message = { type: 'join' | 'leave'; data: JoinMessage }
|
|
18
|
+
|
|
19
|
+
export type Listener<TData> = (state: TData | null) => void
|
|
20
|
+
export type CustomEventListener<TData> = (event: TData) => void
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"allowJs": true,
|
|
12
|
+
"outDir": "dist",
|
|
13
|
+
"rootDir": ".",
|
|
14
|
+
"baseUrl": ".",
|
|
15
|
+
"declaration": true,
|
|
16
|
+
"lib": ["dom"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/*.ts", "index.ts"],
|
|
19
|
+
"exclude": [
|
|
20
|
+
"node_modules",
|
|
21
|
+
"dist"
|
|
22
|
+
]
|
|
23
|
+
}
|