@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 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.
@@ -0,0 +1,4 @@
1
+ export { Stream } from './src/stream';
2
+ export { StreamItemSubscription } from './src/stream-item';
3
+ export { StreamGroupSubscription } from './src/stream-group';
4
+ export { StreamSubscription } from './src/stream-subscription';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { Stream } from './src/stream';
2
+ export { StreamItemSubscription } from './src/stream-item';
3
+ export { StreamGroupSubscription } from './src/stream-group';
4
+ export { StreamSubscription } from './src/stream-subscription';
@@ -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
@@ -0,0 +1,4 @@
1
+ export { Stream } from './src/stream'
2
+ export { StreamItemSubscription } from './src/stream-item'
3
+ export { StreamGroupSubscription } from './src/stream-group'
4
+ export { StreamSubscription } from './src/stream-subscription'
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
+ }