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 +241 -0
- package/dist/index.d.cts +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +204 -0
- package/package.json +34 -0
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
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|