stitchdb 1.0.0 → 1.1.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
@@ -30,12 +30,21 @@ interface ExecResult {
30
30
  }
31
31
  declare class StitchDBError extends Error {
32
32
  status: number;
33
- constructor(message: string, status: number);
33
+ constructor(message: string, status?: number);
34
34
  }
35
35
  declare class StitchDB {
36
36
  private url;
37
+ private wsUrl;
37
38
  private apiKey;
39
+ private ws;
40
+ private pending;
41
+ private msgId;
42
+ private connecting;
43
+ private useWebSocket;
38
44
  constructor(config: StitchDBConfig);
45
+ private connect;
46
+ private wsSend;
47
+ private httpQuery;
39
48
  /** Run a SQL query with parameterized bindings. */
40
49
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<T>>;
41
50
  /** Run multiple queries atomically in a single batch. */
@@ -59,7 +68,8 @@ declare class StitchDB {
59
68
  limit?: number;
60
69
  offset?: number;
61
70
  }): Promise<T[]>;
62
- private post;
71
+ /** Close the WebSocket connection. */
72
+ close(): void;
63
73
  }
64
74
  /** Create a StitchDB client. */
65
75
  declare function createClient(config: StitchDBConfig): StitchDB;
package/dist/index.d.ts CHANGED
@@ -30,12 +30,21 @@ interface ExecResult {
30
30
  }
31
31
  declare class StitchDBError extends Error {
32
32
  status: number;
33
- constructor(message: string, status: number);
33
+ constructor(message: string, status?: number);
34
34
  }
35
35
  declare class StitchDB {
36
36
  private url;
37
+ private wsUrl;
37
38
  private apiKey;
39
+ private ws;
40
+ private pending;
41
+ private msgId;
42
+ private connecting;
43
+ private useWebSocket;
38
44
  constructor(config: StitchDBConfig);
45
+ private connect;
46
+ private wsSend;
47
+ private httpQuery;
39
48
  /** Run a SQL query with parameterized bindings. */
40
49
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<T>>;
41
50
  /** Run multiple queries atomically in a single batch. */
@@ -59,7 +68,8 @@ declare class StitchDB {
59
68
  limit?: number;
60
69
  offset?: number;
61
70
  }): Promise<T[]>;
62
- private post;
71
+ /** Close the WebSocket connection. */
72
+ close(): void;
63
73
  }
64
74
  /** Create a StitchDB client. */
65
75
  declare function createClient(config: StitchDBConfig): StitchDB;
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ __export(index_exports, {
27
27
  });
28
28
  module.exports = __toCommonJS(index_exports);
