Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions common/config/rush/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions dev/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ services:
- EXCLUDED_APPLICATIONS_FOR_ANONYMOUS=["chunter", "notification"]
# - DISABLE_SIGNUP=true
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces
- PULSE_URL=ws://huly.local:8099/ws
restart: unless-stopped
transactor_cockroach:
image: hardcoreeng/transactor
Expand Down Expand Up @@ -533,10 +534,10 @@ services:
redis:
condition: service_started
ports:
- 8095:8095
- 8099:8099
environment:
- HULY_REDIS_URLS=redis://redis:6379
- HULY_BIND_PORT=8095
- HULY_BIND_PORT=8099
restart: unless-stopped

process-service:
Expand Down
1 change: 1 addition & 0 deletions dev/prod/public/config-dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"PUBLIC_SCHEDULE_URL": "https://schedule.hc.engineering",
"CALDAV_SERVER_URL": "https://caldav.hc.engineering",
"BACKUP_URL": "https://front.hc.engineering/api/backup",
"PULSE_URL": "wss://pulse.hc.engineering/ws",
"COMMUNICATION_API_ENABLED": "true",
"FILES_URL": "https://datalake.hc.engineering/blob/:workspace/:blobId/:filename"
}
1 change: 1 addition & 0 deletions dev/prod/public/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@
"EXPORT_URL": "http://huly.local:4009",
"COMMUNICATION_API_ENABLED": "true",
"BACKUP_URL": "http://huly.local:4039/api/backup",
"PULSE_URL": "ws://huly.local:8099/ws",
"EXCLUDED_APPLICATIONS_FOR_ANONYMOUS": "[\"chunter\", \"notification\"]"
}
5 changes: 4 additions & 1 deletion dev/prod/src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@ export interface Config {
MAIL_URL?: string,
COMMUNICATION_API_ENABLED?: string
BILLING_URL?: string,
EXCLUDED_APPLICATIONS_FOR_ANONYMOUS?: string
EXCLUDED_APPLICATIONS_FOR_ANONYMOUS?: string,
PULSE_URL?: string
}

