@yydb/yydb-client 0.0.0 → 0.0.1
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 +25 -3
- package/dist/client.d.ts +32 -0
- package/dist/client.js +89 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/node.d.ts +7 -0
- package/dist/node.js +23 -0
- package/dist/tcp.d.ts +3 -0
- package/dist/tcp.js +65 -0
- package/dist/transport.d.ts +7 -0
- package/dist/transport.js +23 -0
- package/dist/wire.d.ts +54 -0
- package/dist/wire.js +167 -0
- package/dist/ws.d.ts +3 -0
- package/dist/ws.js +48 -0
- package/package.json +45 -6
package/README.md
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@yydb/yydb-client`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
TypeScript client for the **YY wire protocol**. Used by `yydb-webui` and
|
|
4
|
+
`homepage`.
|
|
5
|
+
|
|
6
|
+
Frame prefix:
|
|
7
|
+
|
|
8
|
+
- product magic: **`YYDB`** or **`YYDS`** — backend self-claim; **same wire
|
|
9
|
+
effect**; client accepts either and does not require consistency
|
|
10
|
+
- version digits: **`0000`** now; later **`0001`**, … (this is what matters)
|
|
11
|
+
|
|
12
|
+
| Entry | Transport |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `@yydb/yydb-client` | WebSocket (`/wire`) — browsers / WebUI |
|
|
15
|
+
| `@yydb/yydb-client/node` | Node TCP (or `ws://` WebSocket) |
|
|
16
|
+
|
|
17
|
+
Rust crates in this repo:
|
|
18
|
+
|
|
19
|
+
- `backends/yydb` — YYDB **embedded** facade
|
|
20
|
+
- `backends/yydb-client` — YYDB **remote** TCP client for `yydb serve`
|
|
21
|
+
|
|
22
|
+
## Security
|
|
23
|
+
|
|
24
|
+
Reference serve has **no ACL**. Only connect to loopback endpoints you control.
|
|
25
|
+
|
|
26
|
+
See [`../../documentation/serve-protocol.md`](../../documentation/serve-protocol.md).
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SchemaVersion } from "./wire.js";
|
|
2
|
+
import type { Transport } from "./transport.js";
|
|
3
|
+
export type { Frame, MsgTypeCode, SchemaVersion, } from "./wire.js";
|
|
4
|
+
export { MsgType, encodeFrame, decodeFrame } from "./wire.js";
|
|
5
|
+
export interface ConnectOptions {
|
|
6
|
+
/** `ws://127.0.0.1:7700/wire` or `127.0.0.1:7700` (upgraded to `/wire`). */
|
|
7
|
+
endpoint: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* TypeScript client for YY wire (browser = WebSocket `/wire`).
|
|
11
|
+
* Accepts product magic `YYDB` or `YYDS` as equivalent self-claims (same wire);
|
|
12
|
+
* does not require the magic to match a preferred product. Version gate: `0000`.
|
|
13
|
+
*/
|
|
14
|
+
export declare class Client {
|
|
15
|
+
readonly endpoint: string;
|
|
16
|
+
private transport;
|
|
17
|
+
private nextId;
|
|
18
|
+
private constructor();
|
|
19
|
+
/** Construct from an already-open transport (used by Node TCP entry). */
|
|
20
|
+
static fromTransport(endpoint: string, transport: Transport): Client;
|
|
21
|
+
/** Open WebSocket to `yydb serve` `/wire` and Hello-handshake. */
|
|
22
|
+
static connect(options: ConnectOptions | string): Promise<Client>;
|
|
23
|
+
close(): void;
|
|
24
|
+
hello(): Promise<string>;
|
|
25
|
+
serverVersion(): Promise<string>;
|
|
26
|
+
info(): Promise<string>;
|
|
27
|
+
getSchema(): Promise<SchemaVersion | null>;
|
|
28
|
+
ensureSchema(version: number, document: string): Promise<void>;
|
|
29
|
+
get(key: string): Promise<Uint8Array | null>;
|
|
30
|
+
put(key: string, value: Uint8Array | string): Promise<void>;
|
|
31
|
+
private roundtrip;
|
|
32
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { decodeKvGetOk, decodeSchemaGetOk, encodeKvGet, encodeKvPut, encodeSchemaEnsure, MsgType, } from "./wire.js";
|
|
2
|
+
import { openWebSocketTransport } from "./ws.js";
|
|
3
|
+
export { MsgType, encodeFrame, decodeFrame } from "./wire.js";
|
|
4
|
+
/**
|
|
5
|
+
* TypeScript client for YY wire (browser = WebSocket `/wire`).
|
|
6
|
+
* Accepts product magic `YYDB` or `YYDS` as equivalent self-claims (same wire);
|
|
7
|
+
* does not require the magic to match a preferred product. Version gate: `0000`.
|
|
8
|
+
*/
|
|
9
|
+
export class Client {
|
|
10
|
+
endpoint;
|
|
11
|
+
transport;
|
|
12
|
+
nextId = 1;
|
|
13
|
+
constructor(endpoint, transport) {
|
|
14
|
+
this.endpoint = endpoint;
|
|
15
|
+
this.transport = transport;
|
|
16
|
+
}
|
|
17
|
+
/** Construct from an already-open transport (used by Node TCP entry). */
|
|
18
|
+
static fromTransport(endpoint, transport) {
|
|
19
|
+
return new Client(endpoint, transport);
|
|
20
|
+
}
|
|
21
|
+
/** Open WebSocket to `yydb serve` `/wire` and Hello-handshake. */
|
|
22
|
+
static async connect(options) {
|
|
23
|
+
const endpoint = typeof options === "string" ? options : options.endpoint;
|
|
24
|
+
if (!endpoint.trim()) {
|
|
25
|
+
throw new Error("empty serve endpoint");
|
|
26
|
+
}
|
|
27
|
+
const transport = await openWebSocketTransport(endpoint);
|
|
28
|
+
const client = new Client(endpoint, transport);
|
|
29
|
+
await client.hello();
|
|
30
|
+
return client;
|
|
31
|
+
}
|
|
32
|
+
close() {
|
|
33
|
+
this.transport.close();
|
|
34
|
+
}
|
|
35
|
+
async hello() {
|
|
36
|
+
const response = await this.roundtrip(MsgType.Hello, new Uint8Array());
|
|
37
|
+
return new TextDecoder().decode(response.body);
|
|
38
|
+
}
|
|
39
|
+
async serverVersion() {
|
|
40
|
+
return this.hello();
|
|
41
|
+
}
|
|
42
|
+
async info() {
|
|
43
|
+
const response = await this.roundtrip(MsgType.Info, new Uint8Array());
|
|
44
|
+
return new TextDecoder().decode(response.body);
|
|
45
|
+
}
|
|
46
|
+
async getSchema() {
|
|
47
|
+
const response = await this.roundtrip(MsgType.SchemaGet, new Uint8Array());
|
|
48
|
+
return decodeSchemaGetOk(response.body);
|
|
49
|
+
}
|
|
50
|
+
async ensureSchema(version, document) {
|
|
51
|
+
await this.roundtrip(MsgType.SchemaEnsure, encodeSchemaEnsure(version, document));
|
|
52
|
+
}
|
|
53
|
+
async get(key) {
|
|
54
|
+
const response = await this.roundtrip(MsgType.KvGet, encodeKvGet(key));
|
|
55
|
+
return decodeKvGetOk(response.body);
|
|
56
|
+
}
|
|
57
|
+
async put(key, value) {
|
|
58
|
+
const bytes = typeof value === "string" ? new TextEncoder().encode(value) : value;
|
|
59
|
+
await this.roundtrip(MsgType.KvPut, encodeKvPut(key, bytes));
|
|
60
|
+
}
|
|
61
|
+
async roundtrip(msgType, body) {
|
|
62
|
+
const requestId = this.nextId++;
|
|
63
|
+
const response = await this.transport.send({
|
|
64
|
+
msgType,
|
|
65
|
+
flags: 0,
|
|
66
|
+
requestId,
|
|
67
|
+
body,
|
|
68
|
+
});
|
|
69
|
+
if (response.requestId !== requestId) {
|
|
70
|
+
throw new Error("wire response request_id mismatch");
|
|
71
|
+
}
|
|
72
|
+
if (response.msgType === MsgType.Error) {
|
|
73
|
+
throw new Error(new TextDecoder().decode(response.body));
|
|
74
|
+
}
|
|
75
|
+
const expectedOk = {
|
|
76
|
+
[MsgType.Hello]: MsgType.HelloOk,
|
|
77
|
+
[MsgType.Info]: MsgType.InfoOk,
|
|
78
|
+
[MsgType.SchemaGet]: MsgType.SchemaGetOk,
|
|
79
|
+
[MsgType.SchemaEnsure]: MsgType.SchemaEnsureOk,
|
|
80
|
+
[MsgType.KvGet]: MsgType.KvGetOk,
|
|
81
|
+
[MsgType.KvPut]: MsgType.KvPutOk,
|
|
82
|
+
};
|
|
83
|
+
const want = expectedOk[msgType];
|
|
84
|
+
if (want !== undefined && response.msgType !== want) {
|
|
85
|
+
throw new Error(`unexpected wire response type ${response.msgType}`);
|
|
86
|
+
}
|
|
87
|
+
return response;
|
|
88
|
+
}
|
|
89
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Browser-safe entry (WebSocket). For Node TCP use `@yydb/yydb-client/node`. */
|
|
2
|
+
export { Client } from "./client.js";
|
|
3
|
+
export type { ConnectOptions } from "./client.js";
|
|
4
|
+
export { MAGIC, MAGIC_YYDB, MAGIC_YYDS, WIRE_VERSION, WIRE_VERSION_NEXT, HEADER_LEN, MsgType, encodeFrame, decodeFrame, encodeSchemaEnsure, encodeKvGet, encodeKvPut, decodeSchemaGetOk, decodeKvGetOk, } from "./wire.js";
|
|
5
|
+
export type { Frame, MsgTypeCode, ProductMagic, SchemaVersion } from "./wire.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Browser-safe entry (WebSocket). For Node TCP use `@yydb/yydb-client/node`. */
|
|
2
|
+
export { Client } from "./client.js";
|
|
3
|
+
export { MAGIC, MAGIC_YYDB, MAGIC_YYDS, WIRE_VERSION, WIRE_VERSION_NEXT, HEADER_LEN, MsgType, encodeFrame, decodeFrame, encodeSchemaEnsure, encodeKvGet, encodeKvPut, decodeSchemaGetOk, decodeKvGetOk, } from "./wire.js";
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Client, type ConnectOptions } from "./client.js";
|
|
2
|
+
export { Client } from "./client.js";
|
|
3
|
+
export type { ConnectOptions } from "./client.js";
|
|
4
|
+
export { MsgType, encodeFrame, decodeFrame, encodeSchemaEnsure, encodeKvGet, encodeKvPut, decodeSchemaGetOk, decodeKvGetOk, } from "./wire.js";
|
|
5
|
+
export type { Frame, MsgTypeCode, SchemaVersion } from "./wire.js";
|
|
6
|
+
/** Node entry: TCP by default; `ws://` uses WebSocket. */
|
|
7
|
+
export declare function connect(options: ConnectOptions | string): Promise<Client>;
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Client } from "./client.js";
|
|
2
|
+
import { openTcpTransport } from "./tcp.js";
|
|
3
|
+
import { openWebSocketTransport } from "./ws.js";
|
|
4
|
+
import { normalizeWsUrl } from "./transport.js";
|
|
5
|
+
export { Client } from "./client.js";
|
|
6
|
+
export { MsgType, encodeFrame, decodeFrame, encodeSchemaEnsure, encodeKvGet, encodeKvPut, decodeSchemaGetOk, decodeKvGetOk, } from "./wire.js";
|
|
7
|
+
function wantsWebSocket(endpoint) {
|
|
8
|
+
const trimmed = endpoint.trim();
|
|
9
|
+
return trimmed.startsWith("ws://") || trimmed.startsWith("wss://");
|
|
10
|
+
}
|
|
11
|
+
/** Node entry: TCP by default; `ws://` uses WebSocket. */
|
|
12
|
+
export async function connect(options) {
|
|
13
|
+
const endpoint = typeof options === "string" ? options : options.endpoint;
|
|
14
|
+
if (!endpoint.trim()) {
|
|
15
|
+
throw new Error("empty serve endpoint");
|
|
16
|
+
}
|
|
17
|
+
const transport = wantsWebSocket(endpoint)
|
|
18
|
+
? await openWebSocketTransport(endpoint)
|
|
19
|
+
: await openTcpTransport(endpoint);
|
|
20
|
+
const client = Client.fromTransport(wantsWebSocket(endpoint) ? normalizeWsUrl(endpoint) : endpoint, transport);
|
|
21
|
+
await client.hello();
|
|
22
|
+
return client;
|
|
23
|
+
}
|
package/dist/tcp.d.ts
ADDED
package/dist/tcp.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { decodeFrame, encodeFrame } from "./wire.js";
|
|
2
|
+
import { normalizeTcpHostPort } from "./transport.js";
|
|
3
|
+
/** Node.js TCP transport for `yydb serve` binary frames. */
|
|
4
|
+
export async function openTcpTransport(endpoint) {
|
|
5
|
+
const net = await import("node:net");
|
|
6
|
+
const hostport = normalizeTcpHostPort(endpoint);
|
|
7
|
+
const [host, portText] = hostport.includes("]:")
|
|
8
|
+
? (() => {
|
|
9
|
+
const rest = hostport.slice(1);
|
|
10
|
+
const idx = rest.indexOf("]:");
|
|
11
|
+
return [rest.slice(0, idx), rest.slice(idx + 2)];
|
|
12
|
+
})()
|
|
13
|
+
: hostport.split(":");
|
|
14
|
+
const port = Number(portText);
|
|
15
|
+
if (!host || !Number.isFinite(port)) {
|
|
16
|
+
throw new Error(`invalid TCP endpoint: ${endpoint}`);
|
|
17
|
+
}
|
|
18
|
+
const socket = net.createConnection({ host, port });
|
|
19
|
+
await new Promise((resolve, reject) => {
|
|
20
|
+
socket.once("connect", () => resolve());
|
|
21
|
+
socket.once("error", reject);
|
|
22
|
+
});
|
|
23
|
+
let buffer = new Uint8Array(0);
|
|
24
|
+
const waiters = [];
|
|
25
|
+
socket.on("data", (chunk) => {
|
|
26
|
+
const next = new Uint8Array(buffer.byteLength + chunk.byteLength);
|
|
27
|
+
next.set(buffer, 0);
|
|
28
|
+
next.set(chunk, buffer.byteLength);
|
|
29
|
+
buffer = next;
|
|
30
|
+
for (const wake of waiters.splice(0)) {
|
|
31
|
+
wake();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
async function readExact(n) {
|
|
35
|
+
while (buffer.byteLength < n) {
|
|
36
|
+
await new Promise((resolve) => waiters.push(resolve));
|
|
37
|
+
}
|
|
38
|
+
const out = buffer.slice(0, n);
|
|
39
|
+
buffer = buffer.slice(n);
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
async send(frame) {
|
|
44
|
+
const encoded = encodeFrame(frame);
|
|
45
|
+
await new Promise((resolve, reject) => {
|
|
46
|
+
socket.write(Buffer.from(encoded), (error) => {
|
|
47
|
+
if (error)
|
|
48
|
+
reject(error);
|
|
49
|
+
else
|
|
50
|
+
resolve();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
const header = await readExact(20);
|
|
54
|
+
const bodyLen = new DataView(header.buffer, header.byteOffset, header.byteLength).getUint32(16, true);
|
|
55
|
+
const body = bodyLen > 0 ? await readExact(bodyLen) : new Uint8Array();
|
|
56
|
+
const full = new Uint8Array(20 + body.byteLength);
|
|
57
|
+
full.set(header, 0);
|
|
58
|
+
full.set(body, 20);
|
|
59
|
+
return decodeFrame(full);
|
|
60
|
+
},
|
|
61
|
+
close() {
|
|
62
|
+
socket.end();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Frame } from "./wire.js";
|
|
2
|
+
export type Transport = {
|
|
3
|
+
send(frame: Frame): Promise<Frame>;
|
|
4
|
+
close(): void;
|
|
5
|
+
};
|
|
6
|
+
export declare function normalizeWsUrl(endpoint: string): string;
|
|
7
|
+
export declare function normalizeTcpHostPort(endpoint: string): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function normalizeWsUrl(endpoint) {
|
|
2
|
+
const trimmed = endpoint.trim();
|
|
3
|
+
if (trimmed.startsWith("ws://") || trimmed.startsWith("wss://")) {
|
|
4
|
+
if (trimmed.includes("/wire")) {
|
|
5
|
+
return trimmed;
|
|
6
|
+
}
|
|
7
|
+
return trimmed.replace(/\/$/, "") + "/wire";
|
|
8
|
+
}
|
|
9
|
+
const hostport = trimmed
|
|
10
|
+
.replace(/^tcp:\/\//, "")
|
|
11
|
+
.replace(/^http:\/\//, "")
|
|
12
|
+
.split("/")[0];
|
|
13
|
+
return `ws://${hostport}/wire`;
|
|
14
|
+
}
|
|
15
|
+
export function normalizeTcpHostPort(endpoint) {
|
|
16
|
+
const trimmed = endpoint
|
|
17
|
+
.trim()
|
|
18
|
+
.replace(/^tcp:\/\//, "")
|
|
19
|
+
.replace(/^http:\/\//, "")
|
|
20
|
+
.replace(/^ws:\/\//, "")
|
|
21
|
+
.replace(/^wss:\/\//, "");
|
|
22
|
+
return trimmed.split("/")[0];
|
|
23
|
+
}
|
package/dist/wire.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YY wire protocol — VOS-native serve frames.
|
|
3
|
+
* Prefix: product magic `YYDB` | `YYDS` (backend self-claim only; same effect),
|
|
4
|
+
* then four ASCII version digits (`0000` current, `0001` next).
|
|
5
|
+
* Frontends accept either magic and do not require product consistency.
|
|
6
|
+
*/
|
|
7
|
+
export declare const MAGIC_YYDB: Uint8Array;
|
|
8
|
+
export declare const MAGIC_YYDS: Uint8Array;
|
|
9
|
+
/** @deprecated Use MAGIC_YYDB — encode default product. */
|
|
10
|
+
export declare const MAGIC: Uint8Array;
|
|
11
|
+
/** Current wire version digits. */
|
|
12
|
+
export declare const WIRE_VERSION: Uint8Array;
|
|
13
|
+
/** Next planned wire version (not spoken yet). */
|
|
14
|
+
export declare const WIRE_VERSION_NEXT: Uint8Array;
|
|
15
|
+
export declare const HEADER_LEN = 20;
|
|
16
|
+
export declare const MAX_BODY_LEN: number;
|
|
17
|
+
export declare const MsgType: {
|
|
18
|
+
readonly Hello: 1;
|
|
19
|
+
readonly HelloOk: 2;
|
|
20
|
+
readonly Info: 3;
|
|
21
|
+
readonly InfoOk: 4;
|
|
22
|
+
readonly SchemaGet: 5;
|
|
23
|
+
readonly SchemaGetOk: 6;
|
|
24
|
+
readonly SchemaEnsure: 7;
|
|
25
|
+
readonly SchemaEnsureOk: 8;
|
|
26
|
+
readonly KvGet: 9;
|
|
27
|
+
readonly KvGetOk: 10;
|
|
28
|
+
readonly KvPut: 11;
|
|
29
|
+
readonly KvPutOk: 12;
|
|
30
|
+
readonly Error: 255;
|
|
31
|
+
};
|
|
32
|
+
export type MsgTypeCode = (typeof MsgType)[keyof typeof MsgType];
|
|
33
|
+
export type ProductMagic = "YYDB" | "YYDS";
|
|
34
|
+
export interface Frame {
|
|
35
|
+
msgType: number;
|
|
36
|
+
flags: number;
|
|
37
|
+
requestId: number;
|
|
38
|
+
body: Uint8Array;
|
|
39
|
+
/** Product magic observed on decode (backend self-claim; optional to ignore). */
|
|
40
|
+
product?: ProductMagic;
|
|
41
|
+
}
|
|
42
|
+
export declare function encodeFrame(frame: Frame, product?: ProductMagic): Uint8Array;
|
|
43
|
+
export declare function decodeFrame(bytes: Uint8Array): Frame;
|
|
44
|
+
export declare function pushU32(out: number[], value: number): void;
|
|
45
|
+
export declare function pushBytes(out: number[], bytes: Uint8Array): void;
|
|
46
|
+
export declare function encodeSchemaEnsure(version: number, document: string): Uint8Array;
|
|
47
|
+
export declare function encodeKvGet(key: string): Uint8Array;
|
|
48
|
+
export declare function encodeKvPut(key: string, value: Uint8Array): Uint8Array;
|
|
49
|
+
export interface SchemaVersion {
|
|
50
|
+
version: number;
|
|
51
|
+
document: string;
|
|
52
|
+
}
|
|
53
|
+
export declare function decodeSchemaGetOk(body: Uint8Array): SchemaVersion | null;
|
|
54
|
+
export declare function decodeKvGetOk(body: Uint8Array): Uint8Array | null;
|
package/dist/wire.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YY wire protocol — VOS-native serve frames.
|
|
3
|
+
* Prefix: product magic `YYDB` | `YYDS` (backend self-claim only; same effect),
|
|
4
|
+
* then four ASCII version digits (`0000` current, `0001` next).
|
|
5
|
+
* Frontends accept either magic and do not require product consistency.
|
|
6
|
+
*/
|
|
7
|
+
export const MAGIC_YYDB = new TextEncoder().encode("YYDB");
|
|
8
|
+
export const MAGIC_YYDS = new TextEncoder().encode("YYDS");
|
|
9
|
+
/** @deprecated Use MAGIC_YYDB — encode default product. */
|
|
10
|
+
export const MAGIC = MAGIC_YYDB;
|
|
11
|
+
/** Current wire version digits. */
|
|
12
|
+
export const WIRE_VERSION = new TextEncoder().encode("0000");
|
|
13
|
+
/** Next planned wire version (not spoken yet). */
|
|
14
|
+
export const WIRE_VERSION_NEXT = new TextEncoder().encode("0001");
|
|
15
|
+
export const HEADER_LEN = 20;
|
|
16
|
+
export const MAX_BODY_LEN = 16 * 1024 * 1024;
|
|
17
|
+
export const MsgType = {
|
|
18
|
+
Hello: 1,
|
|
19
|
+
HelloOk: 2,
|
|
20
|
+
Info: 3,
|
|
21
|
+
InfoOk: 4,
|
|
22
|
+
SchemaGet: 5,
|
|
23
|
+
SchemaGetOk: 6,
|
|
24
|
+
SchemaEnsure: 7,
|
|
25
|
+
SchemaEnsureOk: 8,
|
|
26
|
+
KvGet: 9,
|
|
27
|
+
KvGetOk: 10,
|
|
28
|
+
KvPut: 11,
|
|
29
|
+
KvPutOk: 12,
|
|
30
|
+
Error: 255,
|
|
31
|
+
};
|
|
32
|
+
function writeU16(view, offset, value) {
|
|
33
|
+
view.setUint16(offset, value, true);
|
|
34
|
+
}
|
|
35
|
+
function writeU32(view, offset, value) {
|
|
36
|
+
view.setUint32(offset, value, true);
|
|
37
|
+
}
|
|
38
|
+
function readU16(view, offset) {
|
|
39
|
+
return view.getUint16(offset, true);
|
|
40
|
+
}
|
|
41
|
+
function readU32(view, offset) {
|
|
42
|
+
return view.getUint32(offset, true);
|
|
43
|
+
}
|
|
44
|
+
function productBytes(product) {
|
|
45
|
+
return product === "YYDS" ? MAGIC_YYDS : MAGIC_YYDB;
|
|
46
|
+
}
|
|
47
|
+
function parseProduct(bytes) {
|
|
48
|
+
const text = new TextDecoder().decode(bytes.subarray(0, 4));
|
|
49
|
+
if (text === "YYDB" || text === "YYDS") {
|
|
50
|
+
return text;
|
|
51
|
+
}
|
|
52
|
+
throw new Error("wire frame bad product magic (want YYDB|YYDS)");
|
|
53
|
+
}
|
|
54
|
+
function checkVersion(bytes) {
|
|
55
|
+
const version = new TextDecoder().decode(bytes.subarray(4, 8));
|
|
56
|
+
if (version !== "0000") {
|
|
57
|
+
throw new Error(`wire version not supported (got ${version}, want 0000)`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function encodeFrame(frame, product = "YYDB") {
|
|
61
|
+
if (frame.body.byteLength > MAX_BODY_LEN) {
|
|
62
|
+
throw new Error("wire body too large");
|
|
63
|
+
}
|
|
64
|
+
const out = new Uint8Array(HEADER_LEN + frame.body.byteLength);
|
|
65
|
+
out.set(productBytes(product), 0);
|
|
66
|
+
out.set(WIRE_VERSION, 4);
|
|
67
|
+
const view = new DataView(out.buffer);
|
|
68
|
+
writeU16(view, 8, frame.msgType);
|
|
69
|
+
writeU16(view, 10, frame.flags);
|
|
70
|
+
writeU32(view, 12, frame.requestId);
|
|
71
|
+
writeU32(view, 16, frame.body.byteLength);
|
|
72
|
+
out.set(frame.body, HEADER_LEN);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
export function decodeFrame(bytes) {
|
|
76
|
+
if (bytes.byteLength < HEADER_LEN) {
|
|
77
|
+
throw new Error("wire frame truncated header");
|
|
78
|
+
}
|
|
79
|
+
const product = parseProduct(bytes);
|
|
80
|
+
checkVersion(bytes);
|
|
81
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
82
|
+
const msgType = readU16(view, 8);
|
|
83
|
+
const flags = readU16(view, 10);
|
|
84
|
+
const requestId = readU32(view, 12);
|
|
85
|
+
const bodyLen = readU32(view, 16);
|
|
86
|
+
if (bodyLen > MAX_BODY_LEN) {
|
|
87
|
+
throw new Error("wire body too large");
|
|
88
|
+
}
|
|
89
|
+
const need = HEADER_LEN + bodyLen;
|
|
90
|
+
if (bytes.byteLength < need) {
|
|
91
|
+
throw new Error("wire frame truncated body");
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
msgType,
|
|
95
|
+
flags,
|
|
96
|
+
requestId,
|
|
97
|
+
body: bytes.slice(HEADER_LEN, need),
|
|
98
|
+
product,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export function pushU32(out, value) {
|
|
102
|
+
out.push(value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff, (value >> 24) & 0xff);
|
|
103
|
+
}
|
|
104
|
+
export function pushBytes(out, bytes) {
|
|
105
|
+
pushU32(out, bytes.byteLength);
|
|
106
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
107
|
+
out.push(bytes[i]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function encodeSchemaEnsure(version, document) {
|
|
111
|
+
const out = [];
|
|
112
|
+
pushU32(out, version >>> 0);
|
|
113
|
+
pushBytes(out, new TextEncoder().encode(document));
|
|
114
|
+
return Uint8Array.from(out);
|
|
115
|
+
}
|
|
116
|
+
export function encodeKvGet(key) {
|
|
117
|
+
const out = [];
|
|
118
|
+
pushBytes(out, new TextEncoder().encode(key));
|
|
119
|
+
return Uint8Array.from(out);
|
|
120
|
+
}
|
|
121
|
+
export function encodeKvPut(key, value) {
|
|
122
|
+
const out = [];
|
|
123
|
+
pushBytes(out, new TextEncoder().encode(key));
|
|
124
|
+
pushBytes(out, value);
|
|
125
|
+
return Uint8Array.from(out);
|
|
126
|
+
}
|
|
127
|
+
function readU32At(body, offset) {
|
|
128
|
+
if (offset.n + 4 > body.byteLength) {
|
|
129
|
+
throw new Error("wire body truncated u32");
|
|
130
|
+
}
|
|
131
|
+
const view = new DataView(body.buffer, body.byteOffset, body.byteLength);
|
|
132
|
+
const value = readU32(view, offset.n);
|
|
133
|
+
offset.n += 4;
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
function readBytesAt(body, offset, len) {
|
|
137
|
+
if (offset.n + len > body.byteLength) {
|
|
138
|
+
throw new Error("wire body truncated bytes");
|
|
139
|
+
}
|
|
140
|
+
const slice = body.slice(offset.n, offset.n + len);
|
|
141
|
+
offset.n += len;
|
|
142
|
+
return slice;
|
|
143
|
+
}
|
|
144
|
+
export function decodeSchemaGetOk(body) {
|
|
145
|
+
if (body.byteLength === 0) {
|
|
146
|
+
throw new Error("schema get ok empty");
|
|
147
|
+
}
|
|
148
|
+
if (body[0] === 0) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const offset = { n: 1 };
|
|
152
|
+
const version = readU32At(body, offset);
|
|
153
|
+
const docLen = readU32At(body, offset);
|
|
154
|
+
const doc = readBytesAt(body, offset, docLen);
|
|
155
|
+
return { version, document: new TextDecoder().decode(doc) };
|
|
156
|
+
}
|
|
157
|
+
export function decodeKvGetOk(body) {
|
|
158
|
+
if (body.byteLength === 0) {
|
|
159
|
+
throw new Error("kv get ok empty");
|
|
160
|
+
}
|
|
161
|
+
if (body[0] === 0) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const offset = { n: 1 };
|
|
165
|
+
const valLen = readU32At(body, offset);
|
|
166
|
+
return readBytesAt(body, offset, valLen);
|
|
167
|
+
}
|
package/dist/ws.d.ts
ADDED
package/dist/ws.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { decodeFrame, encodeFrame } from "./wire.js";
|
|
2
|
+
import { normalizeWsUrl } from "./transport.js";
|
|
3
|
+
/** Browser / any host with WebSocket — path `/wire`, binary frames. */
|
|
4
|
+
export async function openWebSocketTransport(endpoint) {
|
|
5
|
+
const url = normalizeWsUrl(endpoint);
|
|
6
|
+
const ws = new WebSocket(url);
|
|
7
|
+
ws.binaryType = "arraybuffer";
|
|
8
|
+
await new Promise((resolve, reject) => {
|
|
9
|
+
ws.addEventListener("open", () => resolve(), { once: true });
|
|
10
|
+
ws.addEventListener("error", () => reject(new Error(`websocket failed: ${url}`)), { once: true });
|
|
11
|
+
});
|
|
12
|
+
let pending = null;
|
|
13
|
+
ws.addEventListener("message", (event) => {
|
|
14
|
+
try {
|
|
15
|
+
const data = event.data instanceof ArrayBuffer
|
|
16
|
+
? new Uint8Array(event.data)
|
|
17
|
+
: new Uint8Array(event.data);
|
|
18
|
+
const frame = decodeFrame(data);
|
|
19
|
+
if (pending) {
|
|
20
|
+
const slot = pending;
|
|
21
|
+
pending = null;
|
|
22
|
+
slot.resolve(frame);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (pending) {
|
|
27
|
+
const slot = pending;
|
|
28
|
+
pending = null;
|
|
29
|
+
slot.reject(error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
send(frame) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
if (pending) {
|
|
37
|
+
reject(new Error("overlapping websocket requests"));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
pending = { resolve, reject };
|
|
41
|
+
ws.send(encodeFrame(frame));
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
close() {
|
|
45
|
+
ws.close();
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yydb/yydb-client",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "TypeScript client for YY wire protocol v1 (VOS-native; YYDB/YYDS/compatible hosts)",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./node": {
|
|
15
|
+
"types": "./dist/node.d.ts",
|
|
16
|
+
"import": "./dist/node.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json",
|
|
25
|
+
"test": "node --experimental-strip-types --test src/wire.test.ts"
|
|
9
26
|
},
|
|
10
27
|
"repository": {
|
|
11
28
|
"type": "git",
|
|
12
|
-
"url": "git+https://github.com/yy-database/yydb.git"
|
|
29
|
+
"url": "git+https://github.com/yy-database/yydb.rs.git",
|
|
30
|
+
"directory": "frontends/yydb-client"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/yy-database/yydb.rs/tree/dev/frontends/yydb-client#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/yy-database/yydb.rs/issues"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"yydb",
|
|
38
|
+
"yyds",
|
|
39
|
+
"vos",
|
|
40
|
+
"wire-protocol",
|
|
41
|
+
"typescript"
|
|
42
|
+
],
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^20.10.0",
|
|
48
|
+
"typescript": "~5.3.0"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18"
|
|
13
52
|
}
|
|
14
53
|
}
|