Skip to content

Commit

Permalink
add disconnect button and implement username validation in chat example
Browse files Browse the repository at this point in the history
  • Loading branch information
milyin committed Dec 8, 2024
1 parent bd64c3f commit 2974897
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 10 deletions.
1 change: 1 addition & 0 deletions zenoh-ts/examples/chat/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<label for="username">Username:</label>
<input type="text" id="username" value="Jean Dupont">
<button id="connect-button">Connect</button>
<button id="disconnect-button">Disconnect</button>
</div>
<div class="main-panel">
<div class="user-list">
Expand Down
2 changes: 0 additions & 2 deletions zenoh-ts/examples/chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,11 @@
"start": "http-server ../.. -c-1 -o examples/chat/dist/index.html"
},
"devDependencies": {
"@types/crypto-js": "^4.2.2",
"http-server": "^14.1.1",
"jspm": "^3.3.3",
"typescript": "^5.7.2"
},
"dependencies": {
"crypto-js": "^4.2.0",
"@eclipse-zenoh/zenoh-ts": "file:../.."
}
}
86 changes: 78 additions & 8 deletions zenoh-ts/examples/chat/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,66 @@
import { Config, Session, Queryable, Query } from '@eclipse-zenoh/zenoh-ts';
import CryptoJS from 'crypto-js';
import { Config, Session, Queryable, Query, Liveliness, LivelinessToken, Reply, Sample, RecvErr, Receiver, KeyExpr } from '@eclipse-zenoh/zenoh-ts';
import { Duration } from 'typed-duration';
import { validate_keyexpr } from './validate_keyexpr';

function validate_username(username: string): boolean {
return /^[a-zA-Z0-9_-]+$/.test(username) && validate_keyexpr(username);
}