export interface Branding {
Expand Down Expand Up @@ -489,6 +490,8 @@ export async function configurePlatform() {

setMetadata(billingPlugin.metadata.BillingURL, config.BILLING_URL ?? '')

setMetadata(presentation.metadata.PulseUrl, config.PULSE_URL ?? '')

const languages = myBranding.languages
? (myBranding.languages as string).split(',').map((l) => l.trim())
: ['en', 'ru', 'es', 'pt', 'zh', 'fr', 'cs', 'it', 'de', 'ja']
Expand Down
152 changes: 152 additions & 0 deletions packages/hulypulse-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# HulypulseClient

A TypeScript/Node.js client for the Hulypulse WebSocket server.
Supports automatic reconnection, request–response correlation, `get` / `put` / `delete`, and subscriptions.

---

### Main Methods

## put(key: string, data: string, TTL?: number): Promise<boolean>

Stores a value under a key.

TTL (optional) — time-to-live in seconds.

Resolves with true if the operation succeeded.

await client.put("workspace/users/123", "Alice", 60) → true

## get(key: string): Promise<any | false>

Retrieves the value for a key.

Resolves with the value if found.
Resolves with false if the key does not exist.

const value = await client.get("workspace/users/123")
if (value) {
console.log("User data:", value)
} else {
console.log("User not found")
}

## get_full(key: string): Promise<{data, etag, expires_at} | false>

Retrieves the full record:

data — stored value,
etag — data identifier,
expires_at — expiration in seconds.

const full = await client.get_full("workspace/users/123")
if (full) {
console.log(full.data, full.etag, full.expires_at)
}

## delete(key: string): Promise<boolean>

Deletes a key.

Resolves with true if the key was deleted.
Resolves with false if the key was not found.

const deleted = await client.delete("workspace/users/123")
console.log(deleted ? "Deleted" : "Not found")

## subscribe(key: string, callback: (msg, key, index) => void): Promise<boolean>

Subscribes to updates for a key (or prefix).

The callback is invoked on every event: Set, Del, Expired

Resolves with true if a new subscription was created.
Resolves with false if the callback was already subscribed.

const cb = (msg, key, index) => {
if( msg.message === 'Expired' ) console.log(`${msg.key} was expired`)
}

await client.subscribe("workspace/users/", cb)
// Now cb will be called when any key starting with "workspace/users/" changes

## unsubscribe(key: string, callback: Callback): Promise<boolean>

Unsubscribes a specific callback.

Resolves with true if the callback was removed (and if it was the last one, the server gets an unsub message).
Resolves with false if the callback was not found.

await client.unsubscribe("workspace/users/", cb)

## send(message: any): Promise<any>

Low-level method to send a raw message.

Automatically attaches a correlation id.
Resolves when a response with the same correlation is received.

const reply = await client.send({ type: "get", key: "workspace/users/123" })
console.log("Raw reply:", reply)

## Reconnection

If the connection drops, the client automatically reconnects.
All active subscriptions are re-sent to the server after reconnect.

## Closing

The client supports both manual closing and the new using syntax (TypeScript 5.2+).

client[Symbol.dispose]() // closes the connection

or, if needed internally:

(client as any).close()

---

## Usage Example

```ts
import { HulypulseClient } from "./hulypulse_client.js"

async function main() {
// connect
const client = await HulypulseClient.connect("wss://hulypulse_mem.lleo.me/ws")

// subscribe to updates
const cb = (msg, key, index) => {
console.log("Update for", key, ":", msg)
}
await client.subscribe("workspace/users/", cb)

// put value
await client.put("workspace/users/123", JSON.stringify({ name: "Alice" }), 5)

// get value
const value = await client.get("workspace/users/123")
console.log("Fetched:", value)

// get full record
const full = await client.get_full("workspace/users/123")
if (full) {
console.log(full.data, full.etag, full.expires_at)
}

// delete key
const deleted = await client.delete("workspace/users/123")
console.log(deleted ? "Deleted" : "Not found")

// unsubscribe
await client.unsubscribe("workspace/users/", cb)

// low-level send
const reply = await client.send({ type: "sublist" })
console.log("My sublists:", reply)

// dispose
client[Symbol.dispose]()
}

main()
7 changes: 7 additions & 0 deletions packages/hulypulse-client/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
2 changes: 2 additions & 0 deletions packages/hulypulse-client/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Set up fetch mock
require('jest-fetch-mock').enableMocks()
51 changes: 51 additions & 0 deletions packages/hulypulse-client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@hcengineering/hulypulse-client",
"version": "0.1.0",
"main": "lib/index.js",
"types": "types/index.d.ts",
"files": [
"lib/**/*",
"types/**/*",
"tsconfig.json"
],
"scripts": {
"build": "compile",
"build:watch": "compile",
"format": "format src",
"test": "jest --passWithNoTests --silent",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"dependencies": {
"@hcengineering/core": "^0.6.32",
"@hcengineering/platform": "^0.6.11"
},
"devDependencies": {
"cross-env": "~7.0.3",
"@hcengineering/platform-rig": "^0.6.0",
"@types/node": "^22.15.29",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"esbuild": "^0.24.2",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"typescript": "^5.8.3",
"jest": "^29.7.0",
"jest-fetch-mock": "^3.0.3",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5"
},
"exports": {
".": {
"types": "./types/index.d.ts",
"require": "./lib/index.js",
"import": "./lib/index.js"
}
}
}
50 changes: 50 additions & 0 deletions packages/hulypulse-client/src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { HulypulseClient } from '../client'

jest.setTimeout(120000)

let client: any

beforeAll(async () => {
client = await HulypulseClient.connect('ws://localhost:8095/ws')
// client = await HulypulseClient.connect("wss://hulypulse_mem.lleo.me/ws");
expect(client).toBeInstanceOf(HulypulseClient)
}, 1000)

afterAll(() => {
client.close()
}, 1000)

test('Put', async () => {
expect(await client.put('test/online/123', 'MY', 200)).toEqual(true)
}, 500)

test('Get', async () => {
expect(await client.get('test/online/123')).toEqual('MY')
}, 500)

test('Delete', async () => {
expect(await client.delete('test/online/123')).toEqual(true)
expect(await client.delete('test/online/123')).toEqual(false)
expect(await client.get('test/online/123')).toEqual(false)
}, 500)

test('Subscribe', async () => {
let r: any
let cb = function (msg: any, key: string, index: number) {
r = { ...msg, ...{ key2: key, index } }
}
expect(await client.subscribe('test/online/', cb)).toEqual(true)
expect(await client.subscribe('test/online/', cb)).toEqual(false)
expect(await client.put('test/online/123', 'Two', 1)).toEqual(true)
expect(r.message).toEqual('Set')
expect(r.key).toEqual('test/online/123')
expect(r.value).toEqual('Two')
expect(r.key2).toEqual('test/online/')
expect(r.index).toEqual(0)
}, 1000)

test('Expired', async () => {
expect(await client.get('test/online/123')).toEqual('Two')
await new Promise((resolve) => setTimeout(resolve, 1000))
expect(await client.get('test/online/123')).toEqual(false)
}, 1500)
Loading
Loading