@yydb/yydb-client 0.0.1 → 0.0.3

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,26 +1,79 @@
1
1
  # `@yydb/yydb-client`
2
2
 
3
- TypeScript client for the **YY wire protocol**. Used by `yydb-webui` and
4
- `homepage`.
3
+ **Lightweight TypeScript client for the YY wire protocol (WebSocket or TCP).**
5
4
 
6
- Frame prefix:
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
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)
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)
11
12
 
12
- | Entry | Transport |
13
- | --- | --- |
14
- | `@yydb/yydb-client` | WebSocket (`/wire`) — browsers / WebUI |
15
- | `@yydb/yydb-client/node` | Node TCP (or `ws://` WebSocket) |
13
+ ## Install
16
14
 
17
- Rust crates in this repo:
15
+ ```bash
16
+ npm install @yydb/yydb-client
17
+ ```
18
18
 
19
- - `backends/yydb` — YYDB **embedded** facade
20
- - `backends/yydb-client` — YYDB **remote** TCP client for `yydb serve`
19
+ ## Browser or WebSocket
21
20
 
22
- ## Security
21
+ Start a local host first:
23
22
 
24
- Reference serve has **no ACL**. Only connect to loopback endpoints you control.
23
+ ```text
24
+ yydb serve app.yydb --bind 127.0.0.1:7700
25
+ ```
25
26
 
