synqlite 0.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.cjs ADDED
@@ -0,0 +1,241 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ SynqliteClient: () => SynqliteClient
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/client.ts
38
+ var WRITE_PREFIXES = ["INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER"];
39
+ function isWriteQuery(sql) {
40
+ const trimmed = sql.trim().toUpperCase();
41
+ return WRITE_PREFIXES.some((p) => trimmed.startsWith(p));
42
+ }
43
+ var SynqliteClient = class {
44
+ apiKey;
45
+ baseUrl;
46
+ constructor(config) {
47
+ this.apiKey = config.apiKey;
48
+ this.baseUrl = "https://api.synqlite.io";
49
+ }
50
+ async request(path, options = {}) {
51
+ const res = await fetch(`${this.baseUrl}${path}`, {
52
+ ...options,
53
+ headers: {
54
+ "Content-Type": "application/json",
55
+ Authorization: `Bearer ${this.apiKey}`,
56
+ ...options.headers
57
+ }
58
+ });
59
+ if (!res.ok) {
60
+ const body = await res.json().catch(() => ({}));
61
+ throw new Error(body?.error?.message ?? `Request failed: ${res.status}`);
62
+ }
63
+ return res.json();
64
+ }
65
+ async requestRaw(path) {
66
+ const res = await fetch(`${this.baseUrl}${path}`, {
67
+ headers: { Authorization: `Bearer ${this.apiKey}` }
68
+ });
69
+ if (!res.ok) {
70
+ const body = await res.json().catch(() => ({}));
71
+ throw new Error(body?.error?.message ?? `Request failed: ${res.status}`);
72
+ }
73
+ return res;
74
+ }
75
+ async listDatabases() {
76
+ return this.request("/v1/databases");
77
+ }
78
+ async createDatabase(name) {
79
+ return this.request("/v1/databases", {
80
+ method: "POST",
81
+ body: JSON.stringify({ name })
82
+ });
83
+ }
84
+ async deleteDatabase(id) {
85
+ await this.request(`/v1/databases/${id}`, { method: "DELETE" });
86
+ }
87
+ async query(databaseId, sql, params) {
88
+ return this.request("/v1/query", {
89
+ method: "POST",
90
+ body: JSON.stringify({ databaseId, sql, params })
91
+ });
92
+ }
93
+ /** Remote-only database handle. Every query goes over the network. */
94
+ db(databaseId) {
95
+ return {
96
+ query: (sql, params) => this.query(databaseId, sql, params),
97
+ tables: async () => {
98
+ const result = await this.query(
99
+ databaseId,
100
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
101
+ );
102
+ return result.rows.map((r) => r.name);
103
+ }
104
+ };
105
+ }
106
+ /**
107
+ * Embedded replica with incremental sync.
108
+ *
109
+ * Downloads the database once on init, then uses a lightweight operation log
110
+ * to stay in sync. Reads run locally (~0ms). Writes go to the server, get
111
+ * applied locally immediately, and any concurrent changes from other writers
112
+ * are pulled automatically.
113
+ *
114
+ * Supports multiple concurrent writers (5-10) — the server serializes all
115
+ * writes and each replica replays the same ordered operation log.
116
+ *
117
+ * Requires `sql.js` as a peer dependency:
118
+ * ```
119
+ * npm install sql.js
120
+ * ```
121
+ */
122
+ async embeddedDb(databaseId, options = {}) {
123
+ const { syncInterval = 5e3 } = options;
124
+ const initSqlJs = await import("sql.js").then((m) => m.default ?? m);
125
+ const SQL = await initSqlJs();
126
+ let localDb = null;
127
+ let localSeq = 0;
128
+ let syncing = false;
129
+ let syncTimer = null;
130
+ const fullDownload = async () => {
131
+ const res = await this.requestRaw(`/v1/databases/${databaseId}/dump`);
132
+ const buf = await res.arrayBuffer();
133
+ const seq = Number(res.headers.get("X-Synqlite-Seq") ?? "0");
134
+ if (localDb) localDb.close();
135
+ localDb = new SQL.Database(new Uint8Array(buf));
136
+ localSeq = seq;
137
+ };
138
+ const applyLocal = (sql, params) => {
139
+ if (!localDb) return;
140
+ const stmt = localDb.prepare(sql);
141
+ if (params.length > 0) {
142
+ stmt.bind(params);
143
+ }
144
+ stmt.step();
145
+ stmt.free();
146
+ };
147
+ const incrementalSync = async () => {
148
+ if (syncing || !localDb) return;
149
+ syncing = true;
150
+ try {
151
+ const data = await this.request(
152
+ `/v1/databases/${databaseId}/changes?since=${localSeq}`
153
+ );
154
+ if (data.fullSync) {
155
+ await fullDownload();
156
+ return;
157
+ }
158
+ if (data.changes && data.changes.length > 0) {
159
+ for (const change of data.changes) {
160
+ if (change.seq <= localSeq) continue;
161
+ applyLocal(change.sql, change.params);
162
+ }
163
+ localSeq = data.currentSeq;
164
+ }
165
+ } finally {
166
+ syncing = false;
167
+ }
168
+ };
169
+ const executeLocal = (sql, params) => {
170
+ if (!localDb) throw new Error("Database is closed");
171
+ const start = performance.now();
172
+ const stmt = localDb.prepare(sql);
173
+ if (params && params.length > 0) {
174
+ stmt.bind(params);
175
+ }
176
+ const columns = stmt.getColumnNames();
177
+ const rows = [];
178
+ while (stmt.step()) {
179
+ const values = stmt.get();
180
+ const row = {};
181
+ columns.forEach((col, i) => {
182
+ row[col] = values[i];
183
+ });
184
+ rows.push(row);
185
+ }
186
+ stmt.free();
187
+ return {
188
+ columns,
189
+ rows,
190
+ rowsAffected: 0,
191
+ duration: Math.round(performance.now() - start)
192
+ };
193
+ };
194
+ await fullDownload();
195
+ if (syncInterval > 0) {
196
+ syncTimer = setInterval(() => {
197
+ incrementalSync().catch(() => {
198
+ });
199
+ }, syncInterval);
200
+ }
201
+ const handle = {
202
+ get localSeq() {
203
+ return localSeq;
204
+ },
205
+ query: async (sql, params) => {
206
+ if (isWriteQuery(sql)) {
207
+ const result = await this.query(databaseId, sql, params);
208
+ applyLocal(sql, params ?? []);
209
+ if (result.seq !== void 0) {
210
+ localSeq = result.seq;
211
+ }
212
+ await incrementalSync();
213
+ return result;
214
+ }
215
+ return executeLocal(sql, params);
216
+ },
217
+ tables: async () => {
218
+ const result = executeLocal(
219
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
220
+ );
221
+ return result.rows.map((r) => r.name);
222
+ },
223
+ sync: () => incrementalSync(),
224
+ close: () => {
225
+ if (syncTimer) {
226
+ clearInterval(syncTimer);
227
+ syncTimer = null;
228
+ }
229
+ if (localDb) {
230
+ localDb.close();
231
+ localDb = null;
232
+ }
233
+ }
234
+ };
235
+ return handle;
236
+ }
237
+ };
238
+ // Annotate the CommonJS export names for ESM import in node:
239
+ 0 && (module.exports = {
240
+ SynqliteClient
241
+ });
@@ -0,0 +1,66 @@
1
+ interface SynqliteConfig {
2
+ apiKey: string;
3
+ }
4
+ interface Database {
5
+ id: string;
6
+ name: string;
7
+ slug: string;
8
+ sizeBytes: number;
9
+ createdAt: string;
10
+ }
11
+ interface QueryResult<T = Record<string, unknown>> {
12
+ columns: string[];
13
+ rows: T[];
14
+ rowsAffected: number;
15
+ duration: number;
16
+ /** Server sequence number (present for write queries) */
17
+ seq?: number;
18
+ }
19
+ interface DatabaseHandle {
20
+ query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<T>>;
21
+ tables(): Promise<string[]>;
22
+ }
23
+ interface EmbeddedDatabaseHandle extends DatabaseHandle {
24
+ /** Pull the latest changes from the server (incremental sync) */
25
+ sync(): Promise<void>;
26
+ /** Close the local database and free memory */
27
+ close(): void;
28
+ /** Current local sequence number */
29
+ readonly localSeq: number;
30
+ }
31
+ interface EmbeddedOptions {
32
+ /** Auto-sync interval in milliseconds. Set to 0 to disable. Default: 5000 */
33
+ syncInterval?: number;
34
+ }
35
+ declare class SynqliteClient {
36
+ private apiKey;
37
+ private baseUrl;
38
+ constructor(config: SynqliteConfig);
39
+ private request;
40
+ private requestRaw;
41
+ listDatabases(): Promise<Database[]>;
42
+ createDatabase(name: string): Promise<Database>;
43
+ deleteDatabase(id: string): Promise<void>;
44
+ query<T = Record<string, unknown>>(databaseId: string, sql: string, params?: unknown[]): Promise<QueryResult<T>>;
45
+ /** Remote-only database handle. Every query goes over the network. */
46
+ db(databaseId: string): DatabaseHandle;
47
+ /**
48
+ * Embedded replica with incremental sync.
49
+ *
50
+ * Downloads the database once on init, then uses a lightweight operation log
51
+ * to stay in sync. Reads run locally (~0ms). Writes go to the server, get
52
+ * applied locally immediately, and any concurrent changes from other writers
53
+ * are pulled automatically.
54
+ *
55
+ * Supports multiple concurrent writers (5-10) — the server serializes all
56
+ * writes and each replica replays the same ordered operation log.
57
+ *
58
+ * Requires `sql.js` as a peer dependency:
59
+ * ```
60
+ * npm install sql.js
61
+ * ```
62
+ */
63
+ embeddedDb(databaseId: string, options?: EmbeddedOptions): Promise<EmbeddedDatabaseHandle>;
64
+ }
65
+
66
+ export { type Database, type DatabaseHandle, type EmbeddedDatabaseHandle, type EmbeddedOptions, type QueryResult, SynqliteClient, type SynqliteConfig };
@@ -0,0 +1,66 @@
1
+ interface SynqliteConfig {
2
+ apiKey: string;
3
+ }
4
+ interface Database {
5
+ id: string;
6
+ name: string;
7
+ slug: string;
8
+ sizeBytes: number;
9
+ createdAt: string;
10
+ }
11
+ interface QueryResult<T = Record<string, unknown>> {
12
+ columns: string[];
13
+ rows: T[];
14
+ rowsAffected: number;
15
+ duration: number;
16
+ /** Server sequence number (present for write queries) */
17
+ seq?: number;
18
+ }
19
+ interface DatabaseHandle {
20
+ query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<T>>;
21
+ tables(): Promise<string[]>;
22
+ }
23
+ interface EmbeddedDatabaseHandle extends DatabaseHandle {
24
+ /** Pull the latest changes from the server (incremental sync) */
25
+ sync(): Promise<void>;
26
+ /** Close the local database and free memory */
27
+ close(): void;
28
+ /** Current local sequence number */
29
+ readonly localSeq: number;
30
+ }
31
+ interface EmbeddedOptions {
32
+ /** Auto-sync interval in milliseconds. Set to 0 to disable. Default: 5000 */
33
+ syncInterval?: number;
34
+ }
35
+ declare class SynqliteClient {
36
+ private apiKey;
37
+ private baseUrl;
38
+ constructor(config: SynqliteConfig);
39
+ private request;
40
+ private requestRaw;
41
+ listDatabases(): Promise<Database[]>;
42
+ createDatabase(name: string): Promise<Database>;
43
+ deleteDatabase(id: string): Promise<void>;
44
+ query<T = Record<string, unknown>>(databaseId: string, sql: string, params?: unknown[]): Promise<QueryResult<T>>;
45
+ /** Remote-only database handle. Every query goes over the network. */
46
+ db(databaseId: string): DatabaseHandle;
47
+ /**
48
+ * Embedded replica with incremental sync.
49
+ *
50
+ * Downloads the database once on init, then uses a lightweight operation log
51
+ * to stay in sync. Reads run locally (~0ms). Writes go to the server, get
52
+ * applied locally immediately, and any concurrent changes from other writers
53
+ * are pulled automatically.
54
+ *
55
+ * Supports multiple concurrent writers (5-10) — the server serializes all
56
+ * writes and each replica replays the same ordered operation log.
57
+ *
58
+ * Requires `sql.js` as a peer dependency:
59
+ * ```
60
+ * npm install sql.js
61
+ * ```
62
+ */
63
+ embeddedDb(databaseId: string, options?: EmbeddedOptions): Promise<EmbeddedDatabaseHandle>;
64
+ }
65
+
66
+ export { type Database, type DatabaseHandle, type EmbeddedDatabaseHandle, type EmbeddedOptions, type QueryResult, SynqliteClient, type SynqliteConfig };
package/dist/index.js ADDED
@@ -0,0 +1,204 @@
1
+ // src/client.ts
2
+ var WRITE_PREFIXES = ["INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER"];
3
+ function isWriteQuery(sql) {
4
+ const trimmed = sql.trim().toUpperCase();
5
+ return WRITE_PREFIXES.some((p) => trimmed.startsWith(p));
6
+ }
7
+ var SynqliteClient = class {
8
+ apiKey;
9
+ baseUrl;
10
+ constructor(config) {
11
+ this.apiKey = config.apiKey;
12
+ this.baseUrl = "https://api.synqlite.io";
13
+ }
14
+ async request(path, options = {}) {
15
+ const res = await fetch(`${this.baseUrl}${path}`, {
16
+ ...options,
17
+ headers: {
18
+ "Content-Type": "application/json",
19
+ Authorization: `Bearer ${this.apiKey}`,
20
+ ...options.headers
21
+ }
22
+ });
23
+ if (!res.ok) {
24
+ const body = await res.json().catch(() => ({}));
25
+ throw new Error(body?.error?.message ?? `Request failed: ${res.status}`);
26
+ }
27
+ return res.json();
28
+ }
29
+ async requestRaw(path) {
30
+ const res = await fetch(`${this.baseUrl}${path}`, {
31
+ headers: { Authorization: `Bearer ${this.apiKey}` }
32
+ });
33
+ if (!res.ok) {
34
+ const body = await res.json().catch(() => ({}));
35
+ throw new Error(body?.error?.message ?? `Request failed: ${res.status}`);
36
+ }
37
+ return res;
38
+ }
39
+ async listDatabases() {
40
+ return this.request("/v1/databases");
41
+ }
42
+ async createDatabase(name) {
43
+ return this.request("/v1/databases", {
44
+ method: "POST",
45
+ body: JSON.stringify({ name })
46
+ });
47
+ }
48
+ async deleteDatabase(id) {
49
+ await this.request(`/v1/databases/${id}`, { method: "DELETE" });
50
+ }
51
+ async query(databaseId, sql, params) {
52
+ return this.request("/v1/query", {
53
+ method: "POST",
54
+ body: JSON.stringify({ databaseId, sql, params })
55
+ });
56
+ }
57
+ /** Remote-only database handle. Every query goes over the network. */
58
+ db(databaseId) {
59
+ return {
60
+ query: (sql, params) => this.query(databaseId, sql, params),
61
+ tables: async () => {
62
+ const result = await this.query(
63
+ databaseId,
64
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
65
+ );
66
+ return result.rows.map((r) => r.name);
67
+ }
68
+ };
69
+ }
70
+ /**
71
+ * Embedded replica with incremental sync.
72
+ *
73
+ * Downloads the database once on init, then uses a lightweight operation log
74
+ * to stay in sync. Reads run locally (~0ms). Writes go to the server, get
75
+ * applied locally immediately, and any concurrent changes from other writers
76
+ * are pulled automatically.
77
+ *
78
+ * Supports multiple concurrent writers (5-10) — the server serializes all
79
+ * writes and each replica replays the same ordered operation log.
80
+ *
81
+ * Requires `sql.js` as a peer dependency:
82
+ * ```
83
+ * npm install sql.js
84
+ * ```
85
+ */
86
+ async embeddedDb(databaseId, options = {}) {
87
+ const { syncInterval = 5e3 } = options;
88
+ const initSqlJs = await import("sql.js").then((m) => m.default ?? m);
89
+ const SQL = await initSqlJs();
90
+ let localDb = null;
91
+ let localSeq = 0;
92
+ let syncing = false;
93
+ let syncTimer = null;
94
+ const fullDownload = async () => {
95
+ const res = await this.requestRaw(`/v1/databases/${databaseId}/dump`);
96
+ const buf = await res.arrayBuffer();
97
+ const seq = Number(res.headers.get("X-Synqlite-Seq") ?? "0");
98
+ if (localDb) localDb.close();
99
+ localDb = new SQL.Database(new Uint8Array(buf));
100
+ localSeq = seq;
101
+ };
102
+ const applyLocal = (sql, params) => {
103
+ if (!localDb) return;
104
+ const stmt = localDb.prepare(sql);
105
+ if (params.length > 0) {
106
+ stmt.bind(params);
107
+ }
108
+ stmt.step();
109
+ stmt.free();
110
+ };
111
+ const incrementalSync = async () => {
112
+ if (syncing || !localDb) return;
113
+ syncing = true;
114
+ try {
115
+ const data = await this.request(
116
+ `/v1/databases/${databaseId}/changes?since=${localSeq}`
117
+ );
118
+ if (data.fullSync) {
119
+ await fullDownload();
120
+ return;
121
+ }
122
+ if (data.changes && data.changes.length > 0) {
123
+ for (const change of data.changes) {
124
+ if (change.seq <= localSeq) continue;
125
+ applyLocal(change.sql, change.params);
126
+ }
127
+ localSeq = data.currentSeq;
128
+ }
129
+ } finally {
130
+ syncing = false;
131
+ }
132
+ };
133
+ const executeLocal = (sql, params) => {
134
+ if (!localDb) throw new Error("Database is closed");
135
+ const start = performance.now();
136
+ const stmt = localDb.prepare(sql);
137
+ if (params && params.length > 0) {
138
+ stmt.bind(params);
139
+ }
140
+ const columns = stmt.getColumnNames();
141
+ const rows = [];
142
+ while (stmt.step()) {
143
+ const values = stmt.get();
144
+ const row = {};
145
+ columns.forEach((col, i) => {
146
+ row[col] = values[i];
147
+ });
148
+ rows.push(row);
149
+ }
150
+ stmt.free();
151
+ return {
152
+ columns,
153
+ rows,
154
+ rowsAffected: 0,
155
+ duration: Math.round(performance.now() - start)
156
+ };
157
+ };
158
+ await fullDownload();
159
+ if (syncInterval > 0) {
160
+ syncTimer = setInterval(() => {
161
+ incrementalSync().catch(() => {
162
+ });
163
+ }, syncInterval);
164
+ }
165
+ const handle = {
166
+ get localSeq() {
167
+ return localSeq;
168
+ },
169
+ query: async (sql, params) => {
170
+ if (isWriteQuery(sql)) {
171
+ const result = await this.query(databaseId, sql, params);
172
+ applyLocal(sql, params ?? []);
173
+ if (result.seq !== void 0) {
174
+ localSeq = result.seq;
175
+ }
176
+ await incrementalSync();
177
+ return result;
178
+ }
179
+ return executeLocal(sql, params);
180
+ },
181
+ tables: async () => {
182
+ const result = executeLocal(
183
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
184
+ );
185
+ return result.rows.map((r) => r.name);
186
+ },
187
+ sync: () => incrementalSync(),
188
+ close: () => {
189
+ if (syncTimer) {
190
+ clearInterval(syncTimer);
191
+ syncTimer = null;
192
+ }
193
+ if (localDb) {
194
+ localDb.close();
195
+ localDb = null;
196
+ }
197
+ }
198
+ };
199
+ return handle;
200
+ }
201
+ };
202
+ export {
203
+ SynqliteClient
204
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "synqlite",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "files": ["dist"],
16
+ "scripts": {
17
+ "build": "tsup",
18
+ "dev": "tsup --watch"
19
+ },
20
+ "peerDependencies": {
21
+ "sql.js": ">=1.0.0"
22
+ },
23
+ "peerDependenciesMeta": {
24
+ "sql.js": {
25
+ "optional": true
26
+ }
27
+ },
28
+ "devDependencies": {
29
+ "tsup": "^8.3.0",
30
+ "typescript": "^5.7.0",
31
+ "@types/sql.js": "^1.4.9",
32
+ "sql.js": "^1.12.0"
33
+ }
34
+ }