@polymathbase/polybase-client 0.0.0-stage → 0.8.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ihabshea
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,55 @@
1
- # Temporary Holding Version
1
+ # Polybase local client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Typed HTTP client for a local Polybase console. This package contains compiled
4
+ ES modules and TypeScript declarations. It does not contain the database engine
5
+ or a hosted service. Requires Node.js 24 or newer and a separately installed
6
+ Polybase console; version 0.8.2 is the release qualification target.
7
+
8
+ ```sh
9
+ npm install @polymathbase/polybase-client@0.8.2
10
+ ```
11
+
12
+ Start `polybase-console /path/to/database 7340`, then:
13
+
14
+ ```js
15
+ import {Client, hex, unhex} from '@polymathbase/polybase-client';
16
+
17
+ const client = new Client('http://127.0.0.1:7340');
18
+ const read = await client.readRevision('tasks/1');
19
+ const receipt = await client.writeRevision(
20
+ read.revision, JSON.stringify({status: 'open'}),
21
+ );
22
+ const page = await client.page({
23
+ key_prefix: hex('tasks/'), predicate: 'json', value: '',
24
+ json: {op: 'eq', path: ['status'], value: 'open'},
25
+ });
26
+ console.log(receipt.position, page.explain);
27
+ for (const row of page.rows) {
28
+ console.log(new TextDecoder().decode(unhex(row.value)));
29
+ }
30
+ ```
31
+
32
+ `readRevision` returns an opaque revision for a conditional `writeRevision`.
33
+ An HTTP 409 reports a stale revision. A transport timeout or cancellation can
34
+ leave a write's outcome uncertain: the server can still commit. The client never
35
+ automatically retries writes. Reconcile application state before another write.
36
+
37
+ Query pages have a captured position and an optional continuation cursor.
38
+ Retain the query and use `client.page(query, page.next, limit)` while `next` is
39
+ non-null. Keys and values in returned rows are lowercase hexadecimal bytes.
40
+ Positions are decimal strings; use `bigint`, never a JavaScript `number`, for
41
+ position arithmetic. Requests reject unsafe integer inputs before transport.
42
+
43
+ `subscribe(query)` captures initial rows and a token. Persist those together,
44
+ then consume `client.stream(session, savedToken, abortSignal)`. Apply each complete
45
+ page of commit transitions and persist its token atomically before requesting
46
+ the next page. On restart call `resume(savedToken)` and explicitly continue from
47
+ the last applied token. Delivery can repeat, tokens can expire, and the client
48
+ does not silently take a new snapshot. Streaming uses polling on consumer demand.
49
+
50
+ The client supports explicit IPv4 loopback HTTP origins only, a 128 KiB request
51
+ bound, an 8 MiB response bound and a five-second request timeout. Keep the console
52
+ on loopback. Query optimizer estimates describe work units, not measured latency.
53
+
54
+ See the [Polybase documentation](https://base.polymathlabs.xyz/docs) for server
55
+ installation, limits and protocol guarantees. Client terms are in `LICENSE`.
@@ -0,0 +1,187 @@
1
+ /** Local loopback client. Writes are never automatically retried. */
2
+ export type Scalar = null | boolean | number | string;
3
+ export type JsonPredicate = {
4
+ op: "eq" | "ne" | "lt" | "le" | "gt" | "ge";
5
+ path: string[];
6
+ value: Scalar;
7
+ } | {
8
+ op: "exists";
9
+ path: string[];
10
+ present: boolean;
11
+ } | {
12
+ op: "type";
13
+ path: string[];
14
+ kind: "null" | "boolean" | "number" | "string" | "array" | "object";
15
+ } | {
16
+ op: "prefix";
17
+ path: string[];
18
+ value: string;
19
+ } | {
20
+ op: "all" | "any";
21
+ predicates: JsonPredicate[];
22
+ } | {
23
+ op: "not";
24
+ predicate: JsonPredicate;
25
+ };
26
+ export type Query = {
27
+ key_prefix: string;
28
+ predicate: "any" | "equals" | "prefix";
29
+ value: string;
30
+ } | {
31
+ key_prefix: string;
32
+ predicate: "json";
33
+ value: "";
34
+ json: JsonPredicate;
35
+ };
36
+ export interface Row {
37
+ key: string;
38
+ value: string;
39
+ }
40
+ export interface OptimizerAlternative {
41
+ strategy: "scan" | "index_lookup" | "index_intersection" | "index_union";
42
+ index_names: string[];
43
+ estimated_candidates: number;
44
+ estimated_work: number;
45
+ }
46
+ export interface OptimizerExplain {
47
+ strategy: OptimizerAlternative["strategy"] | "retained_history_scan";
48
+ initial_strategy: OptimizerAlternative["strategy"] | "retained_history_scan";
49
+ index_names: string[];
50
+ alternatives: OptimizerAlternative[];
51
+ estimated_work: number;
52
+ estimated_range_rows: number;
53
+ statistics_epoch: number | null;
54
+ total_live_records: number;
55
+ statistics_rows: number;
56
+ definition_coverage: {
57
+ name: string;
58
+ prefix: number[];
59
+ records: number;
60
+ }[];
61
+ term_cardinalities: {
62
+ index_name: string;
63
+ path: string[];
64
+ value: Scalar;
65
+ cardinality: number;
66
+ }[];
67
+ actual_work: {
68
+ posting_steps: number;
69
+ canonical_lookups: number;
70
+ predicate_evaluations: number;
71
+ scan_rows: number;
72
+ restarts: number;
73
+ };
74
+ phase_timings_micros: {
75
+ planning: number;
76
+ index_execution: number;
77
+ scan_execution: number;
78
+ };
79
+ replan_reason: string | null;
80
+ calibration: {
81
+ enabled: boolean;
82
+ samples: number;
83
+ lookup_weight_units: number;
84
+ };
85
+ }
86
+ export interface QueryExplain {
87
+ plan: string;
88
+ position: string;
89
+ scanned_records: number;
90
+ scanned_rows: number;
91
+ matched_rows: number;
92
+ returned_rows: number;
93
+ elapsed_micros: number;
94
+ execution_plan: {
95
+ kind: "scan";
96
+ } | {
97
+ kind: "index_lookup";
98
+ index_name: string;
99
+ };
100
+ index_name: string | null;
101
+ fallback_reason: string | null;
102
+ candidate_count: number;
103
+ planning_rows: number;
104
+ index_build_rows: number;
105
+ index_build_micros: number;
106
+ total_rows_examined: number;
107
+ /** Added in 0.8.2; absent in responses from older servers. Estimated work is not elapsed time. */
108
+ optimizer?: OptimizerExplain;
109
+ }
110
+ export interface Page {
111
+ position: string;
112
+ rows: Row[];
113
+ next: string | null;
114
+ explain: QueryExplain;
115
+ }
116
+ export interface Session {
117
+ id: string;
118
+ instance: string;
119
+ token: string;
120
+ position: string;
121
+ rows: Row[];
122
+ }
123
+ export interface Change {
124
+ key: string;
125
+ before: string | null;
126
+ after: string | null;
127
+ }
128
+ export interface StreamPage {
129
+ commits: {
130
+ position: string;
131
+ changes: Change[];
132
+ }[];
133
+ token: string;
134
+ position: string;
135
+ head: string;
136
+ floor: string;
137
+ }
138
+ export interface RevisionRead {
139
+ value: string | null;
140
+ revision: string;
141
+ position: string;
142
+ }
143
+ export interface Receipt {
144
+ position: string;
145
+ durable: boolean;
146
+ wrote: boolean;
147
+ }
148
+ export declare class ApiError extends Error {
149
+ status: number;
150
+ body: unknown;
151
+ constructor(status: number, body: unknown);
152
+ }
153
+ export declare const hex: (value: Uint8Array | string) => string;
154
+ export declare function unhex(value: string): Uint8Array;
155
+ export declare class Client {
156
+ readonly base: string;
157
+ constructor(base?: string);
158
+ call<T>(route: string, body: unknown, signal?: AbortSignal): Promise<T>;
159
+ page(query: Query, cursor?: string | null, limit?: number, forceScan?: boolean): Promise<Page>;
160
+ readRevision(key: Uint8Array | string): Promise<RevisionRead>;
161
+ writeRevision(revision: string, value: Uint8Array | string | null): Promise<Receipt>;
162
+ subscribe(query: Query): Promise<Session>;
163
+ resume(token: string): Promise<{
164
+ id: string;
165
+ instance: string;
166
+ }>;
167
+ poll(session: Pick<Session, "id" | "instance">, token: string, signal?: AbortSignal): Promise<StreamPage>;
168
+ unsubscribe(session: Pick<Session, "id" | "instance">, token: string): Promise<unknown>;
169
+ snapshotAt(position: bigint): Promise<{
170
+ position: string;
171
+ rows: Row[];
172
+ }>;
173
+ backup(directory: string): Promise<unknown>;
174
+ verifyBackup(directory: string): Promise<unknown>;
175
+ createIndex(name: string, prefix: Uint8Array | string, path: string[]): Promise<unknown>;
176
+ indexes(): Promise<unknown>;
177
+ searchIndex(name: string, value: Scalar): Promise<{
178
+ position: string;
179
+ indexed: boolean;
180
+ rows: Row[];
181
+ }>;
182
+ dropIndex(name: string): Promise<unknown>;
183
+ compactionAdvice(retainCommits: bigint): Promise<unknown>;
184
+ /** Yield one complete page at a time. Persist rows and token before requesting another.
185
+ * A cancellation after a write is not an abort. No transport retry or implicit resnapshot. */
186
+ stream(session: Pick<Session, "id" | "instance">, appliedToken: string, signal?: AbortSignal): AsyncGenerator<StreamPage>;
187
+ }
package/dist/client.js ADDED
@@ -0,0 +1,109 @@
1
+ export class ApiError extends Error {
2
+ status;
3
+ body;
4
+ constructor(status, body) { super(`Polybase HTTP ${status}: ${JSON.stringify(body)}`); this.status = status; this.body = body; }
5
+ }
6
+ export const hex = (value) => [...(typeof value === "string" ? new TextEncoder().encode(value) : value)].map(b => b.toString(16).padStart(2, "0")).join("");
7
+ export function unhex(value) {
8
+ if (!/^(?:[0-9a-f]{2})*$/.test(value))
9
+ throw new Error("invalid hex");
10
+ return Uint8Array.from(value.match(/../g) || [], b => Number.parseInt(b, 16));
11
+ }
12
+ function safeNumbers(value, depth = 0) {
13
+ if (depth > 64)
14
+ throw new Error("request nesting exceeds 64");
15
+ if (typeof value === "number" && (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))))
16
+ throw new Error("unsafe JSON number; use strings for exact large integers");
17
+ if (value && typeof value === "object")
18
+ for (const child of Object.values(value))
19
+ safeNumbers(child, depth + 1);
20
+ }
21
+ export class Client {
22
+ base;
23
+ constructor(base = "http://127.0.0.1:7340") {
24
+ if (!/^http:\/\/127\.0\.0\.1:[1-9][0-9]*$/.test(base) || Number(new URL(base).port) > 65535)
25
+ throw new Error("use an IPv4 loopback origin with an explicit port");
26
+ this.base = base;
27
+ }
28
+ async call(route, body, signal) {
29
+ if (!/^[a-z-]+$/.test(route))
30
+ throw new Error("invalid route");
31
+ safeNumbers(body);
32
+ const payload = JSON.stringify(body);
33
+ if (new TextEncoder().encode(payload).length > 128 * 1024)
34
+ throw new Error("request exceeds 128 KiB");
35
+ const response = await fetch(`${this.base}/api/${route}`, { method: "POST", headers: { "Content-Type": "application/json", Origin: this.base }, body: payload, signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(5000)]) : AbortSignal.timeout(5000), redirect: "error" });
36
+ const reader = response.body?.getReader();
37
+ if (!reader)
38
+ throw new Error("missing response body");
39
+ const chunks = [];
40
+ let size = 0;
41
+ try {
42
+ while (true) {
43
+ const { done, value } = await reader.read();
44
+ if (done)
45
+ break;
46
+ size += value.length;
47
+ if (size > 8 * 1024 * 1024)
48
+ throw new Error("response exceeds 8 MiB");
49
+ chunks.push(value);
50
+ }
51
+ }
52
+ finally {
53
+ await reader.cancel();
54
+ reader.releaseLock();
55
+ }
56
+ const all = new Uint8Array(size);
57
+ let offset = 0;
58
+ for (const chunk of chunks) {
59
+ all.set(chunk, offset);
60
+ offset += chunk.length;
61
+ }
62
+ const result = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(all));
63
+ if (!response.ok)
64
+ throw new ApiError(response.status, result);
65
+ return result;
66
+ }
67
+ page(query, cursor = null, limit = 128, forceScan = false) { return this.call("query-page", { query, cursor, limit, ...(forceScan ? { force_scan: true } : {}) }); }
68
+ readRevision(key) { return this.call("read-revision", { key: hex(key) }); }
69
+ writeRevision(revision, value) { return this.call("write-revision", { revision, value: value === null ? null : hex(value) }); }
70
+ subscribe(query) { return this.call("query", query); }
71
+ resume(token) { return this.call("resume", { token }); }
72
+ poll(session, token, signal) { return this.call("poll", { id: session.id, instance: session.instance, token }, signal); }
73
+ unsubscribe(session, token) { return this.call("unsubscribe", { id: session.id, instance: session.instance, token }); }
74
+ snapshotAt(position) { return this.call("snapshot-at", { position: position.toString() }); }
75
+ backup(directory) { return this.call("backup", { directory }); }
76
+ verifyBackup(directory) { return this.call("verify-backup", { directory }); }
77
+ createIndex(name, prefix, path) { return this.call("index-create", { name, prefix: Array.from(typeof prefix === "string" ? new TextEncoder().encode(prefix) : prefix), path }); }
78
+ indexes() { return this.call("index-list", {}); }
79
+ searchIndex(name, value) { return this.call("index-search", { name, value }); }
80
+ dropIndex(name) { return this.call("index-drop", { name }); }
81
+ compactionAdvice(retainCommits) { return this.call("compaction-advice", { retain_commits: retainCommits.toString() }); }
82
+ /** Yield one complete page at a time. Persist rows and token before requesting another.
83
+ * A cancellation after a write is not an abort. No transport retry or implicit resnapshot. */
84
+ async *stream(session, appliedToken, signal) {
85
+ let token = appliedToken;
86
+ try {
87
+ while (!signal?.aborted) {
88
+ const page = await this.poll(session, token, signal);
89
+ if (BigInt(page.position) < BigInt(tokenPosition(token)))
90
+ throw new Error("stream cursor regressed");
91
+ if (page.token !== token) {
92
+ yield page;
93
+ token = page.token;
94
+ }
95
+ else
96
+ await new Promise(resolve => { const done = () => { clearTimeout(timer); signal?.removeEventListener("abort", done); resolve(); }; const timer = setTimeout(done, 25); signal?.addEventListener("abort", done, { once: true }); });
97
+ }
98
+ }
99
+ finally {
100
+ await this.unsubscribe(session, token).catch(() => { });
101
+ }
102
+ }
103
+ }
104
+ function tokenPosition(token) {
105
+ const bytes = unhex(token);
106
+ if (bytes.length < 36 || new TextDecoder().decode(bytes.subarray(0, 8)) !== "ENDBTOK1")
107
+ throw new Error("invalid local cursor");
108
+ return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getBigUint64(24, true);
109
+ }
package/package.json CHANGED
@@ -1,6 +1,29 @@
1
1
  {
2
2
  "name": "@polymathbase/polybase-client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.8.2",
4
+ "description": "Typed loopback HTTP client for the local Polybase database",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/client.js",
8
+ "types": "./dist/client.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/client.d.ts",
12
+ "import": "./dist/client.js"
13
+ }
14
+ },
15
+ "files": ["dist/client.js", "dist/client.d.ts", "README.md", "LICENSE"],
16
+ "sideEffects": false,
17
+ "author": "ihabshea",
18
+ "homepage": "https://base.polymathlabs.xyz/docs",
19
+ "keywords": ["polybase", "database", "client", "local"],
20
+ "publishConfig": {"access": "public", "registry": "https://registry.npmjs.org/"},
21
+ "engines": {"node": ">=24"},
22
+ "scripts": {
23
+ "test": "node --test client.test.mjs",
24
+ "check": "tsc --noEmit --strict --target ES2022 --module nodenext --lib ES2022,DOM client.ts",
25
+ "build": "tsc -p tsconfig.build.json",
26
+ "prepack": "npm run build"
27
+ },
28
+ "devDependencies": {"typescript": "5.9.3"}
29
+ }