repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | /** |
| #2 | * Imperial WebSocket Client |
| #3 | * |
| #4 | * Two persistent connections: |
| #5 | * /ws — wallet-scoped invalidation (positions_updated, orders_updated) |
| #6 | * /ws/market — public market data stream (funding, marks, depth) |
| #7 | * |
| #8 | * Typed event emitter with automatic ping/pong keepalive and exponential-backoff |
| #9 | * reconnect. Works in both Node (ws package) and browser (native WebSocket). |
| #10 | */ |
| #11 | export function createMarketCache() { |
| #12 | return { |
| #13 | funding: new Map(), |
| #14 | markPrices: new Map(), |
| #15 | depth: new Map(), |
| #16 | lastUpdated: 0, |
| #17 | }; |
| #18 | } |
| #19 | // ─── Simple typed event emitter ─────────────────────────────────────────────── |
| #20 | class TypedEmitter { |
| #21 | listeners = {}; |
| #22 | on(event, listener) { |
| #23 | if (!this.listeners[event]) |
| #24 | this.listeners[event] = []; |
| #25 | this.listeners[event].push(listener); |
| #26 | return this; |
| #27 | } |
| #28 | off(event, listener) { |
| #29 | const arr = this.listeners[event]; |
| #30 | if (arr) { |
| #31 | const idx = arr.indexOf(listener); |
| #32 | if (idx >= 0) |
| #33 | arr.splice(idx, 1); |
| #34 | } |
| #35 | return this; |
| #36 | } |
| #37 | emit(event, data) { |
| #38 | const arr = this.listeners[event]; |
| #39 | if (arr) { |
| #40 | for (const fn of arr) |
| #41 | fn(data); |
| #42 | } |
| #43 | } |
| #44 | } |
| #45 | const WS_OPEN = 1; |
| #46 | async function openWs(url) { |
| #47 | // Try native global WebSocket first (browser / Bun) |
| #48 | const NativeWs = globalThis.WebSocket; |
| #49 | if (NativeWs) |
| #50 | return new NativeWs(url); |
| #51 | // Fall back to 'ws' package in Node |
| #52 | try { |
| #53 | const { WebSocket: NodeWs } = await import("ws"); |
| #54 | return new NodeWs(url); |
| #55 | } |
| #56 | catch { |
| #57 | throw new Error("No WebSocket implementation available. Install the 'ws' package."); |
| #58 | } |
| #59 | } |
| #60 | // ─── ImperialMarketWs ───────────────────────────────────────────────────────── |
| #61 | export class ImperialMarketWs extends TypedEmitter { |
| #62 | wsUrl; |
| #63 | subscribeSymbols; |
| #64 | ws = null; |
| #65 | stopped = false; |
| #66 | reconnectDelay = 1000; |
| #67 | pingTimer = null; |
| #68 | cache = createMarketCache(); |
| #69 | constructor(wsUrl, subscribeSymbols) { |
| #70 | super(); |
| #71 | this.wsUrl = wsUrl; |
| #72 | this.subscribeSymbols = subscribeSymbols; |
| #73 | } |
| #74 | async connect() { |
| #75 | if (this.stopped) |
| #76 | return; |
| #77 | try { |
| #78 | this.ws = await openWs(this.wsUrl); |
| #79 | } |
| #80 | catch (err) { |
| #81 | this.emit("error", err instanceof Error ? err : new Error(String(err))); |
| #82 | this.scheduleReconnect(); |
| #83 | return; |
| #84 | } |
| #85 | this.ws.onopen = () => { |
| #86 | this.reconnectDelay = 1000; |
| #87 | this.emit("connected", undefined); |
| #88 | this.subscribe(); |
| #89 | this.startPing(); |
| #90 | }; |
| #91 | this.ws.onmessage = (ev) => { |
| #92 | try { |
| #93 | const msg = JSON.parse(ev.data); |
| #94 | this.handleMessage(msg); |
| #95 | } |
| #96 | catch { |
| #97 | // ignore parse errors |
| #98 | } |
| #99 | }; |
| #100 | this.ws.onerror = (ev) => { |
| #101 | const err = ev instanceof Error ? ev : new Error("WebSocket error"); |
| #102 | this.emit("error", err); |
| #103 | }; |
| #104 | this.ws.onclose = (ev) => { |
| #105 | this.stopPing(); |
| #106 | this.emit("disconnected", { code: ev.code, reason: String(ev.reason ?? "") }); |
| #107 | if (!this.stopped) |
| #108 | this.scheduleReconnect(); |
| #109 | }; |
| #110 | } |
| #111 | subscribe() { |
| #112 | if (!this.ws || this.ws.readyState !== WS_OPEN) |
| #113 | return; |
| #114 | this.ws.send(JSON.stringify({ type: "subscribe_funding_rates" })); |
| #115 | this.ws.send(JSON.stringify({ type: "subscribe_mark_prices" })); |
| #116 | this.ws.send(JSON.stringify({ |
| #117 | type: "subscribe_phoenix_depth", |
| #118 | ...(this.subscribeSymbols ? { symbols: this.subscribeSymbols } : {}), |
| #119 | })); |
| #120 | } |
| #121 | handleMessage(msg) { |
| #122 | switch (msg.type) { |
| #123 | case "funding_rate_update": { |
| #124 | const key = `${msg.symbol}:${msg.venue}`; |
| #125 | this.cache.funding.set(key, msg); |
| #126 | this.cache.lastUpdated = Date.now(); |
| #127 | this.emit("funding", msg); |
| #128 | break; |
| #129 | } |
| #130 | case "mark_price_update": { |
| #131 | const key = `${msg.symbol}:${msg.venue}`; |
| #132 | this.cache.markPrices.set(key, msg); |
| #133 | this.cache.lastUpdated = Date.now(); |
| #134 | this.emit("mark", msg); |
| #135 | break; |
| #136 | } |
| #137 | case "phoenix_depth_update": { |
| #138 | this.cache.depth.set(msg.symbol, msg.snapshot); |
| #139 | this.cache.lastUpdated = Date.now(); |
| #140 | this.emit("depth", msg); |
| #141 | break; |
| #142 | } |
| #143 | } |
| #144 | } |
| #145 | startPing() { |
| #146 | this.pingTimer = setInterval(() => { |
| #147 | if (this.ws?.readyState === WS_OPEN) { |
| #148 | this.ws.send(JSON.stringify({ type: "ping" })); |
| #149 | } |
| #150 | }, 20_000); |
| #151 | } |
| #152 | stopPing() { |
| #153 | if (this.pingTimer) { |
| #154 | clearInterval(this.pingTimer); |
| #155 | this.pingTimer = null; |
| #156 | } |
| #157 | } |
| #158 | scheduleReconnect() { |
| #159 | const delay = this.reconnectDelay; |
| #160 | this.reconnectDelay = Math.min(delay * 2, 30_000); |
| #161 | setTimeout(() => this.connect(), delay); |
| #162 | } |
| #163 | /** Get latest mark price for a symbol from cache. */ |
| #164 | getMarkPrice(symbol, venue = "phoenix") { |
| #165 | return this.cache.markPrices.get(`${symbol}:${venue}`)?.price ?? null; |
| #166 | } |
| #167 | /** Get latest funding for a symbol+venue from cache. */ |
| #168 | getFunding(symbol, venue = "phoenix") { |
| #169 | return this.cache.funding.get(`${symbol}:${venue}`) ?? null; |
| #170 | } |
| #171 | /** Get latest depth snapshot for a symbol. */ |
| #172 | getDepth(symbol) { |
| #173 | return this.cache.depth.get(symbol) ?? null; |
| #174 | } |
| #175 | /** Best mid price from depth cache. */ |
| #176 | getMid(symbol) { |
| #177 | const d = this.getDepth(symbol); |
| #178 | if (!d?.bids.length || !d?.asks.length) |
| #179 | return null; |
| #180 | const bid = d.bids[0]?.[0] ?? 0; |
| #181 | const ask = d.asks[0]?.[0] ?? 0; |
| #182 | return bid > 0 && ask > 0 ? (bid + ask) / 2 : null; |
| #183 | } |
| #184 | disconnect() { |
| #185 | this.stopped = true; |
| #186 | this.stopPing(); |
| #187 | this.ws?.close(); |
| #188 | this.ws = null; |
| #189 | } |
| #190 | } |
| #191 | export class ImperialWalletWs extends TypedEmitter { |
| #192 | wsUrl; |
| #193 | wallet; |
| #194 | ws = null; |
| #195 | stopped = false; |
| #196 | reconnectDelay = 1000; |
| #197 | pingTimer = null; |
| #198 | constructor(wsUrl, wallet) { |
| #199 | super(); |
| #200 | this.wsUrl = wsUrl; |
| #201 | this.wallet = wallet; |
| #202 | } |
| #203 | async connect() { |
| #204 | if (this.stopped) |
| #205 | return; |
| #206 | try { |
| #207 | this.ws = await openWs(this.wsUrl); |
| #208 | } |
| #209 | catch (err) { |
| #210 | this.emit("error", err instanceof Error ? err : new Error(String(err))); |
| #211 | this.scheduleReconnect(); |
| #212 | return; |
| #213 | } |
| #214 | this.ws.onopen = () => { |
| #215 | this.reconnectDelay = 1000; |
| #216 | this.emit("connected", undefined); |
| #217 | this.ws?.send(JSON.stringify({ type: "subscribe", wallet: this.wallet })); |
| #218 | this.pingTimer = setInterval(() => { |
| #219 | if (this.ws?.readyState === WS_OPEN) { |
| #220 | this.ws.send(JSON.stringify({ type: "ping" })); |
| #221 | } |
| #222 | }, 20_000); |
| #223 | }; |
| #224 | this.ws.onmessage = (ev) => { |
| #225 | try { |
| #226 | const msg = JSON.parse(ev.data); |
| #227 | if (msg.type === "positions_updated") |
| #228 | this.emit("positions_updated", undefined); |
| #229 | if (msg.type === "orders_updated") |
| #230 | this.emit("orders_updated", undefined); |
| #231 | } |
| #232 | catch { |
| #233 | // ignore |
| #234 | } |
| #235 | }; |
| #236 | this.ws.onerror = (ev) => { |
| #237 | this.emit("error", ev instanceof Error ? ev : new Error("WS error")); |
| #238 | }; |
| #239 | this.ws.onclose = (ev) => { |
| #240 | if (this.pingTimer) { |
| #241 | clearInterval(this.pingTimer); |
| #242 | this.pingTimer = null; |
| #243 | } |
| #244 | this.emit("disconnected", { code: ev.code, reason: String(ev.reason ?? "") }); |
| #245 | if (!this.stopped) |
| #246 | this.scheduleReconnect(); |
| #247 | }; |
| #248 | } |
| #249 | scheduleReconnect() { |
| #250 | const delay = this.reconnectDelay; |
| #251 | this.reconnectDelay = Math.min(delay * 2, 30_000); |
| #252 | setTimeout(() => this.connect(), delay); |
| #253 | } |
| #254 | disconnect() { |
| #255 | this.stopped = true; |
| #256 | if (this.pingTimer) { |
| #257 | clearInterval(this.pingTimer); |
| #258 | this.pingTimer = null; |
| #259 | } |
| #260 | this.ws?.close(); |
| #261 | this.ws = null; |
| #262 | } |
| #263 | } |
| #264 | // ─── Factory ────────────────────────────────────────────────────────────────── |
| #265 | export function createImperialMarketWs(base, symbols) { |
| #266 | const wsBase = base.replace(/^http/, "ws").replace(/\/api\/v1\/?$/, ""); |
| #267 | return new ImperialMarketWs(`${wsBase}/ws/market`, symbols); |
| #268 | } |
| #269 | export function createImperialWalletWs(base, wallet) { |
| #270 | const wsBase = base.replace(/^http/, "ws").replace(/\/api\/v1\/?$/, ""); |
| #271 | return new ImperialWalletWs(`${wsBase}/ws`, wallet); |
| #272 | } |
| #273 | //# sourceMappingURL=imperialWs.js.map |