@yydb/yydb-client 0.0.0 → 0.0.2

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 CHANGED
@@ -1,4 +1,79 @@
1
- # @yydb/yydb-client
1
+ # `@yydb/yydb-client`
2
2
 
3
- Placeholder 0.0.0 for npm Trusted Publisher bootstrap.
4
- Real releases replace this from CI (`release-npm.yml`).
3
+ **Lightweight TypeScript client for the YY wire protocol (WebSocket or TCP).**
4
+
5
+ Use this when you already run `yydb serve` (from [`@yydb/yydb`](../yydb) CLI or another host) and only need the client
6
+ surface — browser, WebUI, Electron renderer, or a thin Node process.
7
+
8
+ [![npm](https://img.shields.io/npm/v/@yydb/yydb-client)](https://www.npmjs.com/package/@yydb/yydb-client)
9
+ [![Node.js](https://img.shields.io/node/v/@yydb/yydb-client)](https://www.npmjs.com/package/@yydb/yydb-client)
10
+ [![Check Rust](https://github.com/yy-database/yydb/actions/workflows/check-rust.yml/badge.svg?branch=dev)](https://github.com/yy-database/yydb/actions/workflows/check-rust.yml)
11
+ [![Check TypeScript](https://github.com/yy-database/yydb/actions/workflows/check-typescript.yml/badge.svg?branch=dev)](https://github.com/yy-database/yydb/actions/workflows/check-typescript.yml)
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @yydb/yydb-client
17
+ ```
18
+
19
+ ## Browser or WebSocket
20
+
21
+ Start a local host first:
22
+
23
+ ```text
24
+ yydb serve app.yydb --bind 127.0.0.1:7700
25
+ ```
26
+
27
+ Then connect from browser-compatible code:
28
+
29
+ ```ts
30
+ import {Client} from "@yydb/yydb-client";
31
+
32
+ const db = await Client.connect("ws://127.0.0.1:7700/wire");
33
+ await db.ensureSchema(1, "table Setting { @@id: uuid, key: utf8, value: utf8 }");
34
+ await db.put("theme", "dark");
35
+ console.log(await db.get("theme"));
36
+ db.close();
37
+ ```
38
+
39
+ ## Node TCP
40
+
41
+ The `/node` entry point supports a TCP endpoint and `ws://` URLs:
42
+
43
+ ```ts
44
+ import {connect} from "@yydb/yydb-client/node";
45
+
46
+ const db = await connect("127.0.0.1:7700");
47
+ console.log(await db.info());
48
+ db.close();
49
+ ```
50
+
51
+ If a Node application only needs to open a local `.yydb` file, use
52
+ [`@yydb/yydb`](https://www.npmjs.com/package/@yydb/yydb). It manages the local engine for you; this package is for an
53
+ existing host or a custom transport integration.
54
+
55
+ ## Exports
56
+
57
+ | Import | Transport | Typical use |
58
+ |--------------------------|-------------------|--------------------|
59
+ | `@yydb/yydb-client` | WebSocket `/wire` | Browsers and WebUI |
60
+ | `@yydb/yydb-client/node` | TCP or WebSocket | Node and Electron |
61
+
62
+ The high-level client includes `info`, schema ensure/get, key/value get/put, server version, and close operations. Frame
63
+ helpers such as `encodeFrame` and
64
+ `MsgType` are exported for tools and tests.
65
+
66
+ ## Compatibility and security
67
+
68
+ The client accepts hosts that announce either `YYDB` or `YYDS` on the shared wire. The reference `yydb serve` endpoint
69
+ has no authentication, so connect only to a host you control, normally `127.0.0.1` or `localhost`.
70
+
71
+ Protocol details for implementers are in the
72
+ [`yydb-serve-protocol` skill](../yydb-skills/skills/yydb-serve-protocol/SKILL.md) (`@yydb/yydb-skills`).
73
+
74
+ ## Links
75
+
76
+ - [Node file API](https://www.npmjs.com/package/@yydb/yydb)
77
+ - [Local WebUI](https://github.com/yy-database/yydb.rs/tree/dev/projects/packages/yydb-webui)
78
+ - [YYDB user guides](https://github.com/yy-database/yydb.rs/tree/dev/projects/packages/homepage/documentation/zh-hans)
79
+ - [License](https://github.com/yy-database/yydb.rs/blob/dev/License.md)
@@ -0,0 +1,39 @@
1
+ import { MicroRegisterPayload, SchemaVersion, WireUdfScalarValue } from './wire.js';
2
+ import type { MicroHostHandler, Transport } from './transport.js';
3
+ export type { Frame, MicroHostInvokePayload, MicroRegisterPayload, MsgTypeCode, SchemaVersion, WireUdfScalarKind, WireUdfScalarValue, } from './wire.js';
4
+ export type { MicroHostHandler } from './transport.js';
5
+ export { MsgType, encodeFrame, decodeFrame } from './wire.js';
6
+ export interface ConnectOptions {
7
+ /** `ws://127.0.0.1:7700/wire` or `127.0.0.1:7700` (upgraded to `/wire`). */
8
+ endpoint: string;
9
+ }
10
+ /**
11
+ * TypeScript client for YY wire (browser = WebSocket `/wire`).
12
+ * Accepts product magic `YYDB` or `YYDS` as equivalent self-claims (same wire);
13
+ * does not require the magic to match a preferred product. Version gate: `0000`.
14
+ */
15
+ export declare class Client {
16
+ readonly endpoint: string;
17
+ private transport;
18
+ private nextId;
19
+ private constructor();
20
+ /** Construct from an already-open transport (used by Node TCP entry). */
21
+ static fromTransport(endpoint: string, transport: Transport): Client;
22
+ /** Open WebSocket to `yydb serve` `/wire` and Hello-handshake. */
23
+ static connect(options: ConnectOptions | string): Promise<Client>;
24
+ close(): void;
25
+ hello(): Promise<string>;
26
+ serverVersion(): Promise<string>;
27
+ info(): Promise<string>;
28
+ getSchema(): Promise<SchemaVersion | null>;
29
+ ensureSchema(version: number, document: string): Promise<void>;
30
+ get(key: string): Promise<Uint8Array | null>;
31
+ put(key: string, value: Uint8Array | string): Promise<void>;
32
+ /** Register a session-local host micro contract on the remote engine. */
33
+ registerMicro(payload: MicroRegisterPayload): Promise<void>;
34
+ /** Install the host handler used to answer `MicroHostInvoke` server pushes. */
35
+ setMicroHostHandler(handler: MicroHostHandler | undefined): void;
36
+ /** Invoke a registered scalar UDF on the remote engine. */
37
+ callScalar(name: string, version: number, args: readonly WireUdfScalarValue[]): Promise<WireUdfScalarValue>;
38
+ private roundtrip;
39
+ }
package/dist/client.js ADDED
@@ -0,0 +1,104 @@
1
+ import { decodeKvGetOk, decodeScalarCallOk, decodeSchemaGetOk, encodeKvGet, encodeKvPut, encodeMicroRegister, encodeScalarCall, 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
+ /** Register a session-local host micro contract on the remote engine. */
62
+ async registerMicro(payload) {
63
+ await this.roundtrip(MsgType.MicroRegister, encodeMicroRegister(payload));
64
+ }
65
+ /** Install the host handler used to answer `MicroHostInvoke` server pushes. */
66
+ setMicroHostHandler(handler) {
67
+ this.transport.setMicroHostHandler?.(handler);
68
+ }
69
+ /** Invoke a registered scalar UDF on the remote engine. */
70
+ async callScalar(name, version, args) {
71
+ const response = await this.roundtrip(MsgType.ScalarCall, encodeScalarCall(name, version, args));
72
+ return decodeScalarCallOk(response.body);
73
+ }
74
+ async roundtrip(msgType, body) {
75
+ const requestId = this.nextId++;
76
+ const response = await this.transport.send({
77
+ msgType,
78
+ flags: 0,
79
+ requestId,
80
+ body,
81
+ });
82
+ if (response.requestId !== requestId) {
83
+ throw new Error('wire response request_id mismatch');
84
+ }
85
+ if (response.msgType === MsgType.Error) {
86
+ throw new Error(new TextDecoder().decode(response.body));
87
+ }
88
+ const expectedOk = {
89
+ [MsgType.Hello]: MsgType.HelloOk,
90
+ [MsgType.Info]: MsgType.InfoOk,
91
+ [MsgType.SchemaGet]: MsgType.SchemaGetOk,
92
+ [MsgType.SchemaEnsure]: MsgType.SchemaEnsureOk,
93
+ [MsgType.KvGet]: MsgType.KvGetOk,
94
+ [MsgType.KvPut]: MsgType.KvPutOk,
95
+ [MsgType.MicroRegister]: MsgType.MicroRegisterOk,
96
+ [MsgType.ScalarCall]: MsgType.ScalarCallOk,
97
+ };
98
+ const want = expectedOk[msgType];
99
+ if (want !== undefined && response.msgType !== want) {
100
+ throw new Error(`unexpected wire response type ${response.msgType}`);
101
+ }
102
+ return response;
103
+ }
104
+ }
@@ -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,21 @@
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) ? await openWebSocketTransport(endpoint) : await openTcpTransport(endpoint);
18
+ const client = Client.fromTransport(wantsWebSocket(endpoint) ? normalizeWsUrl(endpoint) : endpoint, transport);
19
+ await client.hello();
20
+ return client;
21
+ }
package/dist/tcp.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { type Transport } from './transport.js';
2
+ /** Node.js TCP transport for `yydb serve` binary frames. */
3
+ export declare function openTcpTransport(endpoint: string): Promise<Transport>;
package/dist/tcp.js ADDED
@@ -0,0 +1,101 @@
1
+ import { decodeFrame, decodeMicroHostInvoke, encodeFrame, encodeMicroHostInvokeOk, MsgType } from './wire.js';
2
+ import { normalizeTcpHostPort } from './transport.js';
3
+ async function readExact(readExactBytes, n) {
4
+ return readExactBytes(n);
5
+ }
6
+ async function readFrame(readExactBytes) {
7
+ const header = await readExact(readExactBytes, 20);
8
+ const bodyLen = new DataView(header.buffer, header.byteOffset, header.byteLength).getUint32(16, true);
9
+ const body = bodyLen > 0 ? await readExact(readExactBytes, bodyLen) : new Uint8Array();
10
+ const full = new Uint8Array(20 + body.byteLength);
11
+ full.set(header, 0);
12
+ full.set(body, 20);
13
+ return decodeFrame(full);
14
+ }
15
+ /** Node.js TCP transport for `yydb serve` binary frames. */
16
+ export async function openTcpTransport(endpoint) {
17
+ const net = await import('node:net');
18
+ const hostport = normalizeTcpHostPort(endpoint);
19
+ const [host, portText] = hostport.includes(']:')
20
+ ? (() => {
21
+ const rest = hostport.slice(1);
22
+ const idx = rest.indexOf(']:');
23
+ return [rest.slice(0, idx), rest.slice(idx + 2)];
24
+ })()
25
+ : hostport.split(':');
26
+ const port = Number(portText);
27
+ if (!host || !Number.isFinite(port)) {
28
+ throw new Error(`invalid TCP endpoint: ${endpoint}`);
29
+ }
30
+ const socket = net.createConnection({ host, port });
31
+ await new Promise((resolve, reject) => {
32
+ socket.once('connect', () => resolve());
33
+ socket.once('error', reject);
34
+ });
35
+ let buffer = new Uint8Array(0);
36
+ const waiters = [];
37
+ let microHostHandler;
38
+ socket.on('data', (chunk) => {
39
+ const next = new Uint8Array(buffer.byteLength + chunk.byteLength);
40
+ next.set(buffer, 0);
41
+ next.set(chunk, buffer.byteLength);
42
+ buffer = next;
43
+ for (const wake of waiters.splice(0)) {
44
+ wake();
45
+ }
46
+ });
47
+ async function readExactBytes(n) {
48
+ while (buffer.byteLength < n) {
49
+ await new Promise((resolve) => waiters.push(resolve));
50
+ }
51
+ const out = buffer.slice(0, n);
52
+ buffer = buffer.slice(n);
53
+ return out;
54
+ }
55
+ async function writeFrame(frame) {
56
+ const encoded = encodeFrame(frame);
57
+ await new Promise((resolve, reject) => {
58
+ socket.write(Buffer.from(encoded), (error) => {
59
+ if (error)
60
+ reject(error);
61
+ else
62
+ resolve();
63
+ });
64
+ });
65
+ }
66
+ async function handleServerPush(frame) {
67
+ if (frame.msgType !== MsgType.MicroHostInvoke) {
68
+ return false;
69
+ }
70
+ if (!microHostHandler) {
71
+ throw new Error('received MicroHostInvoke without a micro host handler');
72
+ }
73
+ const payload = decodeMicroHostInvoke(frame.body);
74
+ const result = await microHostHandler(payload);
75
+ await writeFrame({
76
+ msgType: MsgType.MicroHostInvokeOk,
77
+ flags: 0,
78
+ requestId: frame.requestId,
79
+ body: encodeMicroHostInvokeOk(result),
80
+ });
81
+ return true;
82
+ }
83
+ return {
84
+ setMicroHostHandler(handler) {
85
+ microHostHandler = handler;
86
+ },
87
+ async send(frame) {
88
+ await writeFrame(frame);
89
+ while (true) {
90
+ const response = await readFrame(readExactBytes);
91
+ if (await handleServerPush(response)) {
92
+ continue;
93
+ }
94
+ return response;
95
+ }
96
+ },
97
+ close() {
98
+ socket.end();
99
+ },
100
+ };
101
+ }
@@ -0,0 +1,9 @@
1
+ import type { Frame, MicroHostInvokePayload, WireUdfScalarValue } from './wire.js';
2
+ export type MicroHostHandler = (payload: MicroHostInvokePayload) => WireUdfScalarValue | Promise<WireUdfScalarValue>;
3
+ export type Transport = {
4
+ send(frame: Frame): Promise<Frame>;
5
+ close(): void;
6
+ setMicroHostHandler?(handler: MicroHostHandler | undefined): void;
7
+ };
8
+ export declare function normalizeWsUrl(endpoint: string): string;
9
+ 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,89 @@
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 MicroRegister: 13;
31
+ readonly MicroRegisterOk: 14;
32
+ readonly MicroHostInvoke: 15;
33
+ readonly MicroHostInvokeOk: 16;
34
+ readonly ScalarCall: 17;
35
+ readonly ScalarCallOk: 18;
36
+ readonly Error: 255;
37
+ };
38
+ /** Phase-1 scalar kinds on the YY wire micro register path. */
39
+ export type WireUdfScalarKind = 'null' | 'bool' | 'i64' | 'text';
40
+ /** Phase-1 scalar value on the YY wire host invoke path. */
41
+ export type WireUdfScalarValue = null | boolean | number | string;
42
+ export interface MicroHostInvokePayload {
43
+ hostId: number;
44
+ handleVersion: number;
45
+ functionId: string;
46
+ args: readonly WireUdfScalarValue[];
47
+ }
48
+ export type MsgTypeCode = (typeof MsgType)[keyof typeof MsgType];
49
+ export type ProductMagic = 'YYDB' | 'YYDS';
50
+ export interface Frame {
51
+ msgType: number;
52
+ flags: number;
53
+ requestId: number;
54
+ body: Uint8Array;
55
+ /** Product magic observed on decode (backend self-claim; optional to ignore). */
56
+ product?: ProductMagic;
57
+ }
58
+ export declare function encodeFrame(frame: Frame, product?: ProductMagic): Uint8Array;
59
+ export declare function decodeFrame(bytes: Uint8Array): Frame;
60
+ export declare function pushU32(out: number[], value: number): void;
61
+ export declare function pushBytes(out: number[], bytes: Uint8Array): void;
62
+ export declare function encodeSchemaEnsure(version: number, document: string): Uint8Array;
63
+ export declare function encodeKvGet(key: string): Uint8Array;
64
+ export interface MicroRegisterPayload {
65
+ hostId: number;
66
+ handleVersion: number;
67
+ udfVersion: number;
68
+ name: string;
69
+ functionId: string;
70
+ args: readonly WireUdfScalarKind[];
71
+ returns: WireUdfScalarKind;
72
+ fingerprint: Uint8Array;
73
+ }
74
+ /** Decode a `MicroHostInvoke` body. */
75
+ export declare function decodeMicroHostInvoke(body: Uint8Array): MicroHostInvokePayload;
76
+ /** Encode a `MicroHostInvokeOk` body. */
77
+ export declare function encodeMicroHostInvokeOk(value: WireUdfScalarValue): Uint8Array;
78
+ /** Encode a `ScalarCall` body. */
79
+ export declare function encodeScalarCall(name: string, version: number, args: readonly WireUdfScalarValue[]): Uint8Array;
80
+ /** Decode a `ScalarCallOk` body. */
81
+ export declare function decodeScalarCallOk(body: Uint8Array): WireUdfScalarValue;
82
+ export declare function encodeMicroRegister(payload: MicroRegisterPayload): Uint8Array;
83
+ export declare function encodeKvPut(key: string, value: Uint8Array): Uint8Array;
84
+ export interface SchemaVersion {
85
+ version: number;
86
+ document: string;
87
+ }
88
+ export declare function decodeSchemaGetOk(body: Uint8Array): SchemaVersion | null;
89
+ export declare function decodeKvGetOk(body: Uint8Array): Uint8Array | null;
package/dist/wire.js ADDED
@@ -0,0 +1,331 @@
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
+ MicroRegister: 13,
31
+ MicroRegisterOk: 14,
32
+ MicroHostInvoke: 15,
33
+ MicroHostInvokeOk: 16,
34
+ ScalarCall: 17,
35
+ ScalarCallOk: 18,
36
+ Error: 255,
37
+ };
38
+ function writeU16(view, offset, value) {
39
+ view.setUint16(offset, value, true);
40
+ }
41
+ function writeU32(view, offset, value) {
42
+ view.setUint32(offset, value, true);
43
+ }
44
+ function readU16(view, offset) {
45
+ return view.getUint16(offset, true);
46
+ }
47
+ function readU32(view, offset) {
48
+ return view.getUint32(offset, true);
49
+ }
50
+ function productBytes(product) {
51
+ return product === 'YYDS' ? MAGIC_YYDS : MAGIC_YYDB;
52
+ }
53
+ function parseProduct(bytes) {
54
+ const text = new TextDecoder().decode(bytes.subarray(0, 4));
55
+ if (text === 'YYDB' || text === 'YYDS') {
56
+ return text;
57
+ }
58
+ throw new Error('wire frame bad product magic (want YYDB|YYDS)');
59
+ }
60
+ function checkVersion(bytes) {
61
+ const version = new TextDecoder().decode(bytes.subarray(4, 8));
62
+ if (version !== '0000') {
63
+ throw new Error(`wire version not supported (got ${version}, want 0000)`);
64
+ }
65
+ }
66
+ export function encodeFrame(frame, product = 'YYDB') {
67
+ if (frame.body.byteLength > MAX_BODY_LEN) {
68
+ throw new Error('wire body too large');
69
+ }
70
+ const out = new Uint8Array(HEADER_LEN + frame.body.byteLength);
71
+ out.set(productBytes(product), 0);
72
+ out.set(WIRE_VERSION, 4);
73
+ const view = new DataView(out.buffer);
74
+ writeU16(view, 8, frame.msgType);
75
+ writeU16(view, 10, frame.flags);
76
+ writeU32(view, 12, frame.requestId);
77
+ writeU32(view, 16, frame.body.byteLength);
78
+ out.set(frame.body, HEADER_LEN);
79
+ return out;
80
+ }
81
+ export function decodeFrame(bytes) {
82
+ if (bytes.byteLength < HEADER_LEN) {
83
+ throw new Error('wire frame truncated header');
84
+ }
85
+ const product = parseProduct(bytes);
86
+ checkVersion(bytes);
87
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
88
+ const msgType = readU16(view, 8);
89
+ const flags = readU16(view, 10);
90
+ const requestId = readU32(view, 12);
91
+ const bodyLen = readU32(view, 16);
92
+ if (bodyLen > MAX_BODY_LEN) {
93
+ throw new Error('wire body too large');
94
+ }
95
+ const need = HEADER_LEN + bodyLen;
96
+ if (bytes.byteLength < need) {
97
+ throw new Error('wire frame truncated body');
98
+ }
99
+ return {
100
+ msgType,
101
+ flags,
102
+ requestId,
103
+ body: bytes.slice(HEADER_LEN, need),
104
+ product,
105
+ };
106
+ }
107
+ export function pushU32(out, value) {
108
+ out.push(value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff, (value >> 24) & 0xff);
109
+ }
110
+ export function pushBytes(out, bytes) {
111
+ pushU32(out, bytes.byteLength);
112
+ for (let i = 0; i < bytes.byteLength; i++) {
113
+ out.push(bytes[i]);
114
+ }
115
+ }
116
+ export function encodeSchemaEnsure(version, document) {
117
+ const out = [];
118
+ pushU32(out, version >>> 0);
119
+ pushBytes(out, new TextEncoder().encode(document));
120
+ return Uint8Array.from(out);
121
+ }
122
+ export function encodeKvGet(key) {
123
+ const out = [];
124
+ pushBytes(out, new TextEncoder().encode(key));
125
+ return Uint8Array.from(out);
126
+ }
127
+ function encodeUdfTypeTag(kind) {
128
+ switch (kind) {
129
+ case 'null':
130
+ return 0;
131
+ case 'bool':
132
+ return 1;
133
+ case 'i64':
134
+ return 2;
135
+ case 'text':
136
+ return 3;
137
+ default:
138
+ throw new Error(`unsupported wire udf type: ${kind}`);
139
+ }
140
+ }
141
+ function pushU64(out, value) {
142
+ const view = new DataView(new ArrayBuffer(8));
143
+ view.setBigUint64(0, BigInt(value), true);
144
+ out.push(...new Uint8Array(view.buffer));
145
+ }
146
+ /** Encode a `MicroRegister` body. */
147
+ function encodeUdfValue(out, value) {
148
+ if (value === null) {
149
+ out.push(0);
150
+ return;
151
+ }
152
+ if (typeof value === 'boolean') {
153
+ out.push(1, value ? 1 : 0);
154
+ return;
155
+ }
156
+ if (typeof value === 'number') {
157
+ out.push(2);
158
+ const view = new DataView(new ArrayBuffer(8));
159
+ view.setBigInt64(0, BigInt(value), true);
160
+ out.push(...new Uint8Array(view.buffer));
161
+ return;
162
+ }
163
+ if (typeof value === 'string') {
164
+ out.push(3);
165
+ pushBytes(out, new TextEncoder().encode(value));
166
+ return;
167
+ }
168
+ throw new Error('unsupported wire udf value');
169
+ }
170
+ function decodeUdfValue(body, offset) {
171
+ if (offset.n >= body.byteLength) {
172
+ throw new Error('wire scalar value missing tag');
173
+ }
174
+ const tag = body[offset.n];
175
+ offset.n += 1;
176
+ switch (tag) {
177
+ case 0:
178
+ return null;
179
+ case 1: {
180
+ if (offset.n >= body.byteLength) {
181
+ throw new Error('wire bool value truncated');
182
+ }
183
+ const value = body[offset.n] !== 0;
184
+ offset.n += 1;
185
+ return value;
186
+ }
187
+ case 2: {
188
+ if (offset.n + 8 > body.byteLength) {
189
+ throw new Error('wire i64 value truncated');
190
+ }
191
+ const view = new DataView(body.buffer, body.byteOffset, body.byteLength);
192
+ const value = Number(view.getBigInt64(offset.n, true));
193
+ offset.n += 8;
194
+ return value;
195
+ }
196
+ case 3: {
197
+ const len = readU32At(body, offset);
198
+ const bytes = readBytesAt(body, offset, len);
199
+ return new TextDecoder().decode(bytes);
200
+ }
201
+ default:
202
+ throw new Error('wire scalar value has invalid type tag');
203
+ }
204
+ }
205
+ function encodeUdfValues(values) {
206
+ if (values.length > 255) {
207
+ throw new Error('wire scalar arg count exceeds u8');
208
+ }
209
+ const out = [values.length];
210
+ for (const value of values) {
211
+ encodeUdfValue(out, value);
212
+ }
213
+ return out;
214
+ }
215
+ function decodeUdfValues(body, offset) {
216
+ if (offset.n >= body.byteLength) {
217
+ throw new Error('wire scalar args missing count');
218
+ }
219
+ const count = body[offset.n];
220
+ offset.n += 1;
221
+ const values = [];
222
+ for (let index = 0; index < count; index += 1) {
223
+ values.push(decodeUdfValue(body, offset));
224
+ }
225
+ return values;
226
+ }
227
+ /** Decode a `MicroHostInvoke` body. */
228
+ export function decodeMicroHostInvoke(body) {
229
+ const offset = { n: 0 };
230
+ const hostId = Number(readU64At(body, offset));
231
+ const handleVersion = readU32At(body, offset);
232
+ const functionIdLen = readU32At(body, offset);
233
+ const functionId = new TextDecoder().decode(readBytesAt(body, offset, functionIdLen));
234
+ const args = decodeUdfValues(body, offset);
235
+ return { hostId, handleVersion, functionId, args };
236
+ }
237
+ /** Encode a `MicroHostInvokeOk` body. */
238
+ export function encodeMicroHostInvokeOk(value) {
239
+ const out = [];
240
+ encodeUdfValue(out, value);
241
+ return Uint8Array.from(out);
242
+ }
243
+ /** Encode a `ScalarCall` body. */
244
+ export function encodeScalarCall(name, version, args) {
245
+ const out = [];
246
+ pushU32(out, version >>> 0);
247
+ pushBytes(out, new TextEncoder().encode(name));
248
+ out.push(...encodeUdfValues(args));
249
+ return Uint8Array.from(out);
250
+ }
251
+ /** Decode a `ScalarCallOk` body. */
252
+ export function decodeScalarCallOk(body) {
253
+ return decodeUdfValue(body, { n: 0 });
254
+ }
255
+ function readU64At(body, offset) {
256
+ if (offset.n + 8 > body.byteLength) {
257
+ throw new Error('wire body truncated u64');
258
+ }
259
+ const view = new DataView(body.buffer, body.byteOffset, body.byteLength);
260
+ const value = view.getBigUint64(offset.n, true);
261
+ offset.n += 8;
262
+ return value;
263
+ }
264
+ export function encodeMicroRegister(payload) {
265
+ if (payload.fingerprint.byteLength !== 32) {
266
+ throw new Error('micro register fingerprint must be 32 bytes');
267
+ }
268
+ const out = [];
269
+ pushU64(out, payload.hostId);
270
+ pushU32(out, payload.handleVersion);
271
+ pushU32(out, payload.udfVersion);
272
+ pushBytes(out, new TextEncoder().encode(payload.name));
273
+ pushBytes(out, new TextEncoder().encode(payload.functionId));
274
+ if (payload.args.length > 255) {
275
+ throw new Error('micro register supports at most 255 args');
276
+ }
277
+ out.push(payload.args.length);
278
+ for (const arg of payload.args) {
279
+ out.push(encodeUdfTypeTag(arg));
280
+ }
281
+ out.push(encodeUdfTypeTag(payload.returns));
282
+ out.push(...payload.fingerprint);
283
+ return Uint8Array.from(out);
284
+ }
285
+ export function encodeKvPut(key, value) {
286
+ const out = [];
287
+ pushBytes(out, new TextEncoder().encode(key));
288
+ pushBytes(out, value);
289
+ return Uint8Array.from(out);
290
+ }
291
+ function readU32At(body, offset) {
292
+ if (offset.n + 4 > body.byteLength) {
293
+ throw new Error('wire body truncated u32');
294
+ }
295
+ const view = new DataView(body.buffer, body.byteOffset, body.byteLength);
296
+ const value = readU32(view, offset.n);
297
+ offset.n += 4;
298
+ return value;
299
+ }
300
+ function readBytesAt(body, offset, len) {
301
+ if (offset.n + len > body.byteLength) {
302
+ throw new Error('wire body truncated bytes');
303
+ }
304
+ const slice = body.slice(offset.n, offset.n + len);
305
+ offset.n += len;
306
+ return slice;
307
+ }
308
+ export function decodeSchemaGetOk(body) {
309
+ if (body.byteLength === 0) {
310
+ throw new Error('schema get ok empty');
311
+ }
312
+ if (body[0] === 0) {
313
+ return null;
314
+ }
315
+ const offset = { n: 1 };
316
+ const version = readU32At(body, offset);
317
+ const docLen = readU32At(body, offset);
318
+ const doc = readBytesAt(body, offset, docLen);
319
+ return { version, document: new TextDecoder().decode(doc) };
320
+ }
321
+ export function decodeKvGetOk(body) {
322
+ if (body.byteLength === 0) {
323
+ throw new Error('kv get ok empty');
324
+ }
325
+ if (body[0] === 0) {
326
+ return null;
327
+ }
328
+ const offset = { n: 1 };
329
+ const valLen = readU32At(body, offset);
330
+ return readBytesAt(body, offset, valLen);
331
+ }
package/dist/ws.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { type Transport } from './transport.js';
2
+ /** Browser / any host with WebSocket — path `/wire`, binary frames. */
3
+ export declare function openWebSocketTransport(endpoint: string): Promise<Transport>;
package/dist/ws.js ADDED
@@ -0,0 +1,68 @@
1
+ import { decodeFrame, decodeMicroHostInvoke, encodeFrame, encodeMicroHostInvokeOk, MsgType } 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}`)), {
11
+ once: true,
12
+ });
13
+ });
14
+ let microHostHandler;
15
+ const buffer = [];
16
+ const waiters = [];
17
+ ws.addEventListener('message', (event) => {
18
+ const data = event.data instanceof ArrayBuffer ? new Uint8Array(event.data) : new Uint8Array(event.data);
19
+ buffer.push(decodeFrame(data));
20
+ for (const wake of waiters.splice(0)) {
21
+ wake();
22
+ }
23
+ });
24
+ async function readFrame() {
25
+ while (buffer.length === 0) {
26
+ await new Promise((resolve) => waiters.push(resolve));
27
+ }
28
+ return buffer.shift();
29
+ }
30
+ async function writeFrame(frame) {
31
+ ws.send(encodeFrame(frame));
32
+ }
33
+ async function handleServerPush(frame) {
34
+ if (frame.msgType !== MsgType.MicroHostInvoke) {
35
+ return false;
36
+ }
37
+ if (!microHostHandler) {
38
+ throw new Error('received MicroHostInvoke without a micro host handler');
39
+ }
40
+ const payload = decodeMicroHostInvoke(frame.body);
41
+ const result = await microHostHandler(payload);
42
+ await writeFrame({
43
+ msgType: MsgType.MicroHostInvokeOk,
44
+ flags: 0,
45
+ requestId: frame.requestId,
46
+ body: encodeMicroHostInvokeOk(result),
47
+ });
48
+ return true;
49
+ }
50
+ return {
51
+ setMicroHostHandler(handler) {
52
+ microHostHandler = handler;
53
+ },
54
+ async send(frame) {
55
+ await writeFrame(frame);
56
+ while (true) {
57
+ const response = await readFrame();
58
+ if (await handleServerPush(response)) {
59
+ continue;
60
+ }
61
+ return response;
62
+ }
63
+ },
64
+ close() {
65
+ ws.close();
66
+ },
67
+ };
68
+ }
package/package.json CHANGED
@@ -1,14 +1,53 @@
1
1
  {
2
2
  "name": "@yydb/yydb-client",
3
- "version": "0.0.0",
4
- "description": "Placeholder — YY wire TypeScript client",
5
- "license": "MIT",
6
- "private": false,
7
- "publishConfig": {
8
- "access": "public"
3
+ "version": "0.0.2",
4
+ "description": "TypeScript client for the YYDB WebSocket and TCP wire protocol",
5
+ "license": "MPL-2.0",
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.git",
30
+ "directory": "projects/packages/yydb-client"
31
+ },
32
+ "homepage": "https://github.com/yy-database/yydb/tree/dev/projects/packages/yydb-client#readme",
33
+ "bugs": {
34
+ "url": "https://github.com/yy-database/yydb/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
  }