class ChatSession {
session: Session;
token: LivelinessToken;
queryable: Queryable;

constructor(session: Session, queryable: Queryable) {
constructor(session: Session, queryable: Queryable, token: LivelinessToken) {
this.session = session;
this.queryable = queryable;
this.token = token;
}

public static async connect(serverName: string, serverPort: string, username: string): Promise<ChatSession> {
if (!validate_username(username)) {
return Promise.reject(`Invalid username: ${username}`);
}

let locator = `ws/${serverName}:${serverPort}`;
let config = new Config(locator);
log(`Connecting to zenohd on ${locator}`);
let session = await Session.open(config);
log(`Connected to zenohd on ${locator}`);
let user_id = CryptoJS.MD5(username).toString();
let queryable_keyexpr = `user/${user_id}`;
let queriable = await session.declare_queryable(queryable_keyexpr, {
let queryable_keyexpr = `user/${username}`;
let queryable = await session.declare_queryable(queryable_keyexpr, {
callback: (query: Query) => {
log(`Replying to query: ${query.selector().toString()}`);
query.reply(queryable_keyexpr, username);
},
complete: true
});
log(`Created queryable on ${queryable_keyexpr}`);
return new ChatSession(session, queriable)

let token = session.liveliness().declare_token(queryable_keyexpr);

let receiver = await session.liveliness().get("user/*", {
timeout: Duration.seconds.of(20)
}) as Receiver;

let reply = await receiver.receive();
while (reply != RecvErr.Disconnected) {
if (reply instanceof Reply) {
let resp = reply.result();
if (resp instanceof Sample) {
let sample: Sample = resp;
let keyexpr = sample.keyexpr();
log(`Alive token from ${keyexpr}`);
}
}
reply = await receiver.receive();
}

return new ChatSession(session, queryable, token);
}

async disconnect() {
await this.session.close();
}
}

Expand All @@ -36,18 +70,43 @@ document.addEventListener('DOMContentLoaded', () => {
const toggleLogButton = document.getElementById('toggle-log-button');
const technicalLogPanel = document.getElementById('technical-log-panel');
const connectButton = document.getElementById('connect-button');
const disconnectButton = document.getElementById('disconnect-button');
const serverNameInput = document.getElementById('server-name') as HTMLInputElement;
const serverPortInput = document.getElementById('server-port') as HTMLInputElement;
const usernameInput = document.getElementById('username') as HTMLInputElement;

const adjectives = [
'adorable', 'beautiful', 'clean', 'drab', 'elegant', 'fancy', 'glamorous', 'handsome', 'long', 'magnificent',
'old-fashioned', 'plain', 'quaint', 'sparkling', 'ugliest', 'unsightly', 'angry', 'bewildered', 'clumsy', 'defeated',
'embarrassed', 'fierce', 'grumpy', 'helpless', 'itchy', 'jealous', 'lazy', 'mysterious', 'nervous', 'obnoxious'
];
const animals = [
'ant', 'bear', 'cat', 'dog', 'elephant', 'frog', 'giraffe', 'horse', 'iguana', 'jaguar', 'kangaroo', 'lion', 'monkey',
'newt', 'owl', 'penguin', 'quail', 'rabbit', 'snake', 'tiger', 'unicorn', 'vulture', 'walrus', 'xerus', 'yak', 'zebra'
];
let randomUsername = `${adjectives[Math.floor(Math.random() * adjectives.length)]}-${animals[Math.floor(Math.random() * animals.length)]}`;
usernameInput.value = randomUsername;


toggleLogButton?.addEventListener('click', () => {
if (technicalLogPanel) {
technicalLogPanel.classList.toggle('hidden');
}
});

connectButton?.addEventListener('click', () => {
connect(serverNameInput.value, serverPortInput.value, usernameInput.value);
log_catch(() => connect(serverNameInput.value, serverPortInput.value, usernameInput.value));
});

disconnectButton?.addEventListener('click', () => {
if (chatSession) {
log_catch(async () => {
if (chatSession) {
await chatSession.disconnect();
chatSession = null;
}
});
}
});
});

Expand All @@ -59,6 +118,17 @@ function log(message: string) {
technicalLog?.appendChild(logMessage);
}

async function log_catch(asyncFunc: () => Promise<void>) {
try {
await asyncFunc();
} catch (error) {
log(`Error: ${error}`);
}
}

async function connect(serverName: string, serverPort: string, username: string) {
if (chatSession) {
await chatSession.disconnect();
}
chatSession = await ChatSession.connect(serverName, serverPort, username);
}
63 changes: 63 additions & 0 deletions zenoh-ts/examples/chat/src/validate_keyexpr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// copied from https://github.com/eclipse-zenoh/zenoh/blob/release/1.0.4/commons/zenoh-keyexpr/src/key_expr/borrowed.rs#L615
export function validate_keyexpr(value: string): boolean {
const forbiddenChars = ['#', '?', '$'];
let inBigWild = false;

const chunks = value.split('/');
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
if (chunk === '') {
console.error(`Invalid Key Expr "${value}": empty chunks are forbidden, as well as leading and trailing slashes`);
return false;
}
if (chunk === '$*') {
console.error(`Invalid Key Expr "${value}": lone $*s must be replaced by * to reach canon-form`);
return false;
}
if (inBigWild) {
if (chunk === '**') {
console.error(`Invalid Key Expr "${value}": **/** must be replaced by ** to reach canon-form`);
return false;
}
if (chunk === '*') {
console.error(`Invalid Key Expr "${value}": **/* must be replaced by */** to reach canon-form`);
return false;
}
}
if (chunk === '**') {
inBigWild = true;
} else {
inBigWild = false;
if (chunk !== '*') {
const split = chunk.split('*');
split.pop();
if (split.some(s => !s.endsWith('$'))) {
console.error(`Invalid Key Expr "${value}": * and ** may only be preceded and followed by /`);
return false;
}
}
}
}

for (let i = 0; i < value.length; i++) {
const char = value[i];
if (forbiddenChars.includes(char)) {
if (char === '$') {
if (value[i + 1] === '*') {
if (value[i + 2] === '$') {
console.error(`Invalid Key Expr "${value}": $ is not allowed after $*`);
return false;
}
} else {
console.error(`Invalid Key Expr "${value}": $ is only allowed in $*`);
return false;
}
} else {
console.error(`Invalid Key Expr "${value}": # and ? are forbidden characters`);
return false;
}
}
}

return true;
}
5 changes: 5 additions & 0 deletions zenoh-ts/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,11 @@ export class Session {
return subscriber;
}

/**
* Obtain a Liveliness struct tied to this Zenoh Session.
*
* @returns Liveliness
*/
liveliness() : Liveliness {
return new Liveliness(this.remote_session)
}
Expand Down

0 comments on commit 2974897

Please sign in to comment.