@libsql/hrana-client
Advanced tools
Comparing version 0.2.0 to 0.3.0
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.IdAlloc = void 0; | ||
// An allocator of non-negative integer ids. | ||
@@ -40,3 +41,3 @@ // | ||
if (!this.#usedIds.delete(id)) { | ||
throw new Error("Internal error: freeing an id that is not allocated"); | ||
throw new Error("Freeing an id that is not allocated"); | ||
} | ||
@@ -50,2 +51,2 @@ // maintain the invariant of `#freeIds` | ||
} | ||
exports.default = IdAlloc; | ||
exports.IdAlloc = IdAlloc; |
@@ -13,5 +13,17 @@ "use strict"; | ||
})); | ||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { | ||
Object.defineProperty(o, "default", { enumerable: true, value: v }); | ||
}) : function(o, v) { | ||
o["default"] = v; | ||
}); | ||
var __exportStar = (this && this.__exportStar) || function(m, exports) { | ||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); | ||
}; | ||
var __importStar = (this && this.__importStar) || function (mod) { | ||
if (mod && mod.__esModule) return mod; | ||
var result = {}; | ||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
__setModuleDefault(result, mod); | ||
return result; | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
@@ -21,289 +33,22 @@ return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Stream = exports.Client = exports.open = exports.Stmt = void 0; | ||
exports.open = exports.Stream = exports.Stmt = exports.raw = exports.BatchCond = exports.BatchStep = exports.Batch = exports.Client = void 0; | ||
const isomorphic_ws_1 = __importDefault(require("isomorphic-ws")); | ||
const errors_js_1 = require("./errors.js"); | ||
const id_alloc_js_1 = __importDefault(require("./id_alloc.js")); | ||
const result_js_1 = require("./result.js"); | ||
const stmt_js_1 = require("./stmt.js"); | ||
const client_js_1 = require("./client.js"); | ||
var client_js_2 = require("./client.js"); | ||
Object.defineProperty(exports, "Client", { enumerable: true, get: function () { return client_js_2.Client; } }); | ||
__exportStar(require("./errors.js"), exports); | ||
var stmt_js_2 = require("./stmt.js"); | ||
Object.defineProperty(exports, "Stmt", { enumerable: true, get: function () { return stmt_js_2.Stmt; } }); | ||
var batch_js_1 = require("./batch.js"); | ||
Object.defineProperty(exports, "Batch", { enumerable: true, get: function () { return batch_js_1.Batch; } }); | ||
Object.defineProperty(exports, "BatchStep", { enumerable: true, get: function () { return batch_js_1.BatchStep; } }); | ||
Object.defineProperty(exports, "BatchCond", { enumerable: true, get: function () { return batch_js_1.BatchCond; } }); | ||
exports.raw = __importStar(require("./raw.js")); | ||
var stmt_js_1 = require("./stmt.js"); | ||
Object.defineProperty(exports, "Stmt", { enumerable: true, get: function () { return stmt_js_1.Stmt; } }); | ||
var stream_js_1 = require("./stream.js"); | ||
Object.defineProperty(exports, "Stream", { enumerable: true, get: function () { return stream_js_1.Stream; } }); | ||
/** Open a Hrana client connected to the given `url`. */ | ||
function open(url, jwt) { | ||
const socket = new isomorphic_ws_1.default(url, ["hrana1"]); | ||
return new Client(socket, jwt ?? null); | ||
return new client_js_1.Client(socket, jwt ?? null); | ||
} | ||
exports.open = open; | ||
/** A client that talks to a SQL server using the Hrana protocol over a WebSocket. */ | ||
class Client { | ||
#socket; | ||
// List of messages that we queue until the socket transitions from the CONNECTING to the OPEN state. | ||
#msgsWaitingToOpen; | ||
// Stores the error that caused us to close the client (and the socket). If we are not closed, this is | ||
// `undefined`. | ||
#closed; | ||
// Have we received a response to our "hello" from the server? | ||
#recvdHello; | ||
// A map from request id to the responses that we expect to receive from the server. | ||
#responseMap; | ||
// An allocator of request ids. | ||
#requestIdAlloc; | ||
// An allocator of stream ids. | ||
#streamIdAlloc; | ||
/** @private */ | ||
constructor(socket, jwt) { | ||
this.#socket = socket; | ||
this.#socket.binaryType = "arraybuffer"; | ||
this.#msgsWaitingToOpen = []; | ||
this.#closed = undefined; | ||
this.#recvdHello = false; | ||
this.#responseMap = new Map(); | ||
this.#requestIdAlloc = new id_alloc_js_1.default(); | ||
this.#streamIdAlloc = new id_alloc_js_1.default(); | ||
this.#socket.onopen = () => this.#onSocketOpen(); | ||
this.#socket.onclose = (event) => this.#onSocketClose(event); | ||
this.#socket.onerror = (event) => this.#onSocketError(event); | ||
this.#socket.onmessage = (event) => this.#onSocketMessage(event); | ||
this.#send({ "type": "hello", "jwt": jwt }); | ||
} | ||
// Send (or enqueue to send) a message to the server. | ||
#send(msg) { | ||
if (this.#closed !== undefined) { | ||
throw new errors_js_1.ClientError("Internal error: trying to send a message on a closed client"); | ||
} | ||
if (this.#socket.readyState >= isomorphic_ws_1.default.OPEN) { | ||
this.#sendToSocket(msg); | ||
} | ||
else { | ||
this.#msgsWaitingToOpen.push(msg); | ||
} | ||
} | ||
// The socket transitioned from CONNECTING to OPEN | ||
#onSocketOpen() { | ||
for (const msg of this.#msgsWaitingToOpen) { | ||
this.#sendToSocket(msg); | ||
} | ||
this.#msgsWaitingToOpen.length = 0; | ||
} | ||
#sendToSocket(msg) { | ||
this.#socket.send(JSON.stringify(msg)); | ||
} | ||
// Send a request to the server and invoke a callback when we get the response. | ||
#sendRequest(request, callbacks) { | ||
if (this.#closed !== undefined) { | ||
callbacks.errorCallback(new errors_js_1.ClosedError("Client is closed", this.#closed)); | ||
return; | ||
} | ||
const requestId = this.#requestIdAlloc.alloc(); | ||
this.#responseMap.set(requestId, { ...callbacks, type: request.type }); | ||
this.#send({ "type": "request", "request_id": requestId, request }); | ||
} | ||
// The socket encountered an error. | ||
#onSocketError(event) { | ||
const eventMessage = event.message; | ||
const message = eventMessage ?? "Connection was closed due to an error"; | ||
this.#setClosed(new errors_js_1.ClientError(message)); | ||
} | ||
// The socket was closed. | ||
#onSocketClose(event) { | ||
this.#setClosed(new errors_js_1.ClientError(`WebSocket was closed with code ${event.code}: ${event.reason}`)); | ||
} | ||
// Close the client with the given error. | ||
#setClosed(error) { | ||
if (this.#closed !== undefined) { | ||
return; | ||
} | ||
this.#closed = error; | ||
for (const [requestId, responseState] of this.#responseMap.entries()) { | ||
responseState.errorCallback(error); | ||
this.#requestIdAlloc.free(requestId); | ||
} | ||
this.#responseMap.clear(); | ||
this.#socket.close(); | ||
} | ||
// We received a message from the socket. | ||
#onSocketMessage(event) { | ||
if (typeof event.data !== "string") { | ||
this.#socket.close(3003, "Only string messages are accepted"); | ||
this.#setClosed(new errors_js_1.ProtoError("Received non-string message from server")); | ||
return; | ||
} | ||
try { | ||
this.#handleMsg(event.data); | ||
} | ||
catch (e) { | ||
this.#socket.close(3007, "Could not handle message"); | ||
this.#setClosed(e); | ||
} | ||
} | ||
// Handle a message from the server. | ||
#handleMsg(msgText) { | ||
const msg = JSON.parse(msgText); | ||
if (msg["type"] === "hello_ok" || msg["type"] === "hello_error") { | ||
if (this.#recvdHello) { | ||
throw new errors_js_1.ProtoError("Received a duplicated hello response"); | ||
} | ||
this.#recvdHello = true; | ||
if (msg["type"] === "hello_error") { | ||
throw (0, result_js_1.errorFromProto)(msg["error"]); | ||
} | ||
return; | ||
} | ||
else if (!this.#recvdHello) { | ||
throw new errors_js_1.ProtoError("Received a non-hello message before a hello response"); | ||
} | ||
if (msg["type"] === "response_ok") { | ||
const requestId = msg["request_id"]; | ||
const responseState = this.#responseMap.get(requestId); | ||
this.#responseMap.delete(requestId); | ||
if (responseState === undefined) { | ||
throw new errors_js_1.ProtoError("Received unexpected OK response"); | ||
} | ||
else if (responseState.type !== msg["response"]["type"]) { | ||
throw new errors_js_1.ProtoError("Received unexpected type of response"); | ||
} | ||
try { | ||
responseState.responseCallback(msg["response"]); | ||
} | ||
catch (e) { | ||
responseState.errorCallback(e); | ||
throw e; | ||
} | ||
} | ||
else if (msg["type"] === "response_error") { | ||
const requestId = msg["request_id"]; | ||
const responseState = this.#responseMap.get(requestId); | ||
this.#responseMap.delete(requestId); | ||
if (responseState === undefined) { | ||
throw new errors_js_1.ProtoError("Received unexpected error response"); | ||
} | ||
responseState.errorCallback((0, result_js_1.errorFromProto)(msg["error"])); | ||
} | ||
else { | ||
throw new errors_js_1.ProtoError("Received unexpected message type"); | ||
} | ||
} | ||
/** Open a {@link Stream}, a stream for executing SQL statements. */ | ||
openStream() { | ||
const streamId = this.#streamIdAlloc.alloc(); | ||
const streamState = { | ||
streamId, | ||
closed: undefined, | ||
}; | ||
const responseCallback = () => undefined; | ||
const errorCallback = (e) => this._closeStream(streamState, e); | ||
const request = { | ||
"type": "open_stream", | ||
"stream_id": streamId, | ||
}; | ||
this.#sendRequest(request, { responseCallback, errorCallback }); | ||
return new Stream(this, streamState); | ||
} | ||
// Make sure that the stream is closed. | ||
/** @private */ | ||
_closeStream(streamState, error) { | ||
if (streamState.closed !== undefined || this.#closed !== undefined) { | ||
return; | ||
} | ||
streamState.closed = error; | ||
const callback = () => { | ||
this.#streamIdAlloc.free(streamState.streamId); | ||
}; | ||
const request = { | ||
"type": "close_stream", | ||
"stream_id": streamState.streamId, | ||
}; | ||
this.#sendRequest(request, { responseCallback: callback, errorCallback: callback }); | ||
} | ||
// Execute a statement on a stream and invoke callbacks in `stmtState` when we get the results (or an | ||
// error). | ||
/** @private */ | ||
_execute(streamState, stmtState) { | ||
const responseCallback = (response) => { | ||
stmtState.resultCallback(response["result"]); | ||
}; | ||
const errorCallback = (error) => { | ||
stmtState.errorCallback(error); | ||
}; | ||
if (streamState.closed !== undefined) { | ||
errorCallback(new errors_js_1.ClosedError("Stream was closed", streamState.closed)); | ||
return; | ||
} | ||
else if (this.#closed !== undefined) { | ||
errorCallback(new errors_js_1.ClosedError("Client was closed", this.#closed)); | ||
return; | ||
} | ||
const request = { | ||
"type": "execute", | ||
"stream_id": streamState.streamId, | ||
"stmt": stmtState.stmt, | ||
}; | ||
this.#sendRequest(request, { responseCallback, errorCallback }); | ||
} | ||
/** Close the client and the WebSocket. */ | ||
close() { | ||
this.#setClosed(new errors_js_1.ClientError("Client was manually closed")); | ||
} | ||
} | ||
exports.Client = Client; | ||
/** A stream for executing SQL statements (a "database connection"). */ | ||
class Stream { | ||
#client; | ||
#state; | ||
/** @private */ | ||
constructor(client, state) { | ||
this.#client = client; | ||
this.#state = state; | ||
} | ||
/** Execute a raw Hrana statement. */ | ||
executeRaw(stmt) { | ||
return new Promise((resultCallback, errorCallback) => { | ||
this.#client._execute(this.#state, { stmt, resultCallback, errorCallback }); | ||
}); | ||
} | ||
/** Execute a statement and return rows. */ | ||
query(stmt) { | ||
return new Promise((rowsCallback, errorCallback) => { | ||
this.#client._execute(this.#state, { | ||
stmt: (0, stmt_js_1.stmtToProto)(stmt, true), | ||
resultCallback(result) { rowsCallback((0, result_js_1.rowsResultFromProto)(result)); }, | ||
errorCallback, | ||
}); | ||
}); | ||
} | ||
/** Execute a statement and return at most a single row. */ | ||
queryRow(stmt) { | ||
return new Promise((rowCallback, errorCallback) => { | ||
this.#client._execute(this.#state, { | ||
stmt: (0, stmt_js_1.stmtToProto)(stmt, true), | ||
resultCallback(result) { rowCallback((0, result_js_1.rowResultFromProto)(result)); }, | ||
errorCallback, | ||
}); | ||
}); | ||
} | ||
/** Execute a statement and return at most a single value. */ | ||
queryValue(stmt) { | ||
return new Promise((valueCallback, errorCallback) => { | ||
this.#client._execute(this.#state, { | ||
stmt: (0, stmt_js_1.stmtToProto)(stmt, true), | ||
resultCallback(result) { valueCallback((0, result_js_1.valueResultFromProto)(result)); }, | ||
errorCallback, | ||
}); | ||
}); | ||
} | ||
/** Execute a statement without returning rows. */ | ||
execute(stmt) { | ||
return new Promise((doneCallback, errorCallback) => { | ||
this.#client._execute(this.#state, { | ||
stmt: (0, stmt_js_1.stmtToProto)(stmt, false), | ||
resultCallback(result) { doneCallback((0, result_js_1.stmtResultFromProto)(result)); }, | ||
errorCallback, | ||
}); | ||
}); | ||
} | ||
/** Close the stream. */ | ||
close() { | ||
this.#client._closeStream(this.#state, new errors_js_1.ClientError("Stream was manually closed")); | ||
} | ||
} | ||
exports.Stream = Stream; |
@@ -14,2 +14,5 @@ "use strict"; | ||
else if (typeof value === "number") { | ||
if (!Number.isFinite(value)) { | ||
throw new errors_js_1.ClientError("Only finite numbers (not Infinity or NaN) can be passed as arguments"); | ||
} | ||
return { "type": "float", "value": +value }; | ||
@@ -20,2 +23,5 @@ } | ||
} | ||
else if (typeof value === "boolean") { | ||
return { "type": "integer", "value": value ? "1" : "0" }; | ||
} | ||
else if (value instanceof ArrayBuffer) { | ||
@@ -22,0 +28,0 @@ return { "type": "blob", "base64": js_base64_1.Base64.fromUint8Array(new Uint8Array(value)) }; |
@@ -1,2 +0,2 @@ | ||
export default class IdAlloc { | ||
export declare class IdAlloc { | ||
#private; | ||
@@ -3,0 +3,0 @@ constructor(); |
@@ -8,3 +8,3 @@ // An allocator of non-negative integer ids. | ||
// id). | ||
export default class IdAlloc { | ||
export class IdAlloc { | ||
// Set of all allocated ids | ||
@@ -39,3 +39,3 @@ #usedIds; | ||
if (!this.#usedIds.delete(id)) { | ||
throw new Error("Internal error: freeing an id that is not allocated"); | ||
throw new Error("Freeing an id that is not allocated"); | ||
} | ||
@@ -42,0 +42,0 @@ // maintain the invariant of `#freeIds` |
@@ -1,54 +0,15 @@ | ||
/// <reference types="ws" /> | ||
import WebSocket from "isomorphic-ws"; | ||
/// <reference types="node" /> | ||
import { Client } from "./client.js"; | ||
import type * as proto from "./proto.js"; | ||
import type { StmtResult, RowsResult, RowResult, ValueResult } from "./result.js"; | ||
import type { InStmt } from "./stmt.js"; | ||
export { Client } from "./client.js"; | ||
export * from "./errors.js"; | ||
export { Batch, BatchStep, BatchCond } from "./batch.js"; | ||
export * as raw from "./raw.js"; | ||
export type { StmtResult, RowsResult, RowResult, ValueResult, Row } from "./result.js"; | ||
export type { InStmt, InStmtArgs } from "./stmt.js"; | ||
export { Stmt } from "./stmt.js"; | ||
export { Stream } from "./stream.js"; | ||
export type { Value, InValue } from "./value.js"; | ||
export type { proto }; | ||
/** Open a Hrana client connected to the given `url`. */ | ||
export declare function open(url: string, jwt?: string): Client; | ||
/** A client that talks to a SQL server using the Hrana protocol over a WebSocket. */ | ||
export declare class Client { | ||
#private; | ||
/** @private */ | ||
constructor(socket: WebSocket, jwt: string | null); | ||
/** Open a {@link Stream}, a stream for executing SQL statements. */ | ||
openStream(): Stream; | ||
/** @private */ | ||
_closeStream(streamState: StreamState, error: Error): void; | ||
/** @private */ | ||
_execute(streamState: StreamState, stmtState: StmtState): void; | ||
/** Close the client and the WebSocket. */ | ||
close(): void; | ||
} | ||
interface StmtState { | ||
stmt: proto.Stmt; | ||
resultCallback: (_: proto.StmtResult) => void; | ||
errorCallback: (_: Error) => void; | ||
} | ||
interface StreamState { | ||
streamId: number; | ||
closed: Error | undefined; | ||
} | ||
/** A stream for executing SQL statements (a "database connection"). */ | ||
export declare class Stream { | ||
#private; | ||
/** @private */ | ||
constructor(client: Client, state: StreamState); | ||
/** Execute a raw Hrana statement. */ | ||
executeRaw(stmt: proto.Stmt): Promise<proto.StmtResult>; | ||
/** Execute a statement and return rows. */ | ||
query(stmt: InStmt): Promise<RowsResult>; | ||
/** Execute a statement and return at most a single row. */ | ||
queryRow(stmt: InStmt): Promise<RowResult>; | ||
/** Execute a statement and return at most a single value. */ | ||
queryValue(stmt: InStmt): Promise<ValueResult>; | ||
/** Execute a statement without returning rows. */ | ||
execute(stmt: InStmt): Promise<StmtResult>; | ||
/** Close the stream. */ | ||
close(): void; | ||
} | ||
export declare function open(url: string | URL, jwt?: string): Client; |
import WebSocket from "isomorphic-ws"; | ||
import { ClientError, ProtoError, ClosedError } from "./errors.js"; | ||
import IdAlloc from "./id_alloc.js"; | ||
import { rowsResultFromProto, rowResultFromProto, valueResultFromProto, stmtResultFromProto, errorFromProto, } from "./result.js"; | ||
import { stmtToProto } from "./stmt.js"; | ||
import { Client } from "./client.js"; | ||
export { Client } from "./client.js"; | ||
export * from "./errors.js"; | ||
export { Batch, BatchStep, BatchCond } from "./batch.js"; | ||
export * as raw from "./raw.js"; | ||
export { Stmt } from "./stmt.js"; | ||
export { Stream } from "./stream.js"; | ||
/** Open a Hrana client connected to the given `url`. */ | ||
@@ -13,272 +14,1 @@ export function open(url, jwt) { | ||
} | ||
/** A client that talks to a SQL server using the Hrana protocol over a WebSocket. */ | ||
export class Client { | ||
#socket; | ||
// List of messages that we queue until the socket transitions from the CONNECTING to the OPEN state. | ||
#msgsWaitingToOpen; | ||
// Stores the error that caused us to close the client (and the socket). If we are not closed, this is | ||
// `undefined`. | ||
#closed; | ||
// Have we received a response to our "hello" from the server? | ||
#recvdHello; | ||
// A map from request id to the responses that we expect to receive from the server. | ||
#responseMap; | ||
// An allocator of request ids. | ||
#requestIdAlloc; | ||
// An allocator of stream ids. | ||
#streamIdAlloc; | ||
/** @private */ | ||
constructor(socket, jwt) { | ||
this.#socket = socket; | ||
this.#socket.binaryType = "arraybuffer"; | ||
this.#msgsWaitingToOpen = []; | ||
this.#closed = undefined; | ||
this.#recvdHello = false; | ||
this.#responseMap = new Map(); | ||
this.#requestIdAlloc = new IdAlloc(); | ||
this.#streamIdAlloc = new IdAlloc(); | ||
this.#socket.onopen = () => this.#onSocketOpen(); | ||
this.#socket.onclose = (event) => this.#onSocketClose(event); | ||
this.#socket.onerror = (event) => this.#onSocketError(event); | ||
this.#socket.onmessage = (event) => this.#onSocketMessage(event); | ||
this.#send({ "type": "hello", "jwt": jwt }); | ||
} | ||
// Send (or enqueue to send) a message to the server. | ||
#send(msg) { | ||
if (this.#closed !== undefined) { | ||
throw new ClientError("Internal error: trying to send a message on a closed client"); | ||
} | ||
if (this.#socket.readyState >= WebSocket.OPEN) { | ||
this.#sendToSocket(msg); | ||
} | ||
else { | ||
this.#msgsWaitingToOpen.push(msg); | ||
} | ||
} | ||
// The socket transitioned from CONNECTING to OPEN | ||
#onSocketOpen() { | ||
for (const msg of this.#msgsWaitingToOpen) { | ||
this.#sendToSocket(msg); | ||
} | ||
this.#msgsWaitingToOpen.length = 0; | ||
} | ||
#sendToSocket(msg) { | ||
this.#socket.send(JSON.stringify(msg)); | ||
} | ||
// Send a request to the server and invoke a callback when we get the response. | ||
#sendRequest(request, callbacks) { | ||
if (this.#closed !== undefined) { | ||
callbacks.errorCallback(new ClosedError("Client is closed", this.#closed)); | ||
return; | ||
} | ||
const requestId = this.#requestIdAlloc.alloc(); | ||
this.#responseMap.set(requestId, { ...callbacks, type: request.type }); | ||
this.#send({ "type": "request", "request_id": requestId, request }); | ||
} | ||
// The socket encountered an error. | ||
#onSocketError(event) { | ||
const eventMessage = event.message; | ||
const message = eventMessage ?? "Connection was closed due to an error"; | ||
this.#setClosed(new ClientError(message)); | ||
} | ||
// The socket was closed. | ||
#onSocketClose(event) { | ||
this.#setClosed(new ClientError(`WebSocket was closed with code ${event.code}: ${event.reason}`)); | ||
} | ||
// Close the client with the given error. | ||
#setClosed(error) { | ||
if (this.#closed !== undefined) { | ||
return; | ||
} | ||
this.#closed = error; | ||
for (const [requestId, responseState] of this.#responseMap.entries()) { | ||
responseState.errorCallback(error); | ||
this.#requestIdAlloc.free(requestId); | ||
} | ||
this.#responseMap.clear(); | ||
this.#socket.close(); | ||
} | ||
// We received a message from the socket. | ||
#onSocketMessage(event) { | ||
if (typeof event.data !== "string") { | ||
this.#socket.close(3003, "Only string messages are accepted"); | ||
this.#setClosed(new ProtoError("Received non-string message from server")); | ||
return; | ||
} | ||
try { | ||
this.#handleMsg(event.data); | ||
} | ||
catch (e) { | ||
this.#socket.close(3007, "Could not handle message"); | ||
this.#setClosed(e); | ||
} | ||
} | ||
// Handle a message from the server. | ||
#handleMsg(msgText) { | ||
const msg = JSON.parse(msgText); | ||
if (msg["type"] === "hello_ok" || msg["type"] === "hello_error") { | ||
if (this.#recvdHello) { | ||
throw new ProtoError("Received a duplicated hello response"); | ||
} | ||
this.#recvdHello = true; | ||
if (msg["type"] === "hello_error") { | ||
throw errorFromProto(msg["error"]); | ||
} | ||
return; | ||
} | ||
else if (!this.#recvdHello) { | ||
throw new ProtoError("Received a non-hello message before a hello response"); | ||
} | ||
if (msg["type"] === "response_ok") { | ||
const requestId = msg["request_id"]; | ||
const responseState = this.#responseMap.get(requestId); | ||
this.#responseMap.delete(requestId); | ||
if (responseState === undefined) { | ||
throw new ProtoError("Received unexpected OK response"); | ||
} | ||
else if (responseState.type !== msg["response"]["type"]) { | ||
throw new ProtoError("Received unexpected type of response"); | ||
} | ||
try { | ||
responseState.responseCallback(msg["response"]); | ||
} | ||
catch (e) { | ||
responseState.errorCallback(e); | ||
throw e; | ||
} | ||
} | ||
else if (msg["type"] === "response_error") { | ||
const requestId = msg["request_id"]; | ||
const responseState = this.#responseMap.get(requestId); | ||
this.#responseMap.delete(requestId); | ||
if (responseState === undefined) { | ||
throw new ProtoError("Received unexpected error response"); | ||
} | ||
responseState.errorCallback(errorFromProto(msg["error"])); | ||
} | ||
else { | ||
throw new ProtoError("Received unexpected message type"); | ||
} | ||
} | ||
/** Open a {@link Stream}, a stream for executing SQL statements. */ | ||
openStream() { | ||
const streamId = this.#streamIdAlloc.alloc(); | ||
const streamState = { | ||
streamId, | ||
closed: undefined, | ||
}; | ||
const responseCallback = () => undefined; | ||
const errorCallback = (e) => this._closeStream(streamState, e); | ||
const request = { | ||
"type": "open_stream", | ||
"stream_id": streamId, | ||
}; | ||
this.#sendRequest(request, { responseCallback, errorCallback }); | ||
return new Stream(this, streamState); | ||
} | ||
// Make sure that the stream is closed. | ||
/** @private */ | ||
_closeStream(streamState, error) { | ||
if (streamState.closed !== undefined || this.#closed !== undefined) { | ||
return; | ||
} | ||
streamState.closed = error; | ||
const callback = () => { | ||
this.#streamIdAlloc.free(streamState.streamId); | ||
}; | ||
const request = { | ||
"type": "close_stream", | ||
"stream_id": streamState.streamId, | ||
}; | ||
this.#sendRequest(request, { responseCallback: callback, errorCallback: callback }); | ||
} | ||
// Execute a statement on a stream and invoke callbacks in `stmtState` when we get the results (or an | ||
// error). | ||
/** @private */ | ||
_execute(streamState, stmtState) { | ||
const responseCallback = (response) => { | ||
stmtState.resultCallback(response["result"]); | ||
}; | ||
const errorCallback = (error) => { | ||
stmtState.errorCallback(error); | ||
}; | ||
if (streamState.closed !== undefined) { | ||
errorCallback(new ClosedError("Stream was closed", streamState.closed)); | ||
return; | ||
} | ||
else if (this.#closed !== undefined) { | ||
errorCallback(new ClosedError("Client was closed", this.#closed)); | ||
return; | ||
} | ||
const request = { | ||
"type": "execute", | ||
"stream_id": streamState.streamId, | ||
"stmt": stmtState.stmt, | ||
}; | ||
this.#sendRequest(request, { responseCallback, errorCallback }); | ||
} | ||
/** Close the client and the WebSocket. */ | ||
close() { | ||
this.#setClosed(new ClientError("Client was manually closed")); | ||
} | ||
} | ||
/** A stream for executing SQL statements (a "database connection"). */ | ||
export class Stream { | ||
#client; | ||
#state; | ||
/** @private */ | ||
constructor(client, state) { | ||
this.#client = client; | ||
this.#state = state; | ||
} | ||
/** Execute a raw Hrana statement. */ | ||
executeRaw(stmt) { | ||
return new Promise((resultCallback, errorCallback) => { | ||
this.#client._execute(this.#state, { stmt, resultCallback, errorCallback }); | ||
}); | ||
} | ||
/** Execute a statement and return rows. */ | ||
query(stmt) { | ||
return new Promise((rowsCallback, errorCallback) => { | ||
this.#client._execute(this.#state, { | ||
stmt: stmtToProto(stmt, true), | ||
resultCallback(result) { rowsCallback(rowsResultFromProto(result)); }, | ||
errorCallback, | ||
}); | ||
}); | ||
} | ||
/** Execute a statement and return at most a single row. */ | ||
queryRow(stmt) { | ||
return new Promise((rowCallback, errorCallback) => { | ||
this.#client._execute(this.#state, { | ||
stmt: stmtToProto(stmt, true), | ||
resultCallback(result) { rowCallback(rowResultFromProto(result)); }, | ||
errorCallback, | ||
}); | ||
}); | ||
} | ||
/** Execute a statement and return at most a single value. */ | ||
queryValue(stmt) { | ||
return new Promise((valueCallback, errorCallback) => { | ||
this.#client._execute(this.#state, { | ||
stmt: stmtToProto(stmt, true), | ||
resultCallback(result) { valueCallback(valueResultFromProto(result)); }, | ||
errorCallback, | ||
}); | ||
}); | ||
} | ||
/** Execute a statement without returning rows. */ | ||
execute(stmt) { | ||
return new Promise((doneCallback, errorCallback) => { | ||
this.#client._execute(this.#state, { | ||
stmt: stmtToProto(stmt, false), | ||
resultCallback(result) { doneCallback(stmtResultFromProto(result)); }, | ||
errorCallback, | ||
}); | ||
}); | ||
} | ||
/** Close the stream. */ | ||
close() { | ||
this.#client._closeStream(this.#state, new ClientError("Stream was manually closed")); | ||
} | ||
} |
@@ -33,4 +33,4 @@ export type int32 = number; | ||
}; | ||
export type Request = OpenStreamReq | CloseStreamReq | ExecuteReq; | ||
export type Response = OpenStreamResp | CloseStreamResp | ExecuteResp; | ||
export type Request = OpenStreamReq | CloseStreamReq | ExecuteReq | BatchReq; | ||
export type Response = OpenStreamResp | CloseStreamResp | ExecuteResp | BatchResp; | ||
export type OpenStreamReq = { | ||
@@ -78,2 +78,38 @@ "type": "open_stream"; | ||
}; | ||
export type BatchReq = { | ||
"type": "batch"; | ||
"stream_id": int32; | ||
"batch": Batch; | ||
}; | ||
export type BatchResp = { | ||
"type": "batch"; | ||
"result": BatchResult; | ||
}; | ||
export type Batch = { | ||
"steps": Array<BatchStep>; | ||
}; | ||
export type BatchStep = { | ||
"condition"?: BatchCond | null; | ||
"stmt": Stmt; | ||
}; | ||
export type BatchResult = { | ||
"step_results": Array<StmtResult | null>; | ||
"step_errors": Array<Error | null>; | ||
}; | ||
export type BatchCond = { | ||
"type": "ok"; | ||
"step": int32; | ||
} | { | ||
"type": "error"; | ||
"step": int32; | ||
} | { | ||
"type": "not"; | ||
"cond": BatchCond; | ||
} | { | ||
"type": "and"; | ||
"conds": Array<BatchCond>; | ||
} | { | ||
"type": "or"; | ||
"conds": Array<BatchCond>; | ||
}; | ||
export type Value = { | ||
@@ -80,0 +116,0 @@ "type": "null"; |
@@ -5,5 +5,5 @@ import type * as proto from "./proto.js"; | ||
/** JavaScript values that you can send to the database as an argument. */ | ||
export type InValue = Value | bigint | Uint8Array | Date | RegExp | object; | ||
export type InValue = Value | bigint | boolean | Uint8Array | Date | RegExp | object; | ||
export declare function valueToProto(value: InValue): proto.Value; | ||
export declare const protoNull: proto.Value; | ||
export declare function valueFromProto(value: proto.Value): Value; |
import { Base64 } from "js-base64"; | ||
import { ProtoError } from "./errors.js"; | ||
import { ClientError, ProtoError } from "./errors.js"; | ||
export function valueToProto(value) { | ||
@@ -11,2 +11,5 @@ if (value === null) { | ||
else if (typeof value === "number") { | ||
if (!Number.isFinite(value)) { | ||
throw new ClientError("Only finite numbers (not Infinity or NaN) can be passed as arguments"); | ||
} | ||
return { "type": "float", "value": +value }; | ||
@@ -17,2 +20,5 @@ } | ||
} | ||
else if (typeof value === "boolean") { | ||
return { "type": "integer", "value": value ? "1" : "0" }; | ||
} | ||
else if (value instanceof ArrayBuffer) { | ||
@@ -19,0 +25,0 @@ return { "type": "blob", "base64": Base64.fromUint8Array(new Uint8Array(value)) }; |
{ | ||
"name": "@libsql/hrana-client", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"keywords": [ | ||
@@ -37,2 +37,3 @@ "hrana", | ||
"scripts": { | ||
"clean": "rm -rf ./lib ./*.tsbuildinfo", | ||
"prebuild": "rm -rf ./lib-cjs ./lib-esm", | ||
@@ -43,4 +44,7 @@ "build": "npm run build:cjs && npm run build:esm", | ||
"postbuild": "cp package-cjs.json ./lib-cjs/package.json", | ||
"clean-build": "npm run clean && npm run build", | ||
"test": "jest" | ||
"typecheck": "tsc --noEmit", | ||
"test": "jest", | ||
"typedoc": "rm -rf ./docs && typedoc" | ||
}, | ||
@@ -57,4 +61,5 @@ | ||
"ts-jest": "^29.0.5", | ||
"typedoc": "^0.23.28", | ||
"typescript": "^4.9.4" | ||
} | ||
} |
# Hrana client for TypeScript | ||
**[API docs][docs] | [Github][github] | [npm][npm]** | ||
[docs]: https://libsql.org/hrana-client-ts/ | ||
[github]: https://github.com/libsql/hrana-client-ts/ | ||
[npm]: https://www.npmjs.com/package/@libsql/hrana-client | ||
This package implements a Hrana client for TypeScript. Hrana is a protocol based on WebSockets that can be used to connect to sqld. It is more efficient than the postgres wire protocol (especially for edge deployments) and it supports interactive stateful SQL connections (called "streams") which are not supported by the HTTP API. | ||
@@ -16,3 +22,3 @@ | ||
// databases, but it uses just a single network connection internally | ||
const url = process.env.URL ?? "ws://localhost:2023"; // Address of the sqld server | ||
const url = process.env.URL ?? "ws://localhost:8080"; // Address of the sqld server | ||
const jwt = process.env.JWT; // JWT token for authentication | ||
@@ -28,3 +34,3 @@ const client = hrana.open(url, jwt); | ||
// The rows are returned in an Array... | ||
for (const book of books) { | ||
for (const book of books.rows) { | ||
// every returned row works as an array (`book[1]`) and as an object (`book.year`) | ||
@@ -36,4 +42,4 @@ console.log(`${book.title} from ${book.year}`); | ||
const book = await stream.queryRow("SELECT title, MIN(year) FROM book"); | ||
if (book !== undefined) { | ||
console.log(`The oldest book is ${book.title} from year ${book[1]}`); | ||
if (book.row !== undefined) { | ||
console.log(`The oldest book is ${book.row.title} from year ${book.row[1]}`); | ||
} | ||
@@ -43,9 +49,9 @@ | ||
const year = await stream.queryValue(["SELECT MAX(year) FROM book WHERE author = ?", ["Jane Austen"]]); | ||
if (year !== undefined) { | ||
console.log(`Last book from Jane Austen was published in ${year}`); | ||
if (year.value !== undefined) { | ||
console.log(`Last book from Jane Austen was published in ${year.value}`); | ||
} | ||
// Execute a statement that does not return any rows | ||
const res = await stream.execute(["DELETE FROM book WHERE author = ?", ["J. K. Rowling"]]) | ||
console.log(`${res.rowsAffected} books have been cancelled`); | ||
const res = await stream.run(["DELETE FROM book WHERE author = ?", ["J. K. Rowling"]]) | ||
console.log(`${res.affectedRowCount} books have been cancelled`); | ||
@@ -52,0 +58,0 @@ // When you are done, remember to close the client |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
75925
37
1905
57
5
1