@realnation/builder-shared-sdk 1.0.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/events.d.ts +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +1 -3
- package/dist/http.d.ts +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/oss.js +1 -1
- package/dist/runtime/capability.d.ts +45 -0
- package/dist/runtime/capability.d.ts.map +1 -0
- package/dist/runtime/capability.js +59 -0
- package/dist/runtime/connection.d.ts +92 -0
- package/dist/runtime/connection.d.ts.map +1 -0
- package/dist/runtime/connection.js +247 -0
- package/dist/runtime/decode.d.ts +40 -0
- package/dist/runtime/decode.d.ts.map +1 -0
- package/dist/runtime/decode.js +106 -0
- package/dist/runtime/facade.d.ts +165 -0
- package/dist/runtime/facade.d.ts.map +1 -0
- package/dist/runtime/facade.js +354 -0
- package/dist/runtime/index.d.ts +42 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +70 -0
- package/dist/runtime/mock.d.ts +58 -0
- package/dist/runtime/mock.d.ts.map +1 -0
- package/dist/runtime/mock.js +276 -0
- package/dist/runtime/protocol.d.ts +231 -0
- package/dist/runtime/protocol.d.ts.map +1 -0
- package/dist/runtime/protocol.js +19 -0
- package/dist/runtime/vue/index.d.ts +26 -0
- package/dist/runtime/vue/index.d.ts.map +1 -0
- package/dist/runtime/vue/index.js +47 -0
- package/dist/runtime/vue/player.d.ts +49 -0
- package/dist/runtime/vue/player.d.ts.map +1 -0
- package/dist/runtime/vue/player.js +95 -0
- package/package.json +24 -3
package/README.md
CHANGED
package/dist/events.d.ts
CHANGED
package/dist/events.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE7D,KAAK,QAAQ,CAAC,QAAQ,SAAS,YAAY,IAAI,CAAC,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;AAI3E,qBAAa,WAAW;IACtB,OAAO,CAAC,SAAS,CAAwD;IAEzE,IAAI,CAAC,QAAQ,SAAS,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI;IAMhF,EAAE,CAAC,QAAQ,SAAS,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,CAAC,QAAQ,CAAC,GAAG,MAAM,IAAI;IAO9F,GAAG,CAAC,QAAQ,SAAS,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,CAAC,QAAQ,CAAC,GAAG,IAAI;IASzF,KAAK,IAAI,IAAI;CAGd;AAED,wBAAgB,iBAAiB,IAAI,WAAW,CAE/C;AAED,wBAAgB,iBAAiB,IAAI,WAAW,CAU/C"}
|
package/dist/events.js
CHANGED
package/dist/http.d.ts
CHANGED
package/dist/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,OAAc,EAAE,KAAK,aAAa,EAA6D,MAAM,OAAO,CAAC;AAC7G,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,OAAc,EAAE,KAAK,aAAa,EAA6D,MAAM,OAAO,CAAC;AAC7G,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,iBAAkB,SAAQ,cAAc;CAAG;AAE5D,KAAK,aAAa,GAAG,MAAM,MAAM,GAAG,IAAI,CAAC;AAazC,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAEvD;AAED,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAE5C;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,GAAG,IAAI,CAErE;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,aAAa,CAS1E;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,iBAAiB,GAAG,aAAa,CAG7E;AAED,wBAAgB,aAAa,IAAI,aAAa,CAK7C;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,EAAE,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAS7F"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export * from './types';
|
|
2
|
-
export * from './http';
|
|
3
|
-
export * from './events';
|
|
4
|
-
export * from './flow';
|
|
5
|
-
export * from './oss';
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './http.js';
|
|
3
|
+
export * from './events.js';
|
|
4
|
+
export * from './flow.js';
|
|
5
|
+
export * from './oss.js';
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export * from './types';
|
|
2
|
-
export * from './http';
|
|
3
|
-
export * from './events';
|
|
4
|
-
export * from './flow';
|
|
5
|
-
export * from './oss';
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './http.js';
|
|
3
|
+
export * from './events.js';
|
|
4
|
+
export * from './flow.js';
|
|
5
|
+
export * from './oss.js';
|
package/dist/oss.js
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability gating.
|
|
3
|
+
*
|
|
4
|
+
* The SDK deliberately does no matching of its own. The verdict is computed by
|
|
5
|
+
* the runtime, which is the only side that can see whether a device is actually
|
|
6
|
+
* plugged in; a second implementation here would drift from it and could never
|
|
7
|
+
* produce a reason like NO_DEVICE anyway. This file only reshapes `ready` into
|
|
8
|
+
* something a gate screen can render.
|
|
9
|
+
*/
|
|
10
|
+
import type { Capability, CapabilityMiss, ReadyMessage } from './protocol.js';
|
|
11
|
+
export interface ConnectStatus {
|
|
12
|
+
/** Taken straight from ready.ok — not recomputed. */
|
|
13
|
+
ok: boolean;
|
|
14
|
+
capabilities: Capability[];
|
|
15
|
+
/** Non-empty means ok === false. */
|
|
16
|
+
missing: CapabilityMiss[];
|
|
17
|
+
/** Optional capabilities that were not provided. Does not affect ok. */
|
|
18
|
+
unsatisfied: CapabilityMiss[];
|
|
19
|
+
/** Verbatim echo of what we asked for. For troubleshooting. */
|
|
20
|
+
requested: unknown;
|
|
21
|
+
runtimeVersion: string;
|
|
22
|
+
clientId: string;
|
|
23
|
+
plugins: string[];
|
|
24
|
+
}
|
|
25
|
+
export declare function toConnectStatus(ready: ReadyMessage): ConnectStatus;
|
|
26
|
+
/**
|
|
27
|
+
* Operator-facing text for a miss.
|
|
28
|
+
*
|
|
29
|
+
* Centralised here so every experience shows the same wording. The six reasons
|
|
30
|
+
* exist because each needs a different action on site — collapsing them into
|
|
31
|
+
* "device unavailable" would leave an operator with nothing to do.
|
|
32
|
+
*/
|
|
33
|
+
export declare function explainMiss(miss: CapabilityMiss): string;
|
|
34
|
+
/** A ready-to-render gate summary. */
|
|
35
|
+
export declare function describeGate(status: ConnectStatus): {
|
|
36
|
+
ok: boolean;
|
|
37
|
+
title: string;
|
|
38
|
+
items: Array<{
|
|
39
|
+
name: string;
|
|
40
|
+
reason: string;
|
|
41
|
+
text: string;
|
|
42
|
+
blocking: boolean;
|
|
43
|
+
}>;
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=capability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../../src/runtime/capability.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE9E,MAAM,WAAW,aAAa;IAC5B,qDAAqD;IACrD,EAAE,EAAE,OAAO,CAAC;IACZ,YAAY,EAAE,UAAU,EAAE,CAAC;IAC3B,oCAAoC;IACpC,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,wEAAwE;IACxE,WAAW,EAAE,cAAc,EAAE,CAAC;IAC9B,+DAA+D;IAC/D,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,GAAG,aAAa,CAWlE;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAiBxD;AAED,sCAAsC;AACtC,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG;IACnD,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACjF,CAoBA"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function toConnectStatus(ready) {
|
|
2
|
+
return {
|
|
3
|
+
ok: ready.ok,
|
|
4
|
+
capabilities: ready.capabilities ?? [],
|
|
5
|
+
missing: ready.missing ?? [],
|
|
6
|
+
unsatisfied: ready.unsatisfied ?? [],
|
|
7
|
+
requested: ready.requested,
|
|
8
|
+
runtimeVersion: ready.runtimeVersion,
|
|
9
|
+
clientId: ready.clientId,
|
|
10
|
+
plugins: ready.plugins ?? [],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Operator-facing text for a miss.
|
|
15
|
+
*
|
|
16
|
+
* Centralised here so every experience shows the same wording. The six reasons
|
|
17
|
+
* exist because each needs a different action on site — collapsing them into
|
|
18
|
+
* "device unavailable" would leave an operator with nothing to do.
|
|
19
|
+
*/
|
|
20
|
+
export function explainMiss(miss) {
|
|
21
|
+
switch (miss.reason) {
|
|
22
|
+
case 'NO_PLUGIN':
|
|
23
|
+
return `没有插件提供「${miss.name}」能力,请检查 Runtime 的插件目录`;
|
|
24
|
+
case 'NO_DEVICE':
|
|
25
|
+
return `「${miss.name}」所需的设备未连接,请检查 USB 连接与供电`;
|
|
26
|
+
case 'PARAMS':
|
|
27
|
+
return `设备支持「${miss.name}」,但不满足玩法要求的参数`;
|
|
28
|
+
case 'VERSION':
|
|
29
|
+
return `「${miss.name}」的版本与玩法要求不匹配,请升级 Runtime 或插件`;
|
|
30
|
+
case 'CAPABILITY_CONFLICT':
|
|
31
|
+
return `「${miss.name}」与已启用的其它能力冲突,无法同时使用`;
|
|
32
|
+
case 'START_FAILED':
|
|
33
|
+
return `「${miss.name}」的设备启动失败,可能被其它程序占用`;
|
|
34
|
+
default:
|
|
35
|
+
return `「${miss.name}」不可用`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** A ready-to-render gate summary. */
|
|
39
|
+
export function describeGate(status) {
|
|
40
|
+
const items = [
|
|
41
|
+
...status.missing.map((m) => ({
|
|
42
|
+
name: m.name,
|
|
43
|
+
reason: m.reason,
|
|
44
|
+
text: explainMiss(m),
|
|
45
|
+
blocking: true,
|
|
46
|
+
})),
|
|
47
|
+
...status.unsatisfied.map((m) => ({
|
|
48
|
+
name: m.name,
|
|
49
|
+
reason: m.reason,
|
|
50
|
+
text: explainMiss(m),
|
|
51
|
+
blocking: false,
|
|
52
|
+
})),
|
|
53
|
+
];
|
|
54
|
+
return {
|
|
55
|
+
ok: status.ok,
|
|
56
|
+
title: status.ok ? '设备就绪' : '设备未就绪,无法开始',
|
|
57
|
+
items,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket transport: handshake, heartbeat, reconnect.
|
|
3
|
+
*
|
|
4
|
+
* Everything above this file deals in decoded messages and never sees a socket.
|
|
5
|
+
* That boundary is what makes `source: 'mock'` a drop-in replacement — the mock
|
|
6
|
+
* implements this same interface, so nothing downstream can tell the difference.
|
|
7
|
+
*/
|
|
8
|
+
import type { ClientMessage, HelloMessage, ReadyMessage, ServerMessage } from './protocol.js';
|
|
9
|
+
/** Anything that can carry protocol messages: a real socket, or the mock. */
|
|
10
|
+
export interface Transport {
|
|
11
|
+
connect(): Promise<ReadyMessage>;
|
|
12
|
+
send(msg: ClientMessage): void;
|
|
13
|
+
/** Registers the sink for every server message. Returns an unsubscribe fn. */
|
|
14
|
+
onMessage(fn: (msg: ServerMessage) => void): () => void;
|
|
15
|
+
/** Binary frames (QSBN). Separate from onMessage because they are not JSON. */
|
|
16
|
+
onBinary(fn: (data: ArrayBuffer) => void): () => void;
|
|
17
|
+
onStateChange(fn: (state: ConnectionState) => void): () => void;
|
|
18
|
+
close(): void;
|
|
19
|
+
readonly state: ConnectionState;
|
|
20
|
+
}
|
|
21
|
+
export type ConnectionState = 'idle' | 'connecting' | 'ready' | 'reconnecting' | 'closed';
|
|
22
|
+
export interface ConnectionOptions {
|
|
23
|
+
url?: string;
|
|
24
|
+
client?: string;
|
|
25
|
+
sessionId?: string;
|
|
26
|
+
require?: HelloMessage['require'];
|
|
27
|
+
/** Heartbeat period. The client pings; the runtime only ever answers. */
|
|
28
|
+
heartbeatMs?: number;
|
|
29
|
+
/** Reconnect backoff. Capped so a long outage still retries about twice a minute. */
|
|
30
|
+
backoffStartMs?: number;
|
|
31
|
+
backoffMaxMs?: number;
|
|
32
|
+
/** How long to wait for `ready` before treating the attempt as failed. */
|
|
33
|
+
handshakeTimeoutMs?: number;
|
|
34
|
+
/** Injection seam for tests; defaults to globalThis.WebSocket. */
|
|
35
|
+
socketFactory?: (url: string) => WebSocketLike;
|
|
36
|
+
now?: () => number;
|
|
37
|
+
setTimer?: (fn: () => void, ms: number) => unknown;
|
|
38
|
+
clearTimer?: (handle: unknown) => void;
|
|
39
|
+
}
|
|
40
|
+
/** The subset of WebSocket this file uses. Keeps tests free of a DOM shim. */
|
|
41
|
+
export interface WebSocketLike {
|
|
42
|
+
send(data: string): void;
|
|
43
|
+
close(): void;
|
|
44
|
+
binaryType?: string;
|
|
45
|
+
onopen: ((ev?: unknown) => void) | null;
|
|
46
|
+
onclose: ((ev?: unknown) => void) | null;
|
|
47
|
+
onerror: ((ev?: unknown) => void) | null;
|
|
48
|
+
onmessage: ((ev: {
|
|
49
|
+
data: unknown;
|
|
50
|
+
}) => void) | null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Resolves the endpoint. An https page cannot open a ws:// socket — the browser
|
|
54
|
+
* blocks it as mixed content — so the scheme follows the page rather than being
|
|
55
|
+
* hardcoded, and the failure never reaches a user as "connection refused".
|
|
56
|
+
*/
|
|
57
|
+
export declare function defaultUrl(): string;
|
|
58
|
+
export declare class WsConnection implements Transport {
|
|
59
|
+
private readonly opts;
|
|
60
|
+
private socket;
|
|
61
|
+
private messageHandlers;
|
|
62
|
+
private binaryHandlers;
|
|
63
|
+
private stateHandlers;
|
|
64
|
+
private heartbeatHandle;
|
|
65
|
+
private reconnectHandle;
|
|
66
|
+
private handshakeHandle;
|
|
67
|
+
private backoffMs;
|
|
68
|
+
private closedByUser;
|
|
69
|
+
private _state;
|
|
70
|
+
/** Resolves the *first* connect() only. Reconnects are silent to the caller. */
|
|
71
|
+
private pendingHandshake;
|
|
72
|
+
constructor(options?: ConnectionOptions);
|
|
73
|
+
get state(): ConnectionState;
|
|
74
|
+
connect(): Promise<ReadyMessage>;
|
|
75
|
+
private setState;
|
|
76
|
+
private open;
|
|
77
|
+
private receive;
|
|
78
|
+
private startHeartbeat;
|
|
79
|
+
private stopHeartbeat;
|
|
80
|
+
private clearHandshakeTimer;
|
|
81
|
+
private failHandshake;
|
|
82
|
+
private dropSocket;
|
|
83
|
+
private scheduleReconnect;
|
|
84
|
+
/** Current backoff delay, exposed for tests and debug HUDs. */
|
|
85
|
+
get nextBackoffMs(): number;
|
|
86
|
+
send(msg: ClientMessage): void;
|
|
87
|
+
onMessage(fn: (m: ServerMessage) => void): () => void;
|
|
88
|
+
onBinary(fn: (d: ArrayBuffer) => void): () => void;
|
|
89
|
+
onStateChange(fn: (s: ConnectionState) => void): () => void;
|
|
90
|
+
close(): void;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=connection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/runtime/connection.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG9F,6EAA6E;AAC7E,MAAM,WAAW,SAAS;IACxB,OAAO,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC;IACjC,IAAI,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAAC;IAC/B,8EAA8E;IAC9E,SAAS,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACxD,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACtD,aAAa,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAChE,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;CACjC;AAED,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,YAAY,GAAG,OAAO,GAAG,cAAc,GAAG,QAAQ,CAAC;AAE1F,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC;IAClC,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0EAA0E;IAC1E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kEAAkE;IAClE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,aAAa,CAAC;IAC/C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;IACnD,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC;AAED,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,IAAI,IAAI,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACxC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACzC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACzC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;CACrD;AASD;;;;GAIG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAInC;AAED,qBAAa,YAAa,YAAW,SAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,IAAI,CACgD;IACrE,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,eAAe,CAAyC;IAChE,OAAO,CAAC,cAAc,CAAuC;IAC7D,OAAO,CAAC,aAAa,CAA2C;IAChE,OAAO,CAAC,eAAe,CAAiB;IACxC,OAAO,CAAC,eAAe,CAAiB;IACxC,OAAO,CAAC,eAAe,CAAiB;IACxC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,MAAM,CAA2B;IACzC,gFAAgF;IAChF,OAAO,CAAC,gBAAgB,CAGR;gBAEJ,OAAO,GAAE,iBAAsB;IAkB3C,IAAI,KAAK,IAAI,eAAe,CAE3B;IAED,OAAO,IAAI,OAAO,CAAC,YAAY,CAAC;IAQhC,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,IAAI;IAoDZ,OAAO,CAAC,OAAO;IA2Bf,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,UAAU;IAalB,OAAO,CAAC,iBAAiB;IAezB,+DAA+D;IAC/D,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED,IAAI,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI;IAS9B,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,GAAG,MAAM,IAAI;IAKrD,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,IAAI,GAAG,MAAM,IAAI;IAKlD,aAAa,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,eAAe,KAAK,IAAI,GAAG,MAAM,IAAI;IAK3D,KAAK,IAAI,IAAI;CAYd"}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { RUNTIME_URL } from './protocol.js';
|
|
2
|
+
const DEFAULTS = {
|
|
3
|
+
heartbeatMs: 5000,
|
|
4
|
+
backoffStartMs: 500,
|
|
5
|
+
backoffMaxMs: 30000,
|
|
6
|
+
handshakeTimeoutMs: 8000,
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Resolves the endpoint. An https page cannot open a ws:// socket — the browser
|
|
10
|
+
* blocks it as mixed content — so the scheme follows the page rather than being
|
|
11
|
+
* hardcoded, and the failure never reaches a user as "connection refused".
|
|
12
|
+
*/
|
|
13
|
+
export function defaultUrl() {
|
|
14
|
+
const loc = globalThis.location;
|
|
15
|
+
if (loc?.protocol === 'https:')
|
|
16
|
+
return RUNTIME_URL.replace(/^ws:/, 'wss:');
|
|
17
|
+
return RUNTIME_URL;
|
|
18
|
+
}
|
|
19
|
+
export class WsConnection {
|
|
20
|
+
opts;
|
|
21
|
+
socket = null;
|
|
22
|
+
messageHandlers = new Set();
|
|
23
|
+
binaryHandlers = new Set();
|
|
24
|
+
stateHandlers = new Set();
|
|
25
|
+
heartbeatHandle = null;
|
|
26
|
+
reconnectHandle = null;
|
|
27
|
+
handshakeHandle = null;
|
|
28
|
+
backoffMs;
|
|
29
|
+
closedByUser = false;
|
|
30
|
+
_state = 'idle';
|
|
31
|
+
/** Resolves the *first* connect() only. Reconnects are silent to the caller. */
|
|
32
|
+
pendingHandshake = null;
|
|
33
|
+
constructor(options = {}) {
|
|
34
|
+
this.opts = {
|
|
35
|
+
url: options.url ?? defaultUrl(),
|
|
36
|
+
client: options.client ?? 'qiksense-web',
|
|
37
|
+
heartbeatMs: options.heartbeatMs ?? DEFAULTS.heartbeatMs,
|
|
38
|
+
backoffStartMs: options.backoffStartMs ?? DEFAULTS.backoffStartMs,
|
|
39
|
+
backoffMaxMs: options.backoffMaxMs ?? DEFAULTS.backoffMaxMs,
|
|
40
|
+
handshakeTimeoutMs: options.handshakeTimeoutMs ?? DEFAULTS.handshakeTimeoutMs,
|
|
41
|
+
now: options.now ?? (() => Date.now()),
|
|
42
|
+
setTimer: options.setTimer ?? ((fn, ms) => setTimeout(fn, ms)),
|
|
43
|
+
clearTimer: options.clearTimer ?? ((h) => clearTimeout(h)),
|
|
44
|
+
require: options.require,
|
|
45
|
+
sessionId: options.sessionId,
|
|
46
|
+
socketFactory: options.socketFactory,
|
|
47
|
+
};
|
|
48
|
+
this.backoffMs = this.opts.backoffStartMs;
|
|
49
|
+
}
|
|
50
|
+
get state() {
|
|
51
|
+
return this._state;
|
|
52
|
+
}
|
|
53
|
+
connect() {
|
|
54
|
+
this.closedByUser = false;
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
this.pendingHandshake = { resolve, reject };
|
|
57
|
+
this.open();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
setState(next) {
|
|
61
|
+
if (this._state === next)
|
|
62
|
+
return;
|
|
63
|
+
this._state = next;
|
|
64
|
+
for (const fn of this.stateHandlers)
|
|
65
|
+
fn(next);
|
|
66
|
+
}
|
|
67
|
+
open() {
|
|
68
|
+
this.setState(this._state === 'idle' ? 'connecting' : this._state);
|
|
69
|
+
const make = this.opts.socketFactory ??
|
|
70
|
+
((url) => new globalThis.WebSocket(url));
|
|
71
|
+
let sock;
|
|
72
|
+
try {
|
|
73
|
+
sock = make(this.opts.url);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
this.scheduleReconnect();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.socket = sock;
|
|
80
|
+
if ('binaryType' in sock)
|
|
81
|
+
sock.binaryType = 'arraybuffer';
|
|
82
|
+
sock.onopen = () => {
|
|
83
|
+
const hello = {
|
|
84
|
+
type: 'hello',
|
|
85
|
+
client: this.opts.client,
|
|
86
|
+
protocolVersion: 1,
|
|
87
|
+
};
|
|
88
|
+
if (this.opts.sessionId)
|
|
89
|
+
hello.sessionId = this.opts.sessionId;
|
|
90
|
+
if (this.opts.require)
|
|
91
|
+
hello.require = this.opts.require;
|
|
92
|
+
sock.send(JSON.stringify(hello));
|
|
93
|
+
// A socket that opens but never answers `ready` is the nastiest case: it
|
|
94
|
+
// looks alive, so without this the caller waits forever on connect().
|
|
95
|
+
this.handshakeHandle = this.opts.setTimer(() => {
|
|
96
|
+
this.failHandshake(new Error('runtime did not answer hello within timeout'));
|
|
97
|
+
this.dropSocket();
|
|
98
|
+
this.scheduleReconnect();
|
|
99
|
+
}, this.opts.handshakeTimeoutMs);
|
|
100
|
+
};
|
|
101
|
+
sock.onmessage = (ev) => this.receive(ev.data);
|
|
102
|
+
sock.onerror = () => {
|
|
103
|
+
/* onclose always follows; reconnect is handled there so it runs once. */
|
|
104
|
+
};
|
|
105
|
+
sock.onclose = () => {
|
|
106
|
+
this.stopHeartbeat();
|
|
107
|
+
this.clearHandshakeTimer();
|
|
108
|
+
if (this.closedByUser) {
|
|
109
|
+
this.setState('closed');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.scheduleReconnect();
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
receive(data) {
|
|
116
|
+
if (typeof data !== 'string') {
|
|
117
|
+
const buf = toArrayBuffer(data);
|
|
118
|
+
if (buf)
|
|
119
|
+
for (const fn of this.binaryHandlers)
|
|
120
|
+
fn(buf);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let msg;
|
|
124
|
+
try {
|
|
125
|
+
msg = JSON.parse(data);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return; // A malformed frame must not kill the connection.
|
|
129
|
+
}
|
|
130
|
+
if (msg.type === 'ready') {
|
|
131
|
+
this.clearHandshakeTimer();
|
|
132
|
+
this.backoffMs = this.opts.backoffStartMs; // a real handshake resets backoff
|
|
133
|
+
this.setState('ready');
|
|
134
|
+
this.startHeartbeat();
|
|
135
|
+
const pending = this.pendingHandshake;
|
|
136
|
+
this.pendingHandshake = null;
|
|
137
|
+
pending?.resolve(msg);
|
|
138
|
+
}
|
|
139
|
+
for (const fn of this.messageHandlers)
|
|
140
|
+
fn(msg);
|
|
141
|
+
}
|
|
142
|
+
startHeartbeat() {
|
|
143
|
+
this.stopHeartbeat();
|
|
144
|
+
const tick = () => {
|
|
145
|
+
// Client-initiated only. The runtime never sends ping; a client waiting
|
|
146
|
+
// for one would eventually decide a healthy link was dead.
|
|
147
|
+
this.send({ type: 'ping', ts: this.opts.now() });
|
|
148
|
+
this.heartbeatHandle = this.opts.setTimer(tick, this.opts.heartbeatMs);
|
|
149
|
+
};
|
|
150
|
+
this.heartbeatHandle = this.opts.setTimer(tick, this.opts.heartbeatMs);
|
|
151
|
+
}
|
|
152
|
+
stopHeartbeat() {
|
|
153
|
+
if (this.heartbeatHandle !== null) {
|
|
154
|
+
this.opts.clearTimer(this.heartbeatHandle);
|
|
155
|
+
this.heartbeatHandle = null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
clearHandshakeTimer() {
|
|
159
|
+
if (this.handshakeHandle !== null) {
|
|
160
|
+
this.opts.clearTimer(this.handshakeHandle);
|
|
161
|
+
this.handshakeHandle = null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
failHandshake(err) {
|
|
165
|
+
const pending = this.pendingHandshake;
|
|
166
|
+
this.pendingHandshake = null;
|
|
167
|
+
pending?.reject(err);
|
|
168
|
+
}
|
|
169
|
+
dropSocket() {
|
|
170
|
+
const sock = this.socket;
|
|
171
|
+
this.socket = null;
|
|
172
|
+
if (!sock)
|
|
173
|
+
return;
|
|
174
|
+
sock.onopen = sock.onclose = sock.onerror = null;
|
|
175
|
+
sock.onmessage = null;
|
|
176
|
+
try {
|
|
177
|
+
sock.close();
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
/* already gone */
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
scheduleReconnect() {
|
|
184
|
+
if (this.closedByUser)
|
|
185
|
+
return;
|
|
186
|
+
if (this.reconnectHandle !== null)
|
|
187
|
+
return; // one timer at a time
|
|
188
|
+
this.setState('reconnecting');
|
|
189
|
+
const delay = this.backoffMs;
|
|
190
|
+
// Grow before the attempt so the *next* failure waits longer even if this
|
|
191
|
+
// attempt dies before onclose fires.
|
|
192
|
+
this.backoffMs = Math.min(this.backoffMs * 2, this.opts.backoffMaxMs);
|
|
193
|
+
this.reconnectHandle = this.opts.setTimer(() => {
|
|
194
|
+
this.reconnectHandle = null;
|
|
195
|
+
if (this.closedByUser)
|
|
196
|
+
return;
|
|
197
|
+
this.open();
|
|
198
|
+
}, delay);
|
|
199
|
+
}
|
|
200
|
+
/** Current backoff delay, exposed for tests and debug HUDs. */
|
|
201
|
+
get nextBackoffMs() {
|
|
202
|
+
return this.backoffMs;
|
|
203
|
+
}
|
|
204
|
+
send(msg) {
|
|
205
|
+
if (!this.socket)
|
|
206
|
+
return; // dropped while offline; reconnect replays nothing
|
|
207
|
+
try {
|
|
208
|
+
this.socket.send(JSON.stringify(msg));
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
/* socket died mid-send; onclose drives the reconnect */
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
onMessage(fn) {
|
|
215
|
+
this.messageHandlers.add(fn);
|
|
216
|
+
return () => this.messageHandlers.delete(fn);
|
|
217
|
+
}
|
|
218
|
+
onBinary(fn) {
|
|
219
|
+
this.binaryHandlers.add(fn);
|
|
220
|
+
return () => this.binaryHandlers.delete(fn);
|
|
221
|
+
}
|
|
222
|
+
onStateChange(fn) {
|
|
223
|
+
this.stateHandlers.add(fn);
|
|
224
|
+
return () => this.stateHandlers.delete(fn);
|
|
225
|
+
}
|
|
226
|
+
close() {
|
|
227
|
+
this.closedByUser = true;
|
|
228
|
+
this.stopHeartbeat();
|
|
229
|
+
this.clearHandshakeTimer();
|
|
230
|
+
if (this.reconnectHandle !== null) {
|
|
231
|
+
this.opts.clearTimer(this.reconnectHandle);
|
|
232
|
+
this.reconnectHandle = null;
|
|
233
|
+
}
|
|
234
|
+
this.failHandshake(new Error('connection closed before handshake completed'));
|
|
235
|
+
this.dropSocket();
|
|
236
|
+
this.setState('closed');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function toArrayBuffer(data) {
|
|
240
|
+
if (data instanceof ArrayBuffer)
|
|
241
|
+
return data;
|
|
242
|
+
if (ArrayBuffer.isView(data)) {
|
|
243
|
+
const view = data;
|
|
244
|
+
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DepthSilhouette } from './protocol.js';
|
|
2
|
+
/** Decoded occupancy grid: one byte per cell, 0 or 1, row-major. */
|
|
3
|
+
export interface SilhouetteFrame {
|
|
4
|
+
w: number;
|
|
5
|
+
h: number;
|
|
6
|
+
cells: Uint8Array;
|
|
7
|
+
coverage: number;
|
|
8
|
+
nearest?: number;
|
|
9
|
+
farthest?: number;
|
|
10
|
+
ts: number;
|
|
11
|
+
/** Occupancy at a cell. Out-of-range reads as empty rather than throwing. */
|
|
12
|
+
at(cx: number, cy: number): boolean;
|
|
13
|
+
/** Hit test in normalised 0..1 game space. */
|
|
14
|
+
hit(x: number, y: number): boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Unpacks the 1-bit mask.
|
|
18
|
+
*
|
|
19
|
+
* MSB is the leftmost pixel of each byte — the same order the runtime packs
|
|
20
|
+
* (qs_silhouette_v1). Getting this backwards mirrors every row, which looks
|
|
21
|
+
* plausible on a symmetric scene and is why the bit order is asserted in tests.
|
|
22
|
+
*/
|
|
23
|
+
export declare function decodeSilhouette(msg: DepthSilhouette): SilhouetteFrame;
|
|
24
|
+
export declare const QSBN_HEADER_BYTES = 16;
|
|
25
|
+
export interface BinaryFrame {
|
|
26
|
+
version: number;
|
|
27
|
+
/** Correlation id: the request id, or its FNV-1a hash when over 8 bytes. */
|
|
28
|
+
id: bigint;
|
|
29
|
+
payload: ArrayBuffer;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* FNV-1a over the id string. Ids longer than 8 bytes are hashed by both sides
|
|
33
|
+
* with this same function, so the header stays fixed-width without giving up
|
|
34
|
+
* the ability to detect a mispaired frame.
|
|
35
|
+
*/
|
|
36
|
+
export declare function fnv1a64(s: string): bigint;
|
|
37
|
+
/** The id as it appears in a QSBN header: raw big-endian bytes, or the hash. */
|
|
38
|
+
export declare function binaryCorrelationId(id: string): bigint;
|
|
39
|
+
export declare function parseBinaryFrame(buf: ArrayBuffer): BinaryFrame;
|
|
40
|
+
//# sourceMappingURL=decode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decode.d.ts","sourceRoot":"","sources":["../../src/runtime/decode.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,oEAAoE;AACpE,MAAM,WAAW,eAAe;IAC9B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,6EAA6E;IAC7E,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IACpC,8CAA8C;IAC9C,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACpC;AAeD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,GAAG,eAAe,CAkCtE;AAUD,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAGpC,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,WAAW,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CASzC;AAED,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAKtD;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,WAAW,GAAG,WAAW,CAc9D"}
|