stitchdb 1.2.0 → 1.3.0

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/dist/index.d.mts CHANGED
@@ -35,49 +35,42 @@ declare class StitchDBError extends Error {
35
35
  /**
36
36
  * StitchDB client.
37
37
  *
38
- * Uses HTTP/2 with persistent connections (via fetch keep-alive).
39
- * All requests share one TCP connection — no repeated TLS handshakes.
40
- * Batch API sends multiple queries in one request.
38
+ * Uses WebSocket for queries (1 connection, unlimited queries, $0.019/M cost).
39
+ * Falls back to HTTP if WebSocket unavailable.
41
40
  */
42
41
  declare class StitchDB {
43
42
  private url;
43
+ private wsUrl;
44
44
  private apiKey;
45
- private headers;
45
+ private ws;
46
+ private pending;
47
+ private msgId;
48
+ private connecting;
49
+ private wsSupported;
50
+ private wsFailed;
46
51
  constructor(config: StitchDBConfig);
47
- /** Run a SQL query with parameterized bindings. */
52
+ private connect;
53
+ private wsSend;
54
+ private httpPost;
55
+ private send;
48
56
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<T>>;
49
- /**
50
- * Run multiple queries in a single HTTP request.
51
- * All queries execute atomically. One Worker invocation for N queries.
52
- */
53
57
  batch(queries: {
54
58
  sql: string;
55
59
  params?: unknown[];
56
60
  }[]): Promise<BatchResult>;
57
- /** Run a DDL statement (CREATE TABLE, ALTER TABLE, DROP TABLE, etc.) */
58
61
  run(sql: string): Promise<ExecResult>;
59
- /** Insert a row. */
60
62
  insert(table: string, data: Record<string, unknown>): Promise<QueryResult>;
61
- /**
62
- * Insert multiple rows in a single request using batch API.
63
- * One Worker invocation regardless of how many rows.
64
- */
65
63
  insertMany(table: string, rows: Record<string, unknown>[]): Promise<BatchResult>;
66
- /** Update rows matching a WHERE clause. */
67
64
  update(table: string, data: Record<string, unknown>, where: string, whereParams?: unknown[]): Promise<QueryResult>;
68
- /** Delete rows matching a WHERE clause. */
69
65
  remove(table: string, where: string, whereParams?: unknown[]): Promise<QueryResult>;
70
- /** Find one row by ID. */
71
66
  find<T = Record<string, unknown>>(table: string, id: unknown, idColumn?: string): Promise<T | null>;
72
- /** Select rows with optional WHERE, ORDER BY, LIMIT, OFFSET. */
73
67
  select<T = Record<string, unknown>>(table: string, where?: string, params?: unknown[], opts?: {
74
68
  orderBy?: string;
75
69
  limit?: number;
76
70
  offset?: number;
77
71
  }): Promise<T[]>;
78
- private post;
72
+ close(): void;
79
73
  }
80
- /** Create a StitchDB client. */
81
74
  declare function createClient(config: StitchDBConfig): StitchDB;
82
75
 
83
76
  export { type BatchResult, type ExecResult, type QueryResult, StitchDB, type StitchDBConfig, StitchDBError, createClient, StitchDB as default };
package/dist/index.d.ts CHANGED
@@ -35,49 +35,42 @@ declare class StitchDBError extends Error {
35
35
  /**
36
36
  * StitchDB client.
37
37
  *
38
- * Uses HTTP/2 with persistent connections (via fetch keep-alive).
39
- * All requests share one TCP connection — no repeated TLS handshakes.
40
- * Batch API sends multiple queries in one request.
38
+ * Uses WebSocket for queries (1 connection, unlimited queries, $0.019/M cost).
39
+ * Falls back to HTTP if WebSocket unavailable.
41
40
  */
42
41
  declare class StitchDB {
43
42
  private url;
43
+ private wsUrl;
44
44
  private apiKey;
45
- private headers;
45
+ private ws;
46
+ private pending;
47
+ private msgId;
48
+ private connecting;
49
+ private wsSupported;
50
+ private wsFailed;
46
51
  constructor(config: StitchDBConfig);
47
- /** Run a SQL query with parameterized bindings. */
52
+ private connect;
53
+ private wsSend;
54
+ private httpPost;
55
+ private send;
48
56
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<T>>;
49
- /**
50
- * Run multiple queries in a single HTTP request.
51
- * All queries execute atomically. One Worker invocation for N queries.
52
- */
53
57
  batch(queries: {
54
58
  sql: string;
55
59
  params?: unknown[];
56
60
  }[]): Promise<BatchResult>;
57
- /** Run a DDL statement (CREATE TABLE, ALTER TABLE, DROP TABLE, etc.) */
58
61
  run(sql: string): Promise<ExecResult>;
59
- /** Insert a row. */
60
62
  insert(table: string, data: Record<string, unknown>): Promise<QueryResult>;
61
- /**
62
- * Insert multiple rows in a single request using batch API.
63
- * One Worker invocation regardless of how many rows.
64
- */
65
63
  insertMany(table: string, rows: Record<string, unknown>[]): Promise<BatchResult>;
66
- /** Update rows matching a WHERE clause. */
67
64
  update(table: string, data: Record<string, unknown>, where: string, whereParams?: unknown[]): Promise<QueryResult>;
68
- /** Delete rows matching a WHERE clause. */
69
65
  remove(table: string, where: string, whereParams?: unknown[]): Promise<QueryResult>;
70
- /** Find one row by ID. */
71
66
  find<T = Record<string, unknown>>(table: string, id: unknown, idColumn?: string): Promise<T | null>;
72
- /** Select rows with optional WHERE, ORDER BY, LIMIT, OFFSET. */
73
67
  select<T = Record<string, unknown>>(table: string, where?: string, params?: unknown[], opts?: {
74
68
  orderBy?: string;
75
69
  limit?: number;
76
70
  offset?: number;
77
71
  }): Promise<T[]>;
78
- private post;
72
+ close(): void;
79
73
  }
80
- /** Create a StitchDB client. */
81
74
  declare function createClient(config: StitchDBConfig): StitchDB;
82
75
 
83
76
  export { type BatchResult, type ExecResult, type QueryResult, StitchDB, type StitchDBConfig, StitchDBError, createClient, StitchDB as default };
package/dist/index.js CHANGED
@@ -35,59 +35,147 @@ var StitchDBError = class extends Error {
35
35
  };
36
36
  var StitchDB = class {
37
37
  constructor(config) {
38
+ this.ws = null;
39
+ this.pending = /* @__PURE__ */ new Map();
40
+ this.msgId = 0;
41
+ this.connecting = null;
42
+ this.wsFailed = false;
38
43
  this.url = (config.url || "https://db.stitchdb.com").replace(/\/$/, "");
44
+ this.wsUrl = this.url.replace("https://", "wss://").replace("http://", "ws://");
39
45
  this.apiKey = config.apiKey;
40
- this.headers = {
41
- "Authorization": `Bearer ${this.apiKey}`,
42
- "Content-Type": "application/json"
43
- };
46
+ this.wsSupported = typeof WebSocket !== "undefined";
47
+ }
48
+ // ---- WebSocket ----
49
+ async connect() {
50
+ if (this.ws?.readyState === 1) return;
51
+ if (this.connecting) return this.connecting;
52
+ this.connecting = new Promise((resolve, reject) => {
53
+ try {
54
+ const ws = new WebSocket(`${this.wsUrl}/ws/query?key=${this.apiKey}`);
55
+ ws.onopen = () => {
56
+ this.ws = ws;
57
+ this.connecting = null;
58
+ resolve();
59
+ };
60
+ ws.onmessage = (e) => {
61
+ try {
62
+ const data = JSON.parse(typeof e.data === "string" ? e.data : "");
63
+ const p = this.pending.get(data.id);
64
+ if (p) {
65
+ this.pending.delete(data.id);
66
+ data.error ? p.reject(new StitchDBError(data.error)) : p.resolve(data);
67
+ }
68
+ } catch {
69
+ }
70
+ };
71
+ ws.onclose = () => {
72
+ this.ws = null;
73
+ this.connecting = null;
74
+ for (const [, p] of this.pending) p.reject(new StitchDBError("Connection closed"));
75
+ this.pending.clear();
76
+ };
77
+ ws.onerror = () => {
78
+ this.ws = null;
79
+ this.connecting = null;
80
+ this.wsFailed = true;
81
+ resolve();
82
+ };
83
+ } catch {
84
+ this.connecting = null;
85
+ this.wsFailed = true;
86
+ resolve();
87
+ }
88
+ });
89
+ return this.connecting;
90
+ }
91
+ async wsSend(msg) {
92
+ if (this.wsFailed) return this.httpPost(msg);
93
+ await this.connect();
94
+ if (!this.ws || this.ws.readyState !== 1) {
95
+ this.wsFailed = true;
96
+ return this.httpPost(msg);
97
+ }
98
+ const id = String(++this.msgId);
99
+ msg.id = id;
100
+ return new Promise((resolve, reject) => {
101
+ const timeout = setTimeout(() => {
102
+ this.pending.delete(id);
103
+ reject(new StitchDBError("Query timeout"));
104
+ }, 3e4);
105
+ this.pending.set(id, {
106
+ resolve: (v) => {
107
+ clearTimeout(timeout);
108
+ resolve(v);
109
+ },
110
+ reject: (e) => {
111
+ clearTimeout(timeout);
112
+ reject(e);
113
+ }
114
+ });
115
+ try {
116
+ this.ws.send(JSON.stringify(msg));
117
+ } catch {
118
+ clearTimeout(timeout);
119
+ this.pending.delete(id);
120
+ this.wsFailed = true;
121
+ this.httpPost(msg).then(resolve, reject);
122
+ }
123
+ });
124
+ }
125
+ // ---- HTTP fallback ----
126
+ async httpPost(msg) {
127
+ let path = "/v1/query", body = { sql: msg.sql, params: msg.params };
128
+ if (msg.action === "batch") {
129
+ path = "/v1/batch";
130
+ body = { queries: msg.queries };
131
+ } else if (msg.action === "exec") {
132
+ path = "/v1/exec";
133
+ body = { sql: msg.sql };
134
+ }
135
+ const res = await fetch(`${this.url}${path}`, {
136
+ method: "POST",
137
+ headers: { "Authorization": `Bearer ${this.apiKey}`, "Content-Type": "application/json" },
138
+ body: JSON.stringify(body)
139
+ });
140
+ const data = await res.json();
141
+ if (!res.ok || data.error) throw new StitchDBError(data.error || `HTTP ${res.status}`, res.status);
142
+ return data;
143
+ }
144
+ // ---- Public API ----
145
+ async send(action, data) {
146
+ const msg = { action, ...data };
147
+ if (this.wsSupported && !this.wsFailed) return this.wsSend(msg);
148
+ return this.httpPost(msg);
44
149
  }
45
- /** Run a SQL query with parameterized bindings. */
46
150
  async query(sql, params) {
47
- return this.post("/v1/query", { sql, params });
151
+ return this.send("query", { sql, params });
48
152
  }
49
- /**
50
- * Run multiple queries in a single HTTP request.
51
- * All queries execute atomically. One Worker invocation for N queries.
52
- */
53
153
  async batch(queries) {
54
- return this.post("/v1/batch", { queries });
154
+ return this.send("batch", { queries });
55
155
  }
56
- /** Run a DDL statement (CREATE TABLE, ALTER TABLE, DROP TABLE, etc.) */
57
156
  async run(sql) {
58
- return this.post("/v1/exec", { sql });
157
+ return this.send("exec", { sql });
59
158
  }
60
- /** Insert a row. */
61
159
  async insert(table, data) {
62
160
  const cols = Object.keys(data);
63
- const sql = `INSERT INTO "${table}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${cols.map(() => "?").join(", ")})`;
64
- return this.query(sql, Object.values(data));
161
+ return this.query(`INSERT INTO "${table}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${cols.map(() => "?").join(", ")})`, Object.values(data));
65
162
  }
66
- /**
67
- * Insert multiple rows in a single request using batch API.
68
- * One Worker invocation regardless of how many rows.
69
- */
70
163
  async insertMany(table, rows) {
71
- if (rows.length === 0) return { results: [], meta: { rows_read: 0, rows_written: 0, duration_ms: 0, queries_count: 0 } };
164
+ if (!rows.length) return { results: [], meta: { rows_read: 0, rows_written: 0, duration_ms: 0, queries_count: 0 } };
72
165
  const cols = Object.keys(rows[0]);
73
166
  const sql = `INSERT INTO "${table}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${cols.map(() => "?").join(", ")})`;
74
167
  return this.batch(rows.map((row) => ({ sql, params: cols.map((c) => row[c]) })));
75
168
  }
76
- /** Update rows matching a WHERE clause. */
77
169
  async update(table, data, where, whereParams) {
78
- const set = Object.keys(data).map((c) => `"${c}" = ?`).join(", ");
79
- return this.query(`UPDATE "${table}" SET ${set} WHERE ${where}`, [...Object.values(data), ...whereParams || []]);
170
+ return this.query(`UPDATE "${table}" SET ${Object.keys(data).map((c) => `"${c}" = ?`).join(", ")} WHERE ${where}`, [...Object.values(data), ...whereParams || []]);
80
171
  }
81
- /** Delete rows matching a WHERE clause. */
82
172
  async remove(table, where, whereParams) {
83
173
  return this.query(`DELETE FROM "${table}" WHERE ${where}`, whereParams);
84
174
  }
85
- /** Find one row by ID. */
86
175
  async find(table, id, idColumn = "id") {
87
176
  const { results } = await this.query(`SELECT * FROM "${table}" WHERE "${idColumn}" = ? LIMIT 1`, [id]);
88
177
  return results[0] || null;
89
178
  }
90
- /** Select rows with optional WHERE, ORDER BY, LIMIT, OFFSET. */
91
179
  async select(table, where, params, opts) {
92
180
  let sql = `SELECT * FROM "${table}"`;
93
181
  if (where) sql += ` WHERE ${where}`;
@@ -97,19 +185,11 @@ var StitchDB = class {
97
185
  const { results } = await this.query(sql, params);
98
186
  return results;
99
187
  }
100
- async post(path, body) {
101
- const res = await fetch(`${this.url}${path}`, {
102
- method: "POST",
103
- headers: this.headers,
104
- body: JSON.stringify(body),
105
- // @ts-ignore — keepalive hint for HTTP/2 connection reuse
106
- keepalive: true
107
- });
108
- const data = await res.json();
109
- if (!res.ok || data.error) {
110
- throw new StitchDBError(data.error || `Request failed: ${res.status}`, res.status);
188
+ close() {
189
+ if (this.ws) {
190
+ this.ws.close();
191
+ this.ws = null;
111
192
  }
112
- return data;
113
193
  }
114
194
  };
115
195
  function createClient(config) {
package/dist/index.mjs CHANGED
@@ -8,59 +8,147 @@ var StitchDBError = class extends Error {
8
8
  };
9
9
  var StitchDB = class {
10
10
  constructor(config) {
11
+ this.ws = null;
12
+ this.pending = /* @__PURE__ */ new Map();
13
+ this.msgId = 0;
14
+ this.connecting = null;
15
+ this.wsFailed = false;
11
16
  this.url = (config.url || "https://db.stitchdb.com").replace(/\/$/, "");
17
+ this.wsUrl = this.url.replace("https://", "wss://").replace("http://", "ws://");
12
18
  this.apiKey = config.apiKey;
13
- this.headers = {
14
- "Authorization": `Bearer ${this.apiKey}`,
15
- "Content-Type": "application/json"
16
- };
19
+ this.wsSupported = typeof WebSocket !== "undefined";
20
+ }
21
+ // ---- WebSocket ----
22
+ async connect() {
23
+ if (this.ws?.readyState === 1) return;
24
+ if (this.connecting) return this.connecting;
25
+ this.connecting = new Promise((resolve, reject) => {
26
+ try {
27
+ const ws = new WebSocket(`${this.wsUrl}/ws/query?key=${this.apiKey}`);
28
+ ws.onopen = () => {
29
+ this.ws = ws;
30
+ this.connecting = null;
31
+ resolve();
32
+ };
33
+ ws.onmessage = (e) => {
34
+ try {
35
+ const data = JSON.parse(typeof e.data === "string" ? e.data : "");
36
+ const p = this.pending.get(data.id);
37
+ if (p) {
38
+ this.pending.delete(data.id);
39
+ data.error ? p.reject(new StitchDBError(data.error)) : p.resolve(data);
40
+ }
41
+ } catch {
42
+ }
43
+ };
44
+ ws.onclose = () => {
45
+ this.ws = null;
46
+ this.connecting = null;
47
+ for (const [, p] of this.pending) p.reject(new StitchDBError("Connection closed"));
48
+ this.pending.clear();
49
+ };
50
+ ws.onerror = () => {
51
+ this.ws = null;
52
+ this.connecting = null;
53
+ this.wsFailed = true;
54
+ resolve();
55
+ };
56
+ } catch {
57
+ this.connecting = null;
58
+ this.wsFailed = true;
59
+ resolve();
60
+ }
61
+ });
62
+ return this.connecting;
63
+ }
64
+ async wsSend(msg) {
65
+ if (this.wsFailed) return this.httpPost(msg);
66
+ await this.connect();
67
+ if (!this.ws || this.ws.readyState !== 1) {
68
+ this.wsFailed = true;
69
+ return this.httpPost(msg);
70
+ }
71
+ const id = String(++this.msgId);
72
+ msg.id = id;
73
+ return new Promise((resolve, reject) => {
74
+ const timeout = setTimeout(() => {
75
+ this.pending.delete(id);
76
+ reject(new StitchDBError("Query timeout"));
77
+ }, 3e4);
78
+ this.pending.set(id, {
79
+ resolve: (v) => {
80
+ clearTimeout(timeout);
81
+ resolve(v);
82
+ },
83
+ reject: (e) => {
84
+ clearTimeout(timeout);
85
+ reject(e);
86
+ }
87
+ });
88
+ try {
89
+ this.ws.send(JSON.stringify(msg));
90
+ } catch {
91
+ clearTimeout(timeout);
92
+ this.pending.delete(id);
93
+ this.wsFailed = true;
94
+ this.httpPost(msg).then(resolve, reject);
95
+ }
96
+ });
97
+ }
98
+ // ---- HTTP fallback ----
99
+ async httpPost(msg) {
100
+ let path = "/v1/query", body = { sql: msg.sql, params: msg.params };
101
+ if (msg.action === "batch") {
102
+ path = "/v1/batch";
103
+ body = { queries: msg.queries };
104
+ } else if (msg.action === "exec") {
105
+ path = "/v1/exec";
106
+ body = { sql: msg.sql };
107
+ }
108
+ const res = await fetch(`${this.url}${path}`, {
109
+ method: "POST",
110
+ headers: { "Authorization": `Bearer ${this.apiKey}`, "Content-Type": "application/json" },
111
+ body: JSON.stringify(body)
112
+ });
113
+ const data = await res.json();
114
+ if (!res.ok || data.error) throw new StitchDBError(data.error || `HTTP ${res.status}`, res.status);
115
+ return data;
116
+ }
117
+ // ---- Public API ----
118
+ async send(action, data) {
119
+ const msg = { action, ...data };
120
+ if (this.wsSupported && !this.wsFailed) return this.wsSend(msg);
121
+ return this.httpPost(msg);
17
122
  }
18
- /** Run a SQL query with parameterized bindings. */
19
123
  async query(sql, params) {
20
- return this.post("/v1/query", { sql, params });
124
+ return this.send("query", { sql, params });
21
125
  }
22
- /**
23
- * Run multiple queries in a single HTTP request.
24
- * All queries execute atomically. One Worker invocation for N queries.
25
- */
26
126
  async batch(queries) {
27
- return this.post("/v1/batch", { queries });
127
+ return this.send("batch", { queries });
28
128
  }
29
- /** Run a DDL statement (CREATE TABLE, ALTER TABLE, DROP TABLE, etc.) */
30
129
  async run(sql) {
31
- return this.post("/v1/exec", { sql });
130
+ return this.send("exec", { sql });
32
131
  }
33
- /** Insert a row. */
34
132
  async insert(table, data) {
35
133
  const cols = Object.keys(data);
36
- const sql = `INSERT INTO "${table}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${cols.map(() => "?").join(", ")})`;
37
- return this.query(sql, Object.values(data));
134
+ return this.query(`INSERT INTO "${table}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${cols.map(() => "?").join(", ")})`, Object.values(data));
38
135
  }
39
- /**
40
- * Insert multiple rows in a single request using batch API.
41
- * One Worker invocation regardless of how many rows.
42
- */
43
136
  async insertMany(table, rows) {
44
- if (rows.length === 0) return { results: [], meta: { rows_read: 0, rows_written: 0, duration_ms: 0, queries_count: 0 } };
137
+ if (!rows.length) return { results: [], meta: { rows_read: 0, rows_written: 0, duration_ms: 0, queries_count: 0 } };
45
138
  const cols = Object.keys(rows[0]);
46
139
  const sql = `INSERT INTO "${table}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${cols.map(() => "?").join(", ")})`;
47
140
  return this.batch(rows.map((row) => ({ sql, params: cols.map((c) => row[c]) })));
48
141
  }
49
- /** Update rows matching a WHERE clause. */
50
142
  async update(table, data, where, whereParams) {
51
- const set = Object.keys(data).map((c) => `"${c}" = ?`).join(", ");
52
- return this.query(`UPDATE "${table}" SET ${set} WHERE ${where}`, [...Object.values(data), ...whereParams || []]);
143
+ return this.query(`UPDATE "${table}" SET ${Object.keys(data).map((c) => `"${c}" = ?`).join(", ")} WHERE ${where}`, [...Object.values(data), ...whereParams || []]);
53
144
  }
54
- /** Delete rows matching a WHERE clause. */
55
145
  async remove(table, where, whereParams) {
56
146
  return this.query(`DELETE FROM "${table}" WHERE ${where}`, whereParams);
57
147
  }
58
- /** Find one row by ID. */
59
148
  async find(table, id, idColumn = "id") {
60
149
  const { results } = await this.query(`SELECT * FROM "${table}" WHERE "${idColumn}" = ? LIMIT 1`, [id]);
61
150
  return results[0] || null;
62
151
  }
63
- /** Select rows with optional WHERE, ORDER BY, LIMIT, OFFSET. */
64
152
  async select(table, where, params, opts) {
65
153
  let sql = `SELECT * FROM "${table}"`;
66
154
  if (where) sql += ` WHERE ${where}`;
@@ -70,19 +158,11 @@ var StitchDB = class {
70
158
  const { results } = await this.query(sql, params);
71
159
  return results;
72
160
  }
73
- async post(path, body) {
74
- const res = await fetch(`${this.url}${path}`, {
75
- method: "POST",
76
- headers: this.headers,
77
- body: JSON.stringify(body),
78
- // @ts-ignore — keepalive hint for HTTP/2 connection reuse
79
- keepalive: true
80
- });
81
- const data = await res.json();
82
- if (!res.ok || data.error) {
83
- throw new StitchDBError(data.error || `Request failed: ${res.status}`, res.status);
161
+ close() {
162
+ if (this.ws) {
163
+ this.ws.close();
164
+ this.ws = null;
84
165
  }
85
- return data;
86
166
  }
87
167
  };
88
168
  function createClient(config) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchdb",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "StitchDB client for JavaScript and TypeScript",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",