Skip to content

Commit 97718c2

Browse files
committed
Ladies and gentlemen, introducing hulypulse-client and Typing
1 parent 3b14b36 commit 97718c2

File tree

19 files changed

+760
-46
lines changed

19 files changed

+760
-46
lines changed

common/config/rush/pnpm-lock.yaml

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dev/docker-compose.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,10 +533,10 @@ services:
533533
redis:
534534
condition: service_started
535535
ports:
536-
- 8095:8095
536+
- 8099:8099
537537
environment:
538538
- HULY_REDIS_URLS=redis://redis:6379
539-
- HULY_BIND_PORT=8095
539+
- HULY_BIND_PORT=8099
540540
restart: unless-stopped
541541

542542
process-service:

packages/hulypulse-client/README.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# HulypulseClient
2+
3+
A TypeScript/Node.js client for the Hulypulse WebSocket server.
4+
Supports automatic reconnection, request–response correlation, `get` / `put` / `delete`, and subscriptions.
5+
6+
---
7+
8+
### Main Methods
9+
10+
## put(key: string, data: string, TTL?: number): Promise<boolean>
11+
12+
Stores a value under a key.
13+
14+
TTL (optional) — time-to-live in seconds.
15+
16+
Resolves with true if the operation succeeded.
17+
18+
await client.put("workspace/users/123", "Alice", 60) → true
19+
20+
## get(key: string): Promise<any | false>
21+
22+
Retrieves the value for a key.
23+
24+
Resolves with the value if found.
25+
Resolves with false if the key does not exist.
26+
27+
const value = await client.get("workspace/users/123")
28+
if (value) {
29+
console.log("User data:", value)
30+
} else {
31+
console.log("User not found")
32+
}
33+
34+
## get_full(key: string): Promise<{data, etag, expires_at} | false>
35+
36+
Retrieves the full record:
37+
38+
data — stored value,
39+
etag — data identifier,
40+
expires_at — expiration in seconds.
41+
42+
const full = await client.get_full("workspace/users/123")
43+
if (full) {
44+
console.log(full.data, full.etag, full.expires_at)
45+
}
46+
47+
## delete(key: string): Promise<boolean>
48+
49+
Deletes a key.
50+
51+
Resolves with true if the key was deleted.
52+
Resolves with false if the key was not found.
53+
54+
const deleted = await client.delete("workspace/users/123")
55+
console.log(deleted ? "Deleted" : "Not found")
56+
57+
## subscribe(key: string, callback: (msg, key, index) => void): Promise<boolean>
58+
59+
Subscribes to updates for a key (or prefix).
60+
61+
The callback is invoked on every event: Set, Del, Expired
62+
63+
Resolves with true if a new subscription was created.
64+
Resolves with false if the callback was already subscribed.
65+
66+
const cb = (msg, key, index) => {
67+
if( msg.message === 'Expired' ) console.log(`${msg.key} was expired`)
68+
}
69+
70+
await client.subscribe("workspace/users/", cb)
71+
// Now cb will be called when any key starting with "workspace/users/" changes
72+
73+
## unsubscribe(key: string, callback: Callback): Promise<boolean>
74+
75+
Unsubscribes a specific callback.
76+
77+
Resolves with true if the callback was removed (and if it was the last one, the server gets an unsub message).
78+
Resolves with false if the callback was not found.
79+
80+
await client.unsubscribe("workspace/users/", cb)
81+
82+
## send(message: any): Promise<any>
83+
84+
Low-level method to send a raw message.
85+
86+
Automatically attaches a correlation id.
87+
Resolves when a response with the same correlation is received.
88+
89+
const reply = await client.send({ type: "get", key: "workspace/users/123" })
90+
console.log("Raw reply:", reply)
91+
92+
## Reconnection
93+
94+
If the connection drops, the client automatically reconnects.
95+
All active subscriptions are re-sent to the server after reconnect.
96+
97+
## Closing
98+
99+
The client supports both manual closing and the new using syntax (TypeScript 5.2+).
100+
101+
client[Symbol.dispose]() // closes the connection
102+
103+
or, if needed internally:
104+
105+
(client as any).close()
106+
107+
---
108+
109+
## Usage Example
110+
111+
```ts
112+
import { HulypulseClient } from "./hulypulse_client.js"
113+
114+
async function main() {
115+
// connect
116+
const client = await HulypulseClient.connect("wss://hulypulse_mem.lleo.me/ws")
117+
118+
// subscribe to updates
119+
const cb = (msg, key, index) => {
120+
console.log("Update for", key, ":", msg)
121+
}
122+
await client.subscribe("workspace/users/", cb)
123+
124+
// put value
125+
await client.put("workspace/users/123", JSON.stringify({ name: "Alice" }), 5)
126+
127+
// get value
128+
const value = await client.get("workspace/users/123")
129+
console.log("Fetched:", value)
130+
131+
// get full record
132+
const full = await client.get_full("workspace/users/123")
133+
if (full) {
134+
console.log(full.data, full.etag, full.expires_at)
135+
}
136+
137+
// delete key
138+
const deleted = await client.delete("workspace/users/123")
139+
console.log(deleted ? "Deleted" : "Not found")
140+
141+
// unsubscribe
142+
await client.unsubscribe("workspace/users/", cb)
143+
144+
// low-level send
145+
const reply = await client.send({ type: "sublist" })
146+
console.log("My sublists:", reply)
147+
148+
// dispose
149+
client[Symbol.dispose]()
150+
}
151+
152+
main()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = {
2+
preset: 'ts-jest',
3+
testEnvironment: 'node',
4+
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
5+
roots: ["./src"],
6+
coverageReporters: ["text-summary", "html"]
7+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Set up fetch mock
2+
require('jest-fetch-mock').enableMocks()
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"name": "@hcengineering/hulypulse-client",
3+
"version": "0.1.0",
4+
"main": "lib/index.js",
5+
"types": "types/index.d.ts",
6+
"files": [
7+
"lib/**/*",
8+
"types/**/*",
9+
"tsconfig.json"
10+
],
11+
"scripts": {
12+
"build": "compile",
13+
"build:watch": "compile",
14+
"format": "format src",
15+
"test": "jest --passWithNoTests --silent",
16+
"_phase:build": "compile transpile src",
17+
"_phase:test": "jest --passWithNoTests --silent",
18+
"_phase:format": "format src",
19+
"_phase:validate": "compile validate"
20+
},
21+
"dependencies": {
22+
"@hcengineering/core": "^0.6.32",
23+
"@hcengineering/platform": "^0.6.11"
24+
},
25+
"devDependencies": {
26+
"cross-env": "~7.0.3",
27+
"@hcengineering/platform-rig": "^0.6.0",
28+
"@types/node": "^22.15.29",
29+
"@typescript-eslint/eslint-plugin": "^6.11.0",
30+
"eslint-plugin-import": "^2.26.0",
31+
"eslint-plugin-promise": "^6.1.1",
32+
"eslint-plugin-n": "^15.4.0",
33+
"eslint": "^8.54.0",
34+
"esbuild": "^0.24.2",
35+
"@typescript-eslint/parser": "^6.11.0",
36+
"eslint-config-standard-with-typescript": "^40.0.0",
37+
"prettier": "^3.1.0",
38+
"typescript": "^5.8.3",
39+
"jest": "^29.7.0",
40+
"jest-fetch-mock": "^3.0.3",
41+
"ts-jest": "^29.1.1",
42+
"@types/jest": "^29.5.5"
43+
},
44+
"exports": {
45+
".": {
46+
"types": "./types/index.d.ts",
47+
"require": "./lib/index.js",
48+
"import": "./lib/index.js"
49+
}
50+
}
51+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { HulypulseClient } from "../client";
2+
3+
jest.setTimeout(120000);
4+
5+
let client: any;
6+
7+
beforeAll(async () => {
8+
client = await HulypulseClient.connect("ws://localhost:8095/ws");
9+
// client = await HulypulseClient.connect("wss://hulypulse_mem.lleo.me/ws");
10+
expect(client).toBeInstanceOf(HulypulseClient);
11+
},1000);
12+
13+
afterAll(() => {
14+
client.close();
15+
},1000);
16+
17+
test("Put", async () => {
18+
expect( await client.put("test/online/123", "MY",200) ).toEqual(true);
19+
},500);
20+
21+
test("Get", async () => {
22+
expect( await client.get("test/online/123") ).toEqual("MY");
23+
},500);
24+
25+
test("Delete", async () => {
26+
expect( await client.delete("test/online/123") ).toEqual(true);
27+
expect( await client.delete("test/online/123") ).toEqual(false);
28+
expect( await client.get("test/online/123") ).toEqual(false);
29+
},500);
30+
31+
test("Subscribe", async () => {
32+
let r: any;
33+
let cb = function(msg: any, key: string, index: number) {
34+
r = { ...msg, ...{key2:key,index} };
35+
}
36+
expect( await client.subscribe("test/online/", cb) ).toEqual(true);
37+
expect( await client.subscribe("test/online/", cb) ).toEqual(false);
38+
expect( await client.put("test/online/123", "Two",1) ).toEqual(true);
39+
expect(r.message).toEqual("Set");
40+
expect(r.key).toEqual("test/online/123");
41+
expect(r.value).toEqual("Two");
42+
expect(r.key2).toEqual("test/online/");
43+
expect(r.index).toEqual(0);
44+
},1000);
45+
46+
test("Expired", async () => {
47+
expect( await client.get("test/online/123") ).toEqual("Two");
48+
await new Promise((resolve) => setTimeout(resolve, 1000))
49+
expect( await client.get("test/online/123") ).toEqual(false);
50+
},1500);

0 commit comments

Comments
 (0)