29
29
  var StitchDBError = class extends Error {
30
- constructor(message, status) {
30
+ constructor(message, status = 0) {
31
31
  super(message);
32
32
  this.name = "StitchDBError";
33
33
  this.status = status;
@@ -35,20 +35,132 @@ 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;
38
42
  this.url = (config.url || "https://db.stitchdb.com").replace(/\/$/, "");
43
+ this.wsUrl = this.url.replace("https://", "wss://").replace("http://", "ws://");
39
44
  this.apiKey = config.apiKey;
45
+ this.useWebSocket = typeof WebSocket !== "undefined";
46
+ }
47
+ async connect() {
48
+ if (this.ws?.readyState === 1) return;
49
+ if (this.connecting) return this.connecting;
50
+ this.connecting = new Promise((resolve, reject) => {
51
+ try {
52
+ const ws = new WebSocket(`${this.wsUrl}/ws/query?key=${this.apiKey}`);
53
+ ws.onopen = () => {
54
+ this.ws = ws;
55
+ this.connecting = null;
56
+ resolve();
57
+ };
58
+ ws.onmessage = (event) => {
59
+ try {
60
+ const data = JSON.parse(typeof event.data === "string" ? event.data : "");
61
+ const pending = this.pending.get(data.id);
62
+ if (pending) {
63
+ this.pending.delete(data.id);
64
+ if (data.error) {
65
+ pending.reject(new StitchDBError(data.error));
66
+ } else {
67
+ pending.resolve(data);
68
+ }
69
+ }
70
+ } catch {
71
+ }
72
+ };
73
+ ws.onclose = () => {
74
+ this.ws = null;
75
+ this.connecting = null;
76
+ for (const [id, p] of this.pending) {
77
+ p.reject(new StitchDBError("Connection closed"));
78
+ this.pending.delete(id);
79
+ }
80
+ };
81
+ ws.onerror = () => {
82
+ this.ws = null;
83
+ this.connecting = null;
84
+ reject(new StitchDBError("WebSocket connection failed"));
85
+ };
86
+ } catch {
87
+ this.connecting = null;
88
+ this.useWebSocket = false;
89
+ resolve();
90
+ }
91
+ });
92
+ return this.connecting;
93
+ }
94
+ async wsSend(msg) {
95
+ await this.connect();
96
+ if (!this.ws || this.ws.readyState !== 1) {
97
+ this.useWebSocket = false;
98
+ return this.httpQuery(msg);
99
+ }
100
+ const id = String(++this.msgId);
101
+ msg.id = id;
102
+ return new Promise((resolve, reject) => {
103
+ const timeout = setTimeout(() => {
104
+ this.pending.delete(id);
105
+ reject(new StitchDBError("Query timeout"));
106
+ }, 3e4);
107
+ this.pending.set(id, {
108
+ resolve: (v) => {
109
+ clearTimeout(timeout);
110
+ resolve(v);
111
+ },
112
+ reject: (e) => {
113
+ clearTimeout(timeout);
114
+ reject(e);
115
+ }
116
+ });
117
+ this.ws.send(JSON.stringify(msg));
118
+ });
119
+ }
120
+ async httpQuery(msg) {
121
+ let path = "/v1/query";
122
+ let body = { sql: msg.sql, params: msg.params };
123
+ if (msg.action === "batch") {
124
+ path = "/v1/batch";
125
+ body = { queries: msg.queries };
126
+ } else if (msg.action === "exec") {
127
+ path = "/v1/exec";
128
+ body = { sql: msg.sql };
129
+ }
130
+ const res = await fetch(`${this.url}${path}`, {
131
+ method: "POST",
132
+ headers: {
133
+ "Authorization": `Bearer ${this.apiKey}`,
134
+ "Content-Type": "application/json"
135
+ },
136
+ body: JSON.stringify(body)
137
+ });
138
+ const data = await res.json();
139
+ if (!res.ok || data.error) {
140
+ throw new StitchDBError(data.error || `Request failed: ${res.status}`, res.status);
141
+ }
142
+ return data;
40
143
  }
41
144
  /** Run a SQL query with parameterized bindings. */
42
145
  async query(sql, params) {
43
- return this.post("/v1/query", { sql, params });
146
+ if (this.useWebSocket) {
147
+ return this.wsSend({ action: "query", sql, params });
148
+ }
149
+ return this.httpQuery({ action: "query", sql, params });
44
150
  }
45
151
  /** Run multiple queries atomically in a single batch. */
46
152
  async batch(queries) {
47
- return this.post("/v1/batch", { queries });
153
+ if (this.useWebSocket) {
154
+ return this.wsSend({ action: "batch", queries });
155
+ }
156
+ return this.httpQuery({ action: "batch", queries });
48
157
  }
49
158
  /** Run a DDL statement (CREATE TABLE, ALTER TABLE, DROP TABLE, etc.) */
50
159
  async run(sql) {
51
- return this.post("/v1/exec", { sql });
160
+ if (this.useWebSocket) {
161
+ return this.wsSend({ action: "exec", sql });
162
+ }
163
+ return this.httpQuery({ action: "exec", sql });
52
164
  }
53
165
  /** Insert a row. */
54
166
  async insert(table, data) {
@@ -80,20 +192,12 @@ var StitchDB = class {
80
192
  const { results } = await this.query(sql, params);
81
193
  return results;
82
194
  }
83
- async post(path, body) {
84
- const res = await fetch(`${this.url}${path}`, {
85
- method: "POST",
86
- headers: {
87
- "Authorization": `Bearer ${this.apiKey}`,
88
- "Content-Type": "application/json"
89
- },
90
- body: JSON.stringify(body)
91
- });
92
- const data = await res.json();
93
- if (!res.ok || data.error) {
94
- throw new StitchDBError(data.error || `Request failed: ${res.status}`, res.status);
195
+ /** Close the WebSocket connection. */
196
+ close() {
197
+ if (this.ws) {
198
+ this.ws.close();
199
+ this.ws = null;
95
200
  }
96
- return data;
97
201
  }
98
202
  };
99
203
  function createClient(config) {
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  var StitchDBError = class extends Error {
3
- constructor(message, status) {
3
+ constructor(message, status = 0) {
4
4
  super(message);
5
5
  this.name = "StitchDBError";
6
6
  this.status = status;
@@ -8,20 +8,132 @@ 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;
11
15
  this.url = (config.url || "https://db.stitchdb.com").replace(/\/$/, "");
16
+ this.wsUrl = this.url.replace("https://", "wss://").replace("http://", "ws://");
12
17
  this.apiKey = config.apiKey;
18
+ this.useWebSocket = typeof WebSocket !== "undefined";
19
+ }
20
+ async connect() {
21
+ if (this.ws?.readyState === 1) return;
22
+ if (this.connecting) return this.connecting;
23
+ this.connecting = new Promise((resolve, reject) => {
24
+ try {
25
+ const ws = new WebSocket(`${this.wsUrl}/ws/query?key=${this.apiKey}`);
26
+ ws.onopen = () => {
27
+ this.ws = ws;
28
+ this.connecting = null;
29
+ resolve();
30
+ };
31
+ ws.onmessage = (event) => {
32
+ try {
33
+ const data = JSON.parse(typeof event.data === "string" ? event.data : "");
34
+ const pending = this.pending.get(data.id);
35
+ if (pending) {
36
+ this.pending.delete(data.id);
37
+ if (data.error) {
38
+ pending.reject(new StitchDBError(data.error));
39
+ } else {
40
+ pending.resolve(data);
41
+ }
42
+ }
43
+ } catch {
44
+ }
45
+ };
46
+ ws.onclose = () => {
47
+ this.ws = null;
48
+ this.connecting = null;
49
+ for (const [id, p] of this.pending) {
50
+ p.reject(new StitchDBError("Connection closed"));
51
+ this.pending.delete(id);
52
+ }
53
+ };
54
+ ws.onerror = () => {
55
+ this.ws = null;
56
+ this.connecting = null;
57
+ reject(new StitchDBError("WebSocket connection failed"));
58
+ };
59
+ } catch {
60
+ this.connecting = null;
61
+ this.useWebSocket = false;
62
+ resolve();
63
+ }
64
+ });
65
+ return this.connecting;
66
+ }
67
+ async wsSend(msg) {
68
+ await this.connect();
69
+ if (!this.ws || this.ws.readyState !== 1) {
70
+ this.useWebSocket = false;
71
+ return this.httpQuery(msg);
72
+ }
73
+ const id = String(++this.msgId);
74
+ msg.id = id;
75
+ return new Promise((resolve, reject) => {
76
+ const timeout = setTimeout(() => {
77
+ this.pending.delete(id);
78
+ reject(new StitchDBError("Query timeout"));
79
+ }, 3e4);
80
+ this.pending.set(id, {
81
+ resolve: (v) => {
82
+ clearTimeout(timeout);
83
+ resolve(v);
84
+ },
85
+ reject: (e) => {
86
+ clearTimeout(timeout);
87
+ reject(e);
88
+ }
89
+ });
90
+ this.ws.send(JSON.stringify(msg));
91
+ });
92
+ }
93
+ async httpQuery(msg) {
94
+ let path = "/v1/query";
95
+ let body = { sql: msg.sql, params: msg.params };
96
+ if (msg.action === "batch") {
97
+ path = "/v1/batch";
98
+ body = { queries: msg.queries };
99
+ } else if (msg.action === "exec") {
100
+ path = "/v1/exec";
101
+ body = { sql: msg.sql };
102
+ }
103
+ const res = await fetch(`${this.url}${path}`, {
104
+ method: "POST",
105
+ headers: {
106
+ "Authorization": `Bearer ${this.apiKey}`,
107
+ "Content-Type": "application/json"
108
+ },
109
+ body: JSON.stringify(body)
110
+ });
111
+ const data = await res.json();
112
+ if (!res.ok || data.error) {
113
+ throw new StitchDBError(data.error || `Request failed: ${res.status}`, res.status);
114
+ }
115
+ return data;
13
116
  }
14
117
  /** Run a SQL query with parameterized bindings. */
15
118
  async query(sql, params) {
16
- return this.post("/v1/query", { sql, params });
119
+ if (this.useWebSocket) {
120
+ return this.wsSend({ action: "query", sql, params });
121
+ }
122
+ return this.httpQuery({ action: "query", sql, params });
17
123
  }
18
124
  /** Run multiple queries atomically in a single batch. */
19
125
  async batch(queries) {
20
- return this.post("/v1/batch", { queries });
126
+ if (this.useWebSocket) {
127
+ return this.wsSend({ action: "batch", queries });
128
+ }
129
+ return this.httpQuery({ action: "batch", queries });
21
130
  }
22
131
  /** Run a DDL statement (CREATE TABLE, ALTER TABLE, DROP TABLE, etc.) */
23
132
  async run(sql) {
24
- return this.post("/v1/exec", { sql });
133
+ if (this.useWebSocket) {
134
+ return this.wsSend({ action: "exec", sql });
135
+ }
136
+ return this.httpQuery({ action: "exec", sql });
25
137
  }
26
138
  /** Insert a row. */
27
139
  async insert(table, data) {
@@ -53,20 +165,12 @@ var StitchDB = class {
53
165
  const { results } = await this.query(sql, params);
54
166
  return results;
55
167
  }
56
- async post(path, body) {
57
- const res = await fetch(`${this.url}${path}`, {
58
- method: "POST",
59
- headers: {
60
- "Authorization": `Bearer ${this.apiKey}`,
61
- "Content-Type": "application/json"
62
- },
63
- body: JSON.stringify(body)
64
- });
65
- const data = await res.json();
66
- if (!res.ok || data.error) {
67
- throw new StitchDBError(data.error || `Request failed: ${res.status}`, res.status);
168
+ /** Close the WebSocket connection. */
169
+ close() {
170
+ if (this.ws) {
171
+ this.ws.close();
172
+ this.ws = null;
68
173
  }
69
- return data;
70
174
  }
71
175
  };
72
176
  function createClient(config) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchdb",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "StitchDB client for JavaScript and TypeScript",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -19,7 +19,13 @@
19
19
  "build": "tsup src/index.ts --format cjs,esm --dts",
20
20
  "prepublishOnly": "npm run build"
21
21
  },
22
- "keywords": ["stitchdb", "database", "sql", "edge", "serverless"],
22
+ "keywords": [
23
+ "stitchdb",
24
+ "database",
25
+ "sql",
26
+ "edge",
27
+ "serverless"
28
+ ],
23
29
  "license": "MIT",
24
30
  "author": {
25
31
  "name": "StitchDB",
@@ -35,4 +41,4 @@
35
41
  "tsup": "^8.0.0",
36
42
  "typescript": "^5.0.0"
37
43
  }
38
- }
44
+ }