@polymathbase/polybase-client 0.0.0-stage → 0.8.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/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,71 @@
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.3
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
+ `search({kind: 'json', key_prefix: hex('tasks/'), path: ['status'], value: 'open'})`
51
+ and `search({kind: 'text', key_prefix: hex('notes/'), text: 'quick fox'})` call
52
+ `/api/search`: bounded current-head scalar equality or Unicode whole-word search,
53
+ without a subscription. This differs from `searchIndex`, which calls
54
+ `/api/index-search` for one named equality index, and from paginated typed queries.
55
+
56
+ `createIndex(name, prefix, path)` encodes its prefix as a JSON byte array because
57
+ the server's `index-create` route requires that format. Other key and
58
+ `key_prefix` wire fields use lowercase hex. The client handles this conversion.
59
+
60
+ Non-JSON HTTP errors retain their status in `ApiError.status` and response text
61
+ in `ApiError.body` (invalid UTF-8 is replaced). JSON error bodies stay parsed;
62
+ successful responses still require valid JSON.
63
+
64
+ The client supports explicit IPv4 loopback HTTP origins only, a 128 KiB request
65
+ bound, an 8 MiB response bound and a five-second request timeout. Keep the console
66
+ on loopback. Query optimizer estimates describe work units, not measured latency.
67
+
68
+ See the [Polybase documentation](https://base.polymathlabs.xyz/docs) for server
69
+ installation, limits and protocol guarantees. Client terms are in `LICENSE`.
70
+ The public [client source](https://github.com/ihabshea/polybase-downloads/tree/main/clients/typescript)
71
+ and release tags allow comparison with the registry archive; the engine is private.
@@ -0,0 +1,205 @@
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
+ /** A bounded current-head search, separate from named indexes and query pages. */
41
+ export type SearchQuery = {
42
+ kind: "json";
43
+ key_prefix: string;
44
+ path: string[];
45
+ value: Scalar;
46
+ } | {
47
+ kind: "text";
48
+ key_prefix: string;
49
+ text: string;
50
+ };
51
+ export interface SearchResult {
52
+ position: string;
53
+ indexed: boolean;
54
+ rows: Row[];
55
+ }
56
+ export interface OptimizerAlternative {
57
+ strategy: "scan" | "index_lookup" | "index_intersection" | "index_union";
58
+ index_names: string[];
59
+ estimated_candidates: number;
60
+ estimated_work: number;
61
+ }
62
+ export interface OptimizerExplain {
63
+ strategy: OptimizerAlternative["strategy"] | "retained_history_scan";
64
+ initial_strategy: OptimizerAlternative["strategy"] | "retained_history_scan";
65
+ index_names: string[];
66
+ alternatives: OptimizerAlternative[];
67
+ estimated_work: number;
68
+ estimated_range_rows: number;
69
+ statistics_epoch: number | null;
70
+ total_live_records: number;
71
+ statistics_rows: number;
72
+ definition_coverage: {
73
+ name: string;
74
+ prefix: number[];
75
+ records: number;
76
+ }[];
77
+ term_cardinalities: {
78
+ index_name: string;
79
+ path: string[];
80
+ value: Scalar;
81
+ cardinality: number;
82
+ }[];
83
+ actual_work: {
84
+ posting_steps: number;
85
+ canonical_lookups: number;
86
+ predicate_evaluations: number;
87
+ scan_rows: number;
88
+ restarts: number;
89
+ };
90
+ phase_timings_micros: {
91
+ planning: number;
92
+ index_execution: number;
93
+ scan_execution: number;
94
+ };
95
+ replan_reason: string | null;
96
+ calibration: {
97
+ enabled: boolean;
98
+ samples: number;
99
+ lookup_weight_units: number;
100
+ };
101
+ }
102
+ export interface QueryExplain {
103
+ plan: string;
104
+ position: string;
105
+ scanned_records: number;
106
+ scanned_rows: number;
107
+ matched_rows: number;
108
+ returned_rows: number;
109
+ elapsed_micros: number;
110
+ execution_plan: {
111
+ kind: "scan";
112
+ } | {
113
+ kind: "index_lookup";
114
+ index_name: string;
115
+ };
116
+ index_name: string | null;
117
+ fallback_reason: string | null;
118
+ candidate_count: number;
119
+ planning_rows: number;
120
+ index_build_rows: number;
121
+ index_build_micros: number;
122
+ total_rows_examined: number;
123
+ /** Added in 0.8.2; absent in responses from older servers. Estimated work is not elapsed time. */
124
+ optimizer?: OptimizerExplain;
125
+ }
126
+ export interface Page {
127
+ position: string;
128
+ rows: Row[];
129
+ next: string | null;
130
+ explain: QueryExplain;
131
+ }
132
+ export interface Session {
133
+ id: string;
134
+ instance: string;
135
+ token: string;
136
+ position: string;
137
+ rows: Row[];
138
+ }
139
+ export interface Change {
140
+ key: string;
141
+ before: string | null;
142
+ after: string | null;
143
+ }
144
+ export interface StreamPage {
145
+ commits: {
146
+ position: string;
147
+ changes: Change[];
148
+ }[];
149
+ token: string;
150
+ position: string;
151
+ head: string;
152
+ floor: string;
153
+ }
154
+ export interface RevisionRead {
155
+ value: string | null;
156
+ revision: string;
157
+ position: string;
158
+ }
159
+ export interface Receipt {
160
+ position: string;
161
+ durable: boolean;
162
+ wrote: boolean;
163
+ }
164
+ export declare class ApiError extends Error {
165
+ status: number;
166
+ body: unknown;
167
+ constructor(status: number, body: unknown);
168
+ }
169
+ export declare const hex: (value: Uint8Array | string) => string;
170
+ export declare function unhex(value: string): Uint8Array;
171
+ export declare class Client {
172
+ readonly base: string;
173
+ constructor(base?: string);
174
+ call<T>(route: string, body: unknown, signal?: AbortSignal): Promise<T>;
175
+ page(query: Query, cursor?: string | null, limit?: number, forceScan?: boolean): Promise<Page>;
176
+ readRevision(key: Uint8Array | string): Promise<RevisionRead>;
177
+ writeRevision(revision: string, value: Uint8Array | string | null): Promise<Receipt>;
178
+ subscribe(query: Query): Promise<Session>;
179
+ resume(token: string): Promise<{
180
+ id: string;
181
+ instance: string;
182
+ }>;
183
+ poll(session: Pick<Session, "id" | "instance">, token: string, signal?: AbortSignal): Promise<StreamPage>;
184
+ unsubscribe(session: Pick<Session, "id" | "instance">, token: string): Promise<unknown>;
185
+ snapshotAt(position: bigint): Promise<{
186
+ position: string;
187
+ rows: Row[];
188
+ }>;
189
+ backup(directory: string): Promise<unknown>;
190
+ verifyBackup(directory: string): Promise<unknown>;
191
+ /** The index-create route requires prefix bytes as a JSON array, not hex. */
192
+ createIndex(name: string, prefix: Uint8Array | string, path: string[]): Promise<unknown>;
193
+ indexes(): Promise<unknown>;
194
+ searchIndex(name: string, value: Scalar): Promise<{
195
+ position: string;
196
+ indexed: boolean;
197
+ rows: Row[];
198
+ }>;
199
+ dropIndex(name: string): Promise<unknown>;
200
+ search(query: SearchQuery): Promise<SearchResult>;
201
+ compactionAdvice(retainCommits: bigint): Promise<unknown>;
202
+ /** Yield one complete page at a time. Persist rows and token before requesting another.
203
+ * A cancellation after a write is not an abort. No transport retry or implicit resnapshot. */
204
+ stream(session: Pick<Session, "id" | "instance">, appliedToken: string, signal?: AbortSignal): AsyncGenerator<StreamPage>;
205
+ }
package/dist/client.js ADDED
@@ -0,0 +1,123 @@
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
+ if (!response.ok)
39
+ throw new ApiError(response.status, "");
40
+ throw new Error("missing response body");
41
+ }
42
+ const chunks = [];
43
+ let size = 0;
44
+ try {
45
+ while (true) {
46
+ const { done, value } = await reader.read();
47
+ if (done)
48
+ break;
49
+ size += value.length;
50
+ if (size > 8 * 1024 * 1024)
51
+ throw new Error("response exceeds 8 MiB");
52
+ chunks.push(value);
53
+ }
54
+ }
55
+ finally {
56
+ await reader.cancel();
57
+ reader.releaseLock();
58
+ }
59
+ const all = new Uint8Array(size);
60
+ let offset = 0;
61
+ for (const chunk of chunks) {
62
+ all.set(chunk, offset);
63
+ offset += chunk.length;
64
+ }
65
+ const text = new TextDecoder("utf-8", { fatal: response.ok }).decode(all);
66
+ let result;
67
+ try {
68
+ result = JSON.parse(text);
69
+ }
70
+ catch (error) {
71
+ if (response.ok)
72
+ throw error;
73
+ result = text;
74
+ }
75
+ if (!response.ok)
76
+ throw new ApiError(response.status, result);
77
+ return result;
78
+ }
79
+ page(query, cursor = null, limit = 128, forceScan = false) { return this.call("query-page", { query, cursor, limit, ...(forceScan ? { force_scan: true } : {}) }); }
80
+ readRevision(key) { return this.call("read-revision", { key: hex(key) }); }
81
+ writeRevision(revision, value) { return this.call("write-revision", { revision, value: value === null ? null : hex(value) }); }
82
+ subscribe(query) { return this.call("query", query); }
83
+ resume(token) { return this.call("resume", { token }); }
84
+ poll(session, token, signal) { return this.call("poll", { id: session.id, instance: session.instance, token }, signal); }
85
+ unsubscribe(session, token) { return this.call("unsubscribe", { id: session.id, instance: session.instance, token }); }
86
+ snapshotAt(position) { return this.call("snapshot-at", { position: position.toString() }); }
87
+ backup(directory) { return this.call("backup", { directory }); }
88
+ verifyBackup(directory) { return this.call("verify-backup", { directory }); }
89
+ /** The index-create route requires prefix bytes as a JSON array, not hex. */
90
+ createIndex(name, prefix, path) { return this.call("index-create", { name, prefix: Array.from(typeof prefix === "string" ? new TextEncoder().encode(prefix) : prefix), path }); }
91
+ indexes() { return this.call("index-list", {}); }
92
+ searchIndex(name, value) { return this.call("index-search", { name, value }); }
93
+ dropIndex(name) { return this.call("index-drop", { name }); }
94
+ search(query) { return this.call("search", query); }
95
+ compactionAdvice(retainCommits) { return this.call("compaction-advice", { retain_commits: retainCommits.toString() }); }
96
+ /** Yield one complete page at a time. Persist rows and token before requesting another.
97
+ * A cancellation after a write is not an abort. No transport retry or implicit resnapshot. */
98
+ async *stream(session, appliedToken, signal) {
99
+ let token = appliedToken;
100
+ try {
101
+ while (!signal?.aborted) {
102
+ const page = await this.poll(session, token, signal);
103
+ if (BigInt(page.position) < BigInt(tokenPosition(token)))
104
+ throw new Error("stream cursor regressed");
105
+ if (page.token !== token) {
106
+ yield page;
107
+ token = page.token;
108
+ }
109
+ else
110
+ await new Promise(resolve => { const done = () => { clearTimeout(timer); signal?.removeEventListener("abort", done); resolve(); }; const timer = setTimeout(done, 25); signal?.addEventListener("abort", done, { once: true }); });
111
+ }
112
+ }
113
+ finally {
114
+ await this.unsubscribe(session, token).catch(() => { });
115
+ }
116
+ }
117
+ }
118
+ function tokenPosition(token) {
119
+ const bytes = unhex(token);
120
+ if (bytes.length < 36 || new TextDecoder().decode(bytes.subarray(0, 8)) !== "ENDBTOK1")
121
+ throw new Error("invalid local cursor");
122
+ return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getBigUint64(24, true);
123
+ }
package/package.json CHANGED
@@ -1,6 +1,34 @@
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.3",
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
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/ihabshea/polybase-downloads.git",
22
+ "directory": "clients/typescript"
23
+ },
24
+ "keywords": ["polybase", "database", "client", "local"],
25
+ "publishConfig": {"access": "public", "registry": "https://registry.npmjs.org/"},
26
+ "engines": {"node": ">=24"},
27
+ "scripts": {
28
+ "test": "node --test client.test.mjs",
29
+ "check": "tsc --noEmit --strict --target ES2022 --module nodenext --lib ES2022,DOM client.ts",
30
+ "build": "tsc -p tsconfig.build.json",
31
+ "prepack": "npm run build"
32
+ },
33
+ "devDependencies": {"typescript": "5.9.3"}
34
+ }