Skip to content

Commit 6c548db

Browse files
committed
[ws] Simple server
1 parent df05907 commit 6c548db

File tree

8 files changed

+306
-0
lines changed

8 files changed

+306
-0
lines changed

gulpfile.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const TEST_MODULES = [
3333
'synchronizers/synchronizer-local',
3434
'synchronizers/synchronizer-ws-client',
3535
'synchronizers/synchronizer-ws-server',
36+
'synchronizers/synchronizer-ws-server-simple',
3637
'synchronizers/synchronizer-broadcast-channel',
3738
];
3839
const ALL_MODULES = [

site/build.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ const addApi = (docs: Docs): Docs =>
180180
.addApiFile('dist/@types/synchronizers/synchronizer-local/index.d.ts')
181181
.addApiFile('dist/@types/synchronizers/synchronizer-ws-client/index.d.ts')
182182
.addApiFile('dist/@types/synchronizers/synchronizer-ws-server/index.d.ts')
183+
.addApiFile(
184+
'dist/@types/synchronizers/synchronizer-ws-server-simple/index.d.ts',
185+
)
183186
.addApiFile(
184187
'dist/@types/synchronizers/synchronizer-broadcast-channel/index.d.ts',
185188
)
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* The synchronizer-ws-server-simple module of the TinyBase project lets you
3+
* create a server that facilitates synchronization between clients, without the
4+
* complications of listeners, persistence, or statistics.
5+
*
6+
* This makes it more suitable to be used as a reference implementation for
7+
* other server environments.
8+
* @see Synchronization guide
9+
* @see Todo App v6 (collaboration) demo
10+
* @packageDocumentation
11+
* @module synchronizer-ws-server-simple
12+
* @since v5.4.0
13+
*/
14+
/// synchronizer-ws-server-simple
15+
16+
/**
17+
* The WsServerSimple interface represents an object that facilitates
18+
* synchronization between clients that are using WsSynchronizer instances.
19+
*
20+
* The core functionality is equivalent to the WsServer interface, but without
21+
* the complications of listeners, persistence, or statistics.
22+
*
23+
* You should use the createWsServerSimple function to create a WsServerSimple
24+
* object.
25+
* @category Server
26+
* @since v5.4.0
27+
*/
28+
/// WsServerSimple
29+
{
30+
/**
31+
* The getWebSocketServer method returns a reference to the WebSocketServer
32+
* being used for this WsServerSimple.
33+
* @returns The WebSocketServer reference.
34+
* @example
35+
* This example creates a WsServerSimple and then gets the WebSocketServer
36+
* reference back out again.
37+
*
38+
* ```js
39+
* import {WebSocketServer} from 'ws';
40+
* import {createWsServerSimple} from 'tinybase/synchronizers/synchronizer-ws-server-simple';
41+
*
42+
* const webSocketServer = new WebSocketServer({port: 8053});
43+
* const server = createWsServerSimple(webSocketServer);
44+
*
45+
* console.log(server.getWebSocketServer() == webSocketServer);
46+
* // -> true
47+
*
48+
* server.destroy();
49+
* ```
50+
* @category Getter
51+
* @since v5.4.0
52+
*/
53+
/// WsServerSimple.getWebSocketServer
54+
/**
55+
* The destroy method provides a way to clean up the server at the end of its
56+
* use.
57+
*
58+
* This closes the underlying WebSocketServer that was provided when the
59+
* WsServerSimple was created.
60+
* @example
61+
* This example creates a WsServerSimple and then destroys it again, closing
62+
* the underlying WebSocketServer.
63+
*
64+
* ```js
65+
* import {WebSocketServer} from 'ws';
66+
* import {createWsServerSimple} from 'tinybase/synchronizers/synchronizer-ws-server-simple';
67+
*
68+
* const webSocketServer = new WebSocketServer({port: 8053});
69+
* webSocketServer.on('close', () => {
70+
* console.log('WebSocketServer closed');
71+
* });
72+
* const server = createWsServerSimple(webSocketServer);
73+
*
74+
* server.destroy();
75+
* // ...
76+
* // -> 'WebSocketServer closed'
77+
* ```
78+
* @category Getter
79+
* @since v5.4.0
80+
*/
81+
/// WsServerSimple.destroy
82+
}
83+
/**
84+
* The createWsServerSimple function creates a WsServerSimple that facilitates
85+
* synchronization between clients that are using WsSynchronizer instances.
86+
*
87+
* This should be run in a server environment, and you must pass in a configured
88+
* WebSocketServer object in order to create it.
89+
*
90+
* The core functionality is equivalent to the WsServer interface, but without
91+
* the complications of listeners, persistence, or statistics. This makes it
92+
* more suitable to be used as a reference implementation for other server
93+
* environments.
94+
* @param webSocketServer A WebSocketServer object from your server environment.
95+
* @returns A reference to the new WsServerSimple object.
96+
* @example
97+
* This example creates a WsServerSimple that synchronizes two clients on a
98+
* shared path.
99+
*
100+
* ```js
101+
* import {WebSocketServer} from 'ws';
102+
* import {createMergeableStore} from 'tinybase';
103+
* import {createWsServerSimple} from 'tinybase/synchronizers/synchronizer-ws-server-simple';
104+
* import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';
105+
*
106+
* // Server
107+
* const server = createWsServerSimple(new WebSocketServer({port: 8053}));
108+
*
109+
* // Client 1
110+
* const clientStore1 = createMergeableStore();
111+
* clientStore1.setCell('pets', 'fido', 'species', 'dog');
112+
* const synchronizer1 = await createWsSynchronizer(
113+
* clientStore1,
114+
* new WebSocket('ws://localhost:8053/petShop'),
115+
* );
116+
* await synchronizer1.startSync();
117+
* // ...
118+
*
119+
* // Client 2
120+
* const clientStore2 = createMergeableStore();
121+
* clientStore2.setCell('pets', 'felix', 'species', 'cat');
122+
* const synchronizer2 = await createWsSynchronizer(
123+
* clientStore2,
124+
* new WebSocket('ws://localhost:8053/petShop'),
125+
* );
126+
* await synchronizer2.startSync();
127+
* // ...
128+
*
129+
* console.log(clientStore1.getTables());
130+
* // -> {pets: {fido: {species: 'dog'}, felix: {species: 'cat'}}}
131+
*
132+
* console.log(clientStore2.getTables());
133+
* // -> {pets: {fido: {species: 'dog'}, felix: {species: 'cat'}}}
134+
*
135+
* synchronizer1.destroy();
136+
* synchronizer2.destroy();
137+
* server.destroy();
138+
* ```
139+
* @category Creation
140+
* @since v5.4.0
141+
*/
142+
/// createWsServerSimple
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// synchronizer-ws-server-simple
2+
3+
import type {WebSocketServer} from 'ws';
4+
5+
/// WsServerSimple
6+
export interface WsServerSimple {
7+
/// WsServerSimple.getWebSocketServer
8+
getWebSocketServer(): WebSocketServer;
9+
/// WsServerSimple.destroy
10+
destroy(): void;
11+
}
12+
13+
/// createWsServerSimple
14+
export function createWsServerSimple(
15+
webSocketServer: WebSocketServer,
16+
): WsServerSimple;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// synchronizer-ws-server-simple
2+
3+
import type {WebSocketServer} from 'ws';
4+
5+
/// WsServerSimple
6+
export interface WsServerSimple {
7+
/// WsServerSimple.getWebSocketServer
8+
getWebSocketServer(): WebSocketServer;
9+
/// WsServerSimple.destroy
10+
destroy(): void;
11+
}
12+
13+
/// createWsServerSimple
14+
export function createWsServerSimple(
15+
webSocketServer: WebSocketServer,
16+
): WsServerSimple;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {EMPTY_STRING, UTF8, strMatch} from '../../common/strings.ts';
2+
import {
3+
IdMap2,
4+
mapEnsure,
5+
mapForEach,
6+
mapGet,
7+
mapNew,
8+
mapSet,
9+
} from '../../common/map.ts';
10+
import {WebSocket, WebSocketServer} from 'ws';
11+
import type {
12+
WsServerSimple,
13+
createWsServerSimple as createWsServerSimpleDecl,
14+
} from '../../@types/synchronizers/synchronizer-ws-server-simple/index.js';
15+
import {collClear, collDel, collIsEmpty} from '../../common/coll.ts';
16+
import {createRawPayload, ifPayloadValid} from '../common.ts';
17+
import type {Id} from '../../@types/common/index.js';
18+
import {ifNotUndefined} from '../../common/other.ts';
19+
import {objFreeze} from '../../common/obj.ts';
20+
21+
const PATH_REGEX = /\/([^?]*)/;
22+
23+
export const createWsServerSimple = ((webSocketServer: WebSocketServer) => {
24+
const clientsByPath: IdMap2<WebSocket> = mapNew();
25+
26+
webSocketServer.on('connection', (webSocket, request) =>
27+
ifNotUndefined(strMatch(request.url, PATH_REGEX), ([, pathId]) =>
28+
ifNotUndefined(request.headers['sec-websocket-key'], async (clientId) => {
29+
const clients = mapEnsure(clientsByPath, pathId, mapNew<Id, WebSocket>);
30+
mapSet(clients, clientId, webSocket);
31+
32+
webSocket.on('message', (data) => {
33+
const payload = data.toString(UTF8);
34+
ifPayloadValid(payload, (toClientId, remainder) => {
35+
const forwardedPayload = createRawPayload(clientId, remainder);
36+
if (toClientId === EMPTY_STRING) {
37+
mapForEach(clients, (otherClientId, otherWebSocket) =>
38+
otherClientId !== clientId
39+
? otherWebSocket.send(forwardedPayload)
40+
: 0,
41+
);
42+
} else {
43+
mapGet(clients, toClientId)?.send(forwardedPayload);
44+
}
45+
});
46+
});
47+
48+
webSocket.on('close', () => {
49+
collDel(clients, clientId);
50+
if (collIsEmpty(clients)) {
51+
collDel(clientsByPath, pathId);
52+
}
53+
});
54+
}),
55+
),
56+
);
57+
58+
const getWebSocketServer = () => webSocketServer;
59+
60+
const destroy = () => {
61+
collClear(clientsByPath);
62+
webSocketServer.close();
63+
};
64+
65+
const wsServerSimple = {
66+
getWebSocketServer,
67+
destroy,
68+
};
69+
70+
return objFreeze(wsServerSimple as WsServerSimple);
71+
}) as typeof createWsServerSimpleDecl;

test/unit/core/documentation.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import * as TinyBaseSynchronizerBroadcastChannel from 'tinybase/synchronizers/sy
2222
import * as TinyBaseSynchronizerLocal from 'tinybase/synchronizers/synchronizer-local';
2323
import * as TinyBaseSynchronizerWsClient from 'tinybase/synchronizers/synchronizer-ws-client';
2424
import * as TinyBaseSynchronizerWsServer from 'tinybase/synchronizers/synchronizer-ws-server';
25+
import * as TinyBaseSynchronizerWsServerSimple from 'tinybase/synchronizers/synchronizer-ws-server-simple';
2526
import * as TinyBaseSynchronizers from 'tinybase/synchronizers';
2627
import * as TinyBaseTools from 'tinybase/tools';
2728
import * as TinyBaseUiReact from 'tinybase/ui-react';
@@ -86,6 +87,8 @@ import {transformSync} from 'esbuild';
8687
'tinybase/synchronizers/synchronizer-local': TinyBaseSynchronizerLocal,
8788
'tinybase/synchronizers/synchronizer-ws-client': TinyBaseSynchronizerWsClient,
8889
'tinybase/synchronizers/synchronizer-ws-server': TinyBaseSynchronizerWsServer,
90+
'tinybase/synchronizers/synchronizer-ws-server-simple':
91+
TinyBaseSynchronizerWsServerSimple,
8992
'tinybase/synchronizers/synchronizer-broadcast-channel':
9093
TinyBaseSynchronizerBroadcastChannel,
9194
'tinybase/tools': TinyBaseTools,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/* eslint-disable jest/no-conditional-expect */
2+
3+
import {WebSocket, WebSocketServer} from 'ws';
4+
import {createMergeableStore} from 'tinybase';
5+
import {createWsServerSimple} from 'tinybase/synchronizers/synchronizer-ws-server-simple';
6+
import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';
7+
import {pause} from '../common/other.ts';
8+
import {resetHlc} from '../common/mergeable.ts';
9+
10+
beforeEach(() => {
11+
resetHlc();
12+
});
13+
14+
test('Basics', async () => {
15+
const wsServerSimple = createWsServerSimple(
16+
new WebSocketServer({port: 8054}),
17+
);
18+
19+
const s1 = createMergeableStore('s1');
20+
const synchronizer1 = await createWsSynchronizer(
21+
s1,
22+
new WebSocket('ws://localhost:8054'),
23+
);
24+
await synchronizer1.startSync();
25+
s1.setCell('t1', 'r1', 'c1', 4);
26+
27+
const s2 = createMergeableStore('s2');
28+
const synchronizer2 = await createWsSynchronizer(
29+
s2,
30+
new WebSocket('ws://localhost:8054'),
31+
);
32+
await synchronizer2.startSync();
33+
s2.setCell('t1', 'r2', 'price', 5);
34+
35+
await pause();
36+
37+
expect(s1.getTables()).toEqual({
38+
t1: {r2: {price: 5}, r1: {c1: 4}},
39+
});
40+
expect(s2.getTables()).toEqual({
41+
t1: {r2: {price: 5}, r1: {c1: 4}},
42+
});
43+
44+
synchronizer1.destroy();
45+
synchronizer2.destroy();
46+
wsServerSimple.destroy();
47+
});
48+
49+
test('Accessors', async () => {
50+
const wssServer = new WebSocketServer({port: 8054});
51+
const wsServerSimple = createWsServerSimple(wssServer);
52+
expect(wsServerSimple.getWebSocketServer()).toEqual(wssServer);
53+
wsServerSimple.destroy();
54+
});

0 commit comments

Comments
 (0)