26
- See [`../../documentation/serve-protocol.md`](../../documentation/serve-protocol.md).
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)
package/dist/client.d.ts CHANGED
@@ -1,7 +1,8 @@
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";
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';
5
6
  export interface ConnectOptions {
6
7
  /** `ws://127.0.0.1:7700/wire` or `127.0.0.1:7700` (upgraded to `/wire`). */
7
8
  endpoint: string;
@@ -28,5 +29,11 @@ export declare class Client {
28
29
  ensureSchema(version: number, document: string): Promise<void>;
29
30
  get(key: string): Promise<Uint8Array | null>;
30
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>;
31
38
  private roundtrip;
32
39
  }
package/dist/client.js CHANGED
@@ -1,6 +1,6 @@
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";
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
4
  /**
5
5
  * TypeScript client for YY wire (browser = WebSocket `/wire`).
6
6
  * Accepts product magic `YYDB` or `YYDS` as equivalent self-claims (same wire);
@@ -20,9 +20,9 @@ export class Client {
20
20
  }
21
21
  /** Open WebSocket to `yydb serve` `/wire` and Hello-handshake. */
22
22
  static async connect(options) {
23
- const endpoint = typeof options === "string" ? options : options.endpoint;
23
+ const endpoint = typeof options === 'string' ? options : options.endpoint;
24
24
  if (!endpoint.trim()) {
25
- throw new Error("empty serve endpoint");
25
+ throw new Error('empty serve endpoint');
26
26
  }
27
27
  const transport = await openWebSocketTransport(endpoint);
28
28
  const client = new Client(endpoint, transport);
@@ -55,9 +55,22 @@ export class Client {
55
55
  return decodeKvGetOk(response.body);
56
56
  }
57
57
  async put(key, value) {
58
- const bytes = typeof value === "string" ? new TextEncoder().encode(value) : value;
58
+ const bytes = typeof value === 'string' ? new TextEncoder().encode(value) : value;
59
59
  await this.roundtrip(MsgType.KvPut, encodeKvPut(key, bytes));
60
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
+ }
61
74
  async roundtrip(msgType, body) {
62
75
  const requestId = this.nextId++;
63
76
  const response = await this.transport.send({
@@ -67,7 +80,7 @@ export class Client {
67
80
  body,
68
81
  });
69
82
  if (response.requestId !== requestId) {
70
- throw new Error("wire response request_id mismatch");
83
+ throw new Error('wire response request_id mismatch');
71
84
  }
72
85
  if (response.msgType === MsgType.Error) {
73
86
  throw new Error(new TextDecoder().decode(response.body));
@@ -79,6 +92,8 @@ export class Client {
79
92
  [MsgType.SchemaEnsure]: MsgType.SchemaEnsureOk,
80
93
  [MsgType.KvGet]: MsgType.KvGetOk,
81
94
  [MsgType.KvPut]: MsgType.KvPutOk,
95
+ [MsgType.MicroRegister]: MsgType.MicroRegisterOk,
96
+ [MsgType.ScalarCall]: MsgType.ScalarCallOk,
82
97
  };
83
98
  const want = expectedOk[msgType];
84
99
  if (want !== undefined && response.msgType !== want) {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
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";
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 CHANGED
@@ -1,3 +1,3 @@
1
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";
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 CHANGED
@@ -1,7 +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";
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
6
  /** Node entry: TCP by default; `ws://` uses WebSocket. */
7
7
  export declare function connect(options: ConnectOptions | string): Promise<Client>;
package/dist/node.js CHANGED
@@ -1,22 +1,20 @@
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";
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
7
  function wantsWebSocket(endpoint) {
8
8
  const trimmed = endpoint.trim();
9
- return trimmed.startsWith("ws://") || trimmed.startsWith("wss://");
9
+ return trimmed.startsWith('ws://') || trimmed.startsWith('wss://');
10
10
  }
11
11
  /** Node entry: TCP by default; `ws://` uses WebSocket. */
12
12
  export async function connect(options) {
13
- const endpoint = typeof options === "string" ? options : options.endpoint;
13
+ const endpoint = typeof options === 'string' ? options : options.endpoint;
14
14
  if (!endpoint.trim()) {
15
- throw new Error("empty serve endpoint");
15
+ throw new Error('empty serve endpoint');
16
16
  }
17
- const transport = wantsWebSocket(endpoint)
18
- ? await openWebSocketTransport(endpoint)
19
- : await openTcpTransport(endpoint);
17
+ const transport = wantsWebSocket(endpoint) ? await openWebSocketTransport(endpoint) : await openTcpTransport(endpoint);
20
18
  const client = Client.fromTransport(wantsWebSocket(endpoint) ? normalizeWsUrl(endpoint) : endpoint, transport);
21
19
  await client.hello();
22
20
  return client;
package/dist/tcp.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import { type Transport } from "./transport.js";
1
+ import { type Transport } from './transport.js';
2
2
  /** Node.js TCP transport for `yydb serve` binary frames. */
3
3
  export declare function openTcpTransport(endpoint: string): Promise<Transport>;
package/dist/tcp.js CHANGED
@@ -1,28 +1,41 @@
1
- import { decodeFrame, encodeFrame } from "./wire.js";
2
- import { normalizeTcpHostPort } from "./transport.js";
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
+ }
3
15
  /** Node.js TCP transport for `yydb serve` binary frames. */
4
16
  export async function openTcpTransport(endpoint) {
5
- const net = await import("node:net");
17
+ const net = await import('node:net');
6
18
  const hostport = normalizeTcpHostPort(endpoint);
7
- const [host, portText] = hostport.includes("]:")
19
+ const [host, portText] = hostport.includes(']:')
8
20
  ? (() => {
9
21
  const rest = hostport.slice(1);
10
- const idx = rest.indexOf("]:");
22
+ const idx = rest.indexOf(']:');
11
23
  return [rest.slice(0, idx), rest.slice(idx + 2)];
12
24
  })()
13
- : hostport.split(":");
25
+ : hostport.split(':');
14
26
  const port = Number(portText);
15
27
  if (!host || !Number.isFinite(port)) {
16
28
  throw new Error(`invalid TCP endpoint: ${endpoint}`);
17
29
  }
18
30
  const socket = net.createConnection({ host, port });
19
31
  await new Promise((resolve, reject) => {
20
- socket.once("connect", () => resolve());
21
- socket.once("error", reject);
32
+ socket.once('connect', () => resolve());
33
+ socket.once('error', reject);
22
34
  });
23
35
  let buffer = new Uint8Array(0);
24
36
  const waiters = [];
25
- socket.on("data", (chunk) => {
37
+ let microHostHandler;
38
+ socket.on('data', (chunk) => {
26
39
  const next = new Uint8Array(buffer.byteLength + chunk.byteLength);
27
40
  next.set(buffer, 0);
28
41
  next.set(chunk, buffer.byteLength);
@@ -31,7 +44,7 @@ export async function openTcpTransport(endpoint) {
31
44
  wake();
32
45
  }
33
46
  });
34
- async function readExact(n) {
47
+ async function readExactBytes(n) {
35
48
  while (buffer.byteLength < n) {
36
49
  await new Promise((resolve) => waiters.push(resolve));
37
50
  }
@@ -39,24 +52,47 @@ export async function openTcpTransport(endpoint) {
39
52
  buffer = buffer.slice(n);
40
53
  return out;
41
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
+ }
42
83
  return {
84
+ setMicroHostHandler(handler) {
85
+ microHostHandler = handler;
86
+ },
43
87
  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);
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
+ }
60
96
  },
61
97
  close() {
62
98
  socket.end();
@@ -1,7 +1,9 @@
1
- import type { Frame } from "./wire.js";
1
+ import type { Frame, MicroHostInvokePayload, WireUdfScalarValue } from './wire.js';
2
+ export type MicroHostHandler = (payload: MicroHostInvokePayload) => WireUdfScalarValue | Promise<WireUdfScalarValue>;
2
3
  export type Transport = {
3
4
  send(frame: Frame): Promise<Frame>;
4
5
  close(): void;
6
+ setMicroHostHandler?(handler: MicroHostHandler | undefined): void;
5
7
  };
6
8
  export declare function normalizeWsUrl(endpoint: string): string;
7
9
  export declare function normalizeTcpHostPort(endpoint: string): string;
package/dist/transport.js CHANGED
@@ -1,23 +1,23 @@
1
1
  export function normalizeWsUrl(endpoint) {
2
2
  const trimmed = endpoint.trim();
3
- if (trimmed.startsWith("ws://") || trimmed.startsWith("wss://")) {
4
- if (trimmed.includes("/wire")) {
3
+ if (trimmed.startsWith('ws://') || trimmed.startsWith('wss://')) {
4
+ if (trimmed.includes('/wire')) {
5
5
  return trimmed;
6
6
  }
7
- return trimmed.replace(/\/$/, "") + "/wire";
7
+ return trimmed.replace(/\/$/, '') + '/wire';
8
8
  }
9
9
  const hostport = trimmed
10
- .replace(/^tcp:\/\//, "")
11
- .replace(/^http:\/\//, "")
12
- .split("/")[0];
10
+ .replace(/^tcp:\/\//, '')
11
+ .replace(/^http:\/\//, '')
12
+ .split('/')[0];
13
13
  return `ws://${hostport}/wire`;
14
14
  }
15
15
  export function normalizeTcpHostPort(endpoint) {
16
16
  const trimmed = endpoint
17
17
  .trim()
18
- .replace(/^tcp:\/\//, "")
19
- .replace(/^http:\/\//, "")
20
- .replace(/^ws:\/\//, "")
21
- .replace(/^wss:\/\//, "");
22
- return trimmed.split("/")[0];
18
+ .replace(/^tcp:\/\//, '')
19
+ .replace(/^http:\/\//, '')
20
+ .replace(/^ws:\/\//, '')
21
+ .replace(/^wss:\/\//, '');
22
+ return trimmed.split('/')[0];
23
23
  }
package/dist/wire.d.ts CHANGED
@@ -27,10 +27,26 @@ export declare const MsgType: {
27
27
  readonly KvGetOk: 10;
28
28
  readonly KvPut: 11;
29
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;
30
36
  readonly Error: 255;
31
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
+ }
32
48
  export type MsgTypeCode = (typeof MsgType)[keyof typeof MsgType];
33
- export type ProductMagic = "YYDB" | "YYDS";
49
+ export type ProductMagic = 'YYDB' | 'YYDS';
34
50
  export interface Frame {
35
51
  msgType: number;
36
52
  flags: number;
@@ -45,6 +61,25 @@ export declare function pushU32(out: number[], value: number): void;
45
61
  export declare function pushBytes(out: number[], bytes: Uint8Array): void;
46
62
  export declare function encodeSchemaEnsure(version: number, document: string): Uint8Array;
47
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;
48
83
  export declare function encodeKvPut(key: string, value: Uint8Array): Uint8Array;
49
84
  export interface SchemaVersion {
50
85
  version: number;
package/dist/wire.js CHANGED
@@ -4,14 +4,14 @@
4
4
  * then four ASCII version digits (`0000` current, `0001` next).
5
5
  * Frontends accept either magic and do not require product consistency.
6
6
  */
7
- export const MAGIC_YYDB = new TextEncoder().encode("YYDB");
8
- export const MAGIC_YYDS = new TextEncoder().encode("YYDS");
7
+ export const MAGIC_YYDB = new TextEncoder().encode('YYDB');
8
+ export const MAGIC_YYDS = new TextEncoder().encode('YYDS');
9
9
  /** @deprecated Use MAGIC_YYDB — encode default product. */
10
10
  export const MAGIC = MAGIC_YYDB;
11
11
  /** Current wire version digits. */
12
- export const WIRE_VERSION = new TextEncoder().encode("0000");
12
+ export const WIRE_VERSION = new TextEncoder().encode('0000');
13
13
  /** Next planned wire version (not spoken yet). */
14
- export const WIRE_VERSION_NEXT = new TextEncoder().encode("0001");
14
+ export const WIRE_VERSION_NEXT = new TextEncoder().encode('0001');
15
15
  export const HEADER_LEN = 20;
16
16
  export const MAX_BODY_LEN = 16 * 1024 * 1024;
17
17
  export const MsgType = {
@@ -27,6 +27,12 @@ export const MsgType = {
27
27
  KvGetOk: 10,
28
28
  KvPut: 11,
29
29
  KvPutOk: 12,
30
+ MicroRegister: 13,
31
+ MicroRegisterOk: 14,
32
+ MicroHostInvoke: 15,
33
+ MicroHostInvokeOk: 16,
34
+ ScalarCall: 17,
35
+ ScalarCallOk: 18,
30
36
  Error: 255,
31
37
  };
32
38
  function writeU16(view, offset, value) {
@@ -42,24 +48,24 @@ function readU32(view, offset) {
42
48
  return view.getUint32(offset, true);
43
49
  }
44
50
  function productBytes(product) {
45
- return product === "YYDS" ? MAGIC_YYDS : MAGIC_YYDB;
51
+ return product === 'YYDS' ? MAGIC_YYDS : MAGIC_YYDB;
46
52
  }
47
53
  function parseProduct(bytes) {
48
54
  const text = new TextDecoder().decode(bytes.subarray(0, 4));
49
- if (text === "YYDB" || text === "YYDS") {
55
+ if (text === 'YYDB' || text === 'YYDS') {
50
56
  return text;
51
57
  }
52
- throw new Error("wire frame bad product magic (want YYDB|YYDS)");
58
+ throw new Error('wire frame bad product magic (want YYDB|YYDS)');
53
59
  }
54
60
  function checkVersion(bytes) {
55
61
  const version = new TextDecoder().decode(bytes.subarray(4, 8));
56
- if (version !== "0000") {
62
+ if (version !== '0000') {
57
63
  throw new Error(`wire version not supported (got ${version}, want 0000)`);
58
64
  }
59
65
  }
60
- export function encodeFrame(frame, product = "YYDB") {
66
+ export function encodeFrame(frame, product = 'YYDB') {
61
67
  if (frame.body.byteLength > MAX_BODY_LEN) {
62
- throw new Error("wire body too large");
68
+ throw new Error('wire body too large');
63
69
  }
64
70
  const out = new Uint8Array(HEADER_LEN + frame.body.byteLength);
65
71
  out.set(productBytes(product), 0);
@@ -74,7 +80,7 @@ export function encodeFrame(frame, product = "YYDB") {
74
80
  }
75
81
  export function decodeFrame(bytes) {
76
82
  if (bytes.byteLength < HEADER_LEN) {
77
- throw new Error("wire frame truncated header");
83
+ throw new Error('wire frame truncated header');
78
84
  }
79
85
  const product = parseProduct(bytes);
80
86
  checkVersion(bytes);
@@ -84,11 +90,11 @@ export function decodeFrame(bytes) {
84
90
  const requestId = readU32(view, 12);
85
91
  const bodyLen = readU32(view, 16);
86
92
  if (bodyLen > MAX_BODY_LEN) {
87
- throw new Error("wire body too large");
93
+ throw new Error('wire body too large');
88
94
  }
89
95
  const need = HEADER_LEN + bodyLen;
90
96
  if (bytes.byteLength < need) {
91
- throw new Error("wire frame truncated body");
97
+ throw new Error('wire frame truncated body');
92
98
  }
93
99
  return {
94
100
  msgType,
@@ -118,6 +124,164 @@ export function encodeKvGet(key) {
118
124
  pushBytes(out, new TextEncoder().encode(key));
119
125
  return Uint8Array.from(out);
120
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
+ }
121
285
  export function encodeKvPut(key, value) {
122
286
  const out = [];
123
287
  pushBytes(out, new TextEncoder().encode(key));
@@ -126,7 +290,7 @@ export function encodeKvPut(key, value) {
126
290
  }
127
291
  function readU32At(body, offset) {
128
292
  if (offset.n + 4 > body.byteLength) {
129
- throw new Error("wire body truncated u32");
293
+ throw new Error('wire body truncated u32');
130
294
  }
131
295
  const view = new DataView(body.buffer, body.byteOffset, body.byteLength);
132
296
  const value = readU32(view, offset.n);
@@ -135,7 +299,7 @@ function readU32At(body, offset) {
135
299
  }
136
300
  function readBytesAt(body, offset, len) {
137
301
  if (offset.n + len > body.byteLength) {
138
- throw new Error("wire body truncated bytes");
302
+ throw new Error('wire body truncated bytes');
139
303
  }
140
304
  const slice = body.slice(offset.n, offset.n + len);
141
305
  offset.n += len;
@@ -143,7 +307,7 @@ function readBytesAt(body, offset, len) {
143
307
  }
144
308
  export function decodeSchemaGetOk(body) {
145
309
  if (body.byteLength === 0) {
146
- throw new Error("schema get ok empty");
310
+ throw new Error('schema get ok empty');
147
311
  }
148
312
  if (body[0] === 0) {
149
313
  return null;
@@ -156,7 +320,7 @@ export function decodeSchemaGetOk(body) {
156
320
  }
157
321
  export function decodeKvGetOk(body) {
158
322
  if (body.byteLength === 0) {
159
- throw new Error("kv get ok empty");
323
+ throw new Error('kv get ok empty');
160
324
  }
161
325
  if (body[0] === 0) {
162
326
  return null;
package/dist/ws.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import { type Transport } from "./transport.js";
1
+ import { type Transport } from './transport.js';
2
2
  /** Browser / any host with WebSocket — path `/wire`, binary frames. */
3
3
  export declare function openWebSocketTransport(endpoint: string): Promise<Transport>;
package/dist/ws.js CHANGED
@@ -1,45 +1,65 @@
1
- import { decodeFrame, encodeFrame } from "./wire.js";
2
- import { normalizeWsUrl } from "./transport.js";
1
+ import { decodeFrame, decodeMicroHostInvoke, encodeFrame, encodeMicroHostInvokeOk, MsgType } from './wire.js';
2
+ import { normalizeWsUrl } from './transport.js';
3
3
  /** Browser / any host with WebSocket — path `/wire`, binary frames. */
4
4
  export async function openWebSocketTransport(endpoint) {
5
5
  const url = normalizeWsUrl(endpoint);
6
6
  const ws = new WebSocket(url);
7
- ws.binaryType = "arraybuffer";
7
+ ws.binaryType = 'arraybuffer';
8
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 });
9
+ ws.addEventListener('open', () => resolve(), { once: true });
10
+ ws.addEventListener('error', () => reject(new Error(`websocket failed: ${url}`)), {
11
+ once: true,
12
+ });
11
13
  });
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
- }
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();
31
22
  }
32
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
+ }
33
50
  return {
34
- send(frame) {
35
- return new Promise((resolve, reject) => {
36
- if (pending) {
37
- reject(new Error("overlapping websocket requests"));
38
- 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;
39
60
  }
40
- pending = { resolve, reject };
41
- ws.send(encodeFrame(frame));
42
- });
61
+ return response;
62
+ }
43
63
  },
44
64
  close() {
45
65
  ws.close();
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@yydb/yydb-client",
3
- "version": "0.0.1",
4
- "description": "TypeScript client for YY wire protocol v1 (VOS-native; YYDB/YYDS/compatible hosts)",
5
- "license": "MIT",
3
+ "version": "0.0.3",
4
+ "description": "TypeScript client for the YYDB WebSocket and TCP wire protocol",
5
+ "license": "MPL-2.0",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
@@ -26,12 +26,12 @@
26
26
  },
27
27
  "repository": {
28
28
  "type": "git",
29
- "url": "git+https://github.com/yy-database/yydb.rs.git",
30
- "directory": "frontends/yydb-client"
29
+ "url": "git+https://github.com/yy-database/yydb.git",
30
+ "directory": "projects/packages/yydb-client"
31
31
  },
32
- "homepage": "https://github.com/yy-database/yydb.rs/tree/dev/frontends/yydb-client#readme",
32
+ "homepage": "https://github.com/yy-database/yydb/tree/dev/projects/packages/yydb-client#readme",
33
33
  "bugs": {
34
- "url": "https://github.com/yy-database/yydb.rs/issues"
34
+ "url": "https://github.com/yy-database/yydb/issues"
35
35
  },
36
36
  "keywords": [
37
37
  "yydb",