firebird-wasm 0.7.0 → 0.9.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/CHANGELOG.md +266 -0
- package/README.md +31 -15
- package/dist/.tsbuildinfo.build +1 -1
- package/dist/browser/engine-transport.d.ts +2 -0
- package/dist/browser/engine-transport.d.ts.map +1 -1
- package/dist/browser/engine-transport.js +43 -11
- package/dist/browser/engine-transport.js.map +1 -1
- package/dist/browser/firebird-browser.d.ts.map +1 -1
- package/dist/browser/firebird-browser.js +59 -20
- package/dist/browser/firebird-browser.js.map +1 -1
- package/dist/browser/index.d.ts +3 -3
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +6 -3
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/params.d.ts +8 -2
- package/dist/browser/params.d.ts.map +1 -1
- package/dist/browser/params.js +14 -5
- package/dist/browser/params.js.map +1 -1
- package/dist/browser/serve-engine.d.ts +30 -0
- package/dist/browser/serve-engine.d.ts.map +1 -0
- package/dist/browser/serve-engine.js +93 -0
- package/dist/browser/serve-engine.js.map +1 -0
- package/dist/browser/worker-entry.d.ts +8 -22
- package/dist/browser/worker-entry.d.ts.map +1 -1
- package/dist/browser/worker-entry.js +18 -79
- package/dist/browser/worker-entry.js.map +1 -1
- package/dist/electric/apply.d.ts +53 -0
- package/dist/electric/apply.d.ts.map +1 -0
- package/dist/electric/apply.js +88 -0
- package/dist/electric/apply.js.map +1 -0
- package/dist/electric/index.d.ts +133 -0
- package/dist/electric/index.d.ts.map +1 -0
- package/dist/electric/index.js +378 -0
- package/dist/electric/index.js.map +1 -0
- package/dist/electric/serial.d.ts +20 -0
- package/dist/electric/serial.d.ts.map +1 -0
- package/dist/electric/serial.js +29 -0
- package/dist/electric/serial.js.map +1 -0
- package/dist/electric/values.d.ts +65 -0
- package/dist/electric/values.d.ts.map +1 -0
- package/dist/electric/values.js +257 -0
- package/dist/electric/values.js.map +1 -0
- package/dist/electric/writes.d.ts +99 -0
- package/dist/electric/writes.d.ts.map +1 -0
- package/dist/electric/writes.js +282 -0
- package/dist/electric/writes.js.map +1 -0
- package/dist/electric-server/capture.d.ts +51 -0
- package/dist/electric-server/capture.d.ts.map +1 -0
- package/dist/electric-server/capture.js +224 -0
- package/dist/electric-server/capture.js.map +1 -0
- package/dist/electric-server/catalog.d.ts +61 -0
- package/dist/electric-server/catalog.d.ts.map +1 -0
- package/dist/electric-server/catalog.js +137 -0
- package/dist/electric-server/catalog.js.map +1 -0
- package/dist/electric-server/index.d.ts +85 -0
- package/dist/electric-server/index.d.ts.map +1 -0
- package/dist/electric-server/index.js +528 -0
- package/dist/electric-server/index.js.map +1 -0
- package/dist/electric-server/where.d.ts +30 -0
- package/dist/electric-server/where.d.ts.map +1 -0
- package/dist/electric-server/where.js +243 -0
- package/dist/electric-server/where.js.map +1 -0
- package/dist/firebird.d.ts +3 -1
- package/dist/firebird.d.ts.map +1 -1
- package/dist/firebird.js +259 -22
- package/dist/firebird.js.map +1 -1
- package/dist/frameworks/query-store.d.ts +99 -0
- package/dist/frameworks/query-store.d.ts.map +1 -0
- package/dist/frameworks/query-store.js +166 -0
- package/dist/frameworks/query-store.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +68 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +92 -0
- package/dist/react/index.js.map +1 -0
- package/dist/repl/index.d.ts +76 -0
- package/dist/repl/index.d.ts.map +1 -0
- package/dist/repl/index.js +328 -0
- package/dist/repl/index.js.map +1 -0
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/vue/index.d.ts +68 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +107 -0
- package/dist/vue/index.js.map +1 -0
- package/dist/wasm/firebird-embedded-st.js +1 -1
- package/dist/wasm/firebird-embedded-st.wasm +0 -0
- package/dist/wasm/firebird-embedded.js +1 -1
- package/dist/wasm/firebird-embedded.wasm +0 -0
- package/dist/wasm-loader.d.ts +57 -7
- package/dist/wasm-loader.d.ts.map +1 -1
- package/dist/wasm-loader.js +106 -21
- package/dist/wasm-loader.js.map +1 -1
- package/package.json +50 -3
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* values.ts – Postgres values, as Electric sends them, into Firebird.
|
|
4
|
+
*
|
|
5
|
+
* Electric sends every value as Postgres's text output and describes the
|
|
6
|
+
* columns once, in the `electric-schema` response header. This turns one
|
|
7
|
+
* into a parameter Firebird converts exactly, and refuses what it does not
|
|
8
|
+
* map by name rather than storing something that looks right and is not.
|
|
9
|
+
*
|
|
10
|
+
* Observed from Electric 1.8 (not all of it in the documentation): a boolean
|
|
11
|
+
* is `"true"` in the initial snapshot and `"f"` in a live update; a
|
|
12
|
+
* `timestamptz` arrives in UTC as `2024-05-01 10:00:00+00`; arrays are marked
|
|
13
|
+
* `dims`, and the primary key `pk_index`.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.checkSchema = checkSchema;
|
|
17
|
+
exports.toFirebird = toFirebird;
|
|
18
|
+
exports.firebirdColumn = firebirdColumn;
|
|
19
|
+
exports.firebirdName = firebirdName;
|
|
20
|
+
exports.tableFromSchema = tableFromSchema;
|
|
21
|
+
exports.primaryKeyOf = primaryKeyOf;
|
|
22
|
+
const asText = (text) => text;
|
|
23
|
+
const asInteger = (text) => Number(text);
|
|
24
|
+
// As text, which Firebird converts to BIGINT exactly; the Node driver cannot
|
|
25
|
+
// bind a JavaScript bigint at all.
|
|
26
|
+
const asBigInt = (text, column) => {
|
|
27
|
+
if (!/^-?\d+$/.test(text)) {
|
|
28
|
+
throw new RangeError(`Column ${column} holds ${text}, which is not a whole number`);
|
|
29
|
+
}
|
|
30
|
+
return text;
|
|
31
|
+
};
|
|
32
|
+
const asFloat = (text, column) => {
|
|
33
|
+
const value = Number(text);
|
|
34
|
+
if (!Number.isFinite(value)) {
|
|
35
|
+
// Postgres has NaN and ±Infinity; Firebird has no literal for them.
|
|
36
|
+
throw new RangeError(`Column ${column} holds ${text}, which Firebird cannot store`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
};
|
|
40
|
+
const asNumeric = (text, column) => {
|
|
41
|
+
if (!/^-?\d+(\.\d+)?$/.test(text)) {
|
|
42
|
+
throw new RangeError(`Column ${column} holds ${text}, which Firebird cannot store`);
|
|
43
|
+
}
|
|
44
|
+
// Exact: the text goes to a NUMERIC column, which converts it digit for digit.
|
|
45
|
+
return text;
|
|
46
|
+
};
|
|
47
|
+
const asBoolean = (text, column) => {
|
|
48
|
+
// Both forms occur: the snapshot says "true", the replication stream "t".
|
|
49
|
+
if (text === 't' || text === 'true')
|
|
50
|
+
return true;
|
|
51
|
+
if (text === 'f' || text === 'false')
|
|
52
|
+
return false;
|
|
53
|
+
throw new RangeError(`Column ${column} holds ${text}, which is not a boolean`);
|
|
54
|
+
};
|
|
55
|
+
/** Firebird keeps time to 1/10000 s; Postgres sends up to six digits. */
|
|
56
|
+
function fourDigitFraction(text) {
|
|
57
|
+
return text.replace(/(\.\d{4})\d+/, '$1');
|
|
58
|
+
}
|
|
59
|
+
/** `+05:30` from `+05:30`, `+05` or `+0530`, as Firebird reads an offset. */
|
|
60
|
+
function offset(sign, hours, minutes = '00') {
|
|
61
|
+
return ` ${sign}${hours}:${minutes}`;
|
|
62
|
+
}
|
|
63
|
+
const asDate = (text, column) => {
|
|
64
|
+
if (!/^\d{4}-\d\d-\d\d$/.test(text)) {
|
|
65
|
+
// 'infinity', BC dates and years past 9999 have no Firebird DATE.
|
|
66
|
+
throw new RangeError(`Column ${column} holds ${text}, which Firebird cannot store`);
|
|
67
|
+
}
|
|
68
|
+
return text;
|
|
69
|
+
};
|
|
70
|
+
const asTime = (text) => fourDigitFraction(text);
|
|
71
|
+
const asTimestamp = (text, column) => {
|
|
72
|
+
if (!/^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d(\.\d+)?$/.test(text)) {
|
|
73
|
+
throw new RangeError(`Column ${column} holds ${text}, which Firebird cannot store`);
|
|
74
|
+
}
|
|
75
|
+
return fourDigitFraction(text);
|
|
76
|
+
};
|
|
77
|
+
const ZONED = /^(.*?)([+-])(\d\d):?(\d\d)?(?::\d\d)?$/;
|
|
78
|
+
const asZoned = (text, column) => {
|
|
79
|
+
const match = ZONED.exec(text);
|
|
80
|
+
if (!match || !/^(\d{4}-\d\d-\d\d )?\d\d:\d\d:\d\d(\.\d+)?$/.test(match[1])) {
|
|
81
|
+
throw new RangeError(`Column ${column} holds ${text}, which Firebird cannot store`);
|
|
82
|
+
}
|
|
83
|
+
return fourDigitFraction(match[1]) + offset(match[2], match[3], match[4]);
|
|
84
|
+
};
|
|
85
|
+
// Postgres's hex output (`\x` and two digits a byte), the default since 9.0,
|
|
86
|
+
// into the bytes, which bind as themselves into a BLOB SUB_TYPE BINARY.
|
|
87
|
+
const asBytes = (text, column) => {
|
|
88
|
+
if (!/^\\x([0-9a-fA-F]{2})*$/.test(text)) {
|
|
89
|
+
throw new RangeError(`Column ${column} holds bytea that is not in hex form (\\x…); ` +
|
|
90
|
+
"Postgres's bytea_output must be 'hex', its default");
|
|
91
|
+
}
|
|
92
|
+
const bytes = new Uint8Array((text.length - 2) / 2);
|
|
93
|
+
for (let i = 0; i < bytes.length; i++)
|
|
94
|
+
bytes[i] = parseInt(text.slice(2 + i * 2, 4 + i * 2), 16);
|
|
95
|
+
return bytes;
|
|
96
|
+
};
|
|
97
|
+
/** Postgres type name to its conversion, for every type this maps. */
|
|
98
|
+
const CONVERSIONS = {
|
|
99
|
+
int2: asInteger,
|
|
100
|
+
int4: asInteger,
|
|
101
|
+
int8: asBigInt,
|
|
102
|
+
numeric: asNumeric,
|
|
103
|
+
float4: asFloat,
|
|
104
|
+
float8: asFloat,
|
|
105
|
+
bool: asBoolean,
|
|
106
|
+
text: asText,
|
|
107
|
+
varchar: asText,
|
|
108
|
+
bpchar: asText,
|
|
109
|
+
name: asText,
|
|
110
|
+
citext: asText,
|
|
111
|
+
uuid: asText,
|
|
112
|
+
json: asText,
|
|
113
|
+
jsonb: asText,
|
|
114
|
+
date: asDate,
|
|
115
|
+
time: asTime,
|
|
116
|
+
timestamp: asTimestamp,
|
|
117
|
+
timetz: asZoned,
|
|
118
|
+
timestamptz: asZoned,
|
|
119
|
+
bytea: asBytes,
|
|
120
|
+
};
|
|
121
|
+
/** Why a type is not mapped, where there is more to say than "not yet". */
|
|
122
|
+
const REFUSED = {};
|
|
123
|
+
/**
|
|
124
|
+
* Check that every column of `schema` can be stored, before any row is.
|
|
125
|
+
* @throws naming each column and type that cannot.
|
|
126
|
+
*/
|
|
127
|
+
function checkSchema(schema) {
|
|
128
|
+
const problems = [];
|
|
129
|
+
for (const [column, info] of Object.entries(schema)) {
|
|
130
|
+
if ((info.dims ?? 0) > 0)
|
|
131
|
+
continue; // arrays: stored as JSON text
|
|
132
|
+
if (CONVERSIONS[info.type])
|
|
133
|
+
continue;
|
|
134
|
+
problems.push(`${column} (${info.type}): ${REFUSED[info.type] ?? 'this Postgres type is not mapped to Firebird'}`);
|
|
135
|
+
}
|
|
136
|
+
if (problems.length > 0) {
|
|
137
|
+
throw new TypeError(`This shape has columns firebird-wasm/electric cannot store:\n ${problems.join('\n ')}\n` +
|
|
138
|
+
'Leave them out with the shape\'s columns parameter, or cast them to text in a view');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* One value from Electric as a Firebird parameter.
|
|
143
|
+
*
|
|
144
|
+
* `value` is Electric's text, `null`, or for an array column the array the
|
|
145
|
+
* client parsed (of texts), which is stored as JSON.
|
|
146
|
+
*/
|
|
147
|
+
function toFirebird(value, column, info) {
|
|
148
|
+
if (value === null || value === undefined)
|
|
149
|
+
return null;
|
|
150
|
+
if ((info.dims ?? 0) > 0)
|
|
151
|
+
return JSON.stringify(value);
|
|
152
|
+
const convert = CONVERSIONS[info.type];
|
|
153
|
+
if (!convert) {
|
|
154
|
+
throw new TypeError(`Column ${column} has type ${info.type}, which is not mapped to Firebird`);
|
|
155
|
+
}
|
|
156
|
+
return convert(String(value), column);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The Firebird identifier for a Postgres column, quoted.
|
|
160
|
+
*
|
|
161
|
+
* Postgres folds unquoted names to lower case and Firebird to upper, so a
|
|
162
|
+
* plain lower-case Postgres name maps to the upper-case Firebird name that
|
|
163
|
+
* the same unquoted word finds (`title` → `"TITLE"`). Anything else was
|
|
164
|
+
* quoted in Postgres to exist and stays exactly as it is (`"camelCase"`).
|
|
165
|
+
*/
|
|
166
|
+
function firebirdColumn(postgresName) {
|
|
167
|
+
if (/^[a-z_][a-z0-9_$]*$/.test(postgresName))
|
|
168
|
+
return quote(postgresName.toUpperCase());
|
|
169
|
+
return quote(postgresName);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* A table or column name as written in Firebird SQL, quoted: `items` and
|
|
173
|
+
* `ITEMS` are `"ITEMS"`; a name given in double quotes keeps its case.
|
|
174
|
+
*/
|
|
175
|
+
function firebirdName(name) {
|
|
176
|
+
const quoted = /^"((?:[^"]|"")+)"$/.exec(name);
|
|
177
|
+
if (quoted)
|
|
178
|
+
return name;
|
|
179
|
+
if (/^[A-Za-z][A-Za-z0-9_$]*$/.test(name))
|
|
180
|
+
return quote(name.toUpperCase());
|
|
181
|
+
throw new TypeError(`${name} is not a Firebird name; write it in double quotes to use it as it is`);
|
|
182
|
+
}
|
|
183
|
+
function quote(name) {
|
|
184
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
185
|
+
}
|
|
186
|
+
/** The Firebird column type for one Postgres column, for `tableFromSchema`. */
|
|
187
|
+
function firebirdType(info) {
|
|
188
|
+
if ((info.dims ?? 0) > 0)
|
|
189
|
+
return 'BLOB SUB_TYPE TEXT';
|
|
190
|
+
switch (info.type) {
|
|
191
|
+
case 'int2': return 'SMALLINT';
|
|
192
|
+
case 'int4': return 'INTEGER';
|
|
193
|
+
case 'int8': return 'BIGINT';
|
|
194
|
+
case 'numeric':
|
|
195
|
+
return info.precision !== undefined
|
|
196
|
+
? `NUMERIC(${Math.min(info.precision, 38)}, ${info.scale ?? 0})`
|
|
197
|
+
: 'DECFLOAT(34)';
|
|
198
|
+
case 'float4': return 'FLOAT';
|
|
199
|
+
case 'float8': return 'DOUBLE PRECISION';
|
|
200
|
+
case 'bool': return 'BOOLEAN';
|
|
201
|
+
case 'varchar': return info.max_length ? `VARCHAR(${info.max_length})` : 'BLOB SUB_TYPE TEXT';
|
|
202
|
+
case 'bpchar': return `CHAR(${info.length ?? 1})`;
|
|
203
|
+
// Not CHAR(36): a CHAR column reads back padded to its byte length.
|
|
204
|
+
case 'uuid': return 'VARCHAR(36)';
|
|
205
|
+
case 'date': return 'DATE';
|
|
206
|
+
case 'time': return 'TIME';
|
|
207
|
+
case 'timetz': return 'TIME WITH TIME ZONE';
|
|
208
|
+
case 'timestamp': return 'TIMESTAMP';
|
|
209
|
+
case 'timestamptz': return 'TIMESTAMP WITH TIME ZONE';
|
|
210
|
+
case 'bytea': return 'BLOB SUB_TYPE BINARY';
|
|
211
|
+
case 'text':
|
|
212
|
+
case 'name':
|
|
213
|
+
case 'citext':
|
|
214
|
+
case 'json':
|
|
215
|
+
case 'jsonb':
|
|
216
|
+
return 'BLOB SUB_TYPE TEXT';
|
|
217
|
+
default:
|
|
218
|
+
throw new TypeError(`${info.type} is not mapped to Firebird`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* `CREATE TABLE` for a shape's columns, to read and adjust before running.
|
|
223
|
+
*
|
|
224
|
+
* Unbounded text becomes `BLOB SUB_TYPE TEXT`, which cannot be part of a key
|
|
225
|
+
* or an index; a text primary key gets `VARCHAR(255)` instead, and is worth
|
|
226
|
+
* a look. Nothing here runs the statement: what types a table should have
|
|
227
|
+
* is the app's call, and a later change to the Postgres table does not
|
|
228
|
+
* migrate this one.
|
|
229
|
+
*/
|
|
230
|
+
function tableFromSchema(table, schema) {
|
|
231
|
+
checkSchema(schema);
|
|
232
|
+
const columns = Object.entries(schema);
|
|
233
|
+
const key = columns
|
|
234
|
+
.filter(([, info]) => info.pk_index !== undefined)
|
|
235
|
+
.sort(([, a], [, b]) => a.pk_index - b.pk_index)
|
|
236
|
+
.map(([name]) => name);
|
|
237
|
+
const lines = columns.map(([name, info]) => {
|
|
238
|
+
let type = firebirdType(info);
|
|
239
|
+
if (key.includes(name) && type === 'BLOB SUB_TYPE TEXT')
|
|
240
|
+
type = 'VARCHAR(255)';
|
|
241
|
+
if (key.includes(name) && type === 'BLOB SUB_TYPE BINARY')
|
|
242
|
+
type = 'VARCHAR(255) CHARACTER SET OCTETS';
|
|
243
|
+
const notNull = info.not_null || key.includes(name) ? ' NOT NULL' : '';
|
|
244
|
+
return ` ${firebirdColumn(name)} ${type}${notNull}`;
|
|
245
|
+
});
|
|
246
|
+
if (key.length > 0)
|
|
247
|
+
lines.push(` PRIMARY KEY (${key.map(firebirdColumn).join(', ')})`);
|
|
248
|
+
return `CREATE TABLE ${firebirdName(table)} (\n${lines.join(',\n')}\n)`;
|
|
249
|
+
}
|
|
250
|
+
/** The shape's primary key, as Postgres column names in key order. */
|
|
251
|
+
function primaryKeyOf(schema) {
|
|
252
|
+
return Object.entries(schema)
|
|
253
|
+
.filter(([, info]) => info.pk_index !== undefined)
|
|
254
|
+
.sort(([, a], [, b]) => a.pk_index - b.pk_index)
|
|
255
|
+
.map(([name]) => name);
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=values.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"values.js","sourceRoot":"","sources":["../../src/electric/values.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* writes.ts – local writes to synced tables, sent to the app's own API.
|
|
3
|
+
*
|
|
4
|
+
* Electric is a read path: a write goes through the app's API to the source
|
|
5
|
+
* and comes back down the shape. This keeps the wait out of sight:
|
|
6
|
+
*
|
|
7
|
+
* - A write is applied to the synced table at once, and recorded in
|
|
8
|
+
* `ELECTRIC_WRITES` in the same transaction, so it survives a reload.
|
|
9
|
+
* - A sender hands the recorded writes, in order, to `send` (the app's API).
|
|
10
|
+
* A failure is retried; a `WriteRejected` rolls the write back.
|
|
11
|
+
* - Each batch the sync applies is followed, in its transaction, by every
|
|
12
|
+
* write still pending on that table, so the server's older state never
|
|
13
|
+
* shows over a local write.
|
|
14
|
+
* - A sent write is dropped once the source's change has come back: when a
|
|
15
|
+
* change carries the transaction id `send` returned (Electric's `txids`,
|
|
16
|
+
* and the shape server's), or, without one, when the sync brings a change
|
|
17
|
+
* to the same row, or at the latest after `confirmTimeoutMs`.
|
|
18
|
+
*
|
|
19
|
+
* Values are what `exec` takes, keyed by the shape's column names (`title`
|
|
20
|
+
* for the Firebird column `TITLE`): strings, numbers, booleans, `null`,
|
|
21
|
+
* `Date` and `Uint8Array`.
|
|
22
|
+
*/
|
|
23
|
+
import type { ElectricTransaction } from './apply';
|
|
24
|
+
/** What a write queue needs of a database: `FirebirdBrowser` and `FirebirdLite` have it. */
|
|
25
|
+
export interface WritesDatabase {
|
|
26
|
+
transaction<T>(fn: (tx: ElectricTransaction) => Promise<T>): Promise<T>;
|
|
27
|
+
}
|
|
28
|
+
/** One write, as `send` receives it. */
|
|
29
|
+
export interface LocalWrite {
|
|
30
|
+
/** Its number in this database, in the order it was made. */
|
|
31
|
+
id: number;
|
|
32
|
+
/** The table, as given to the queue. */
|
|
33
|
+
table: string;
|
|
34
|
+
operation: 'insert' | 'update' | 'delete';
|
|
35
|
+
/** The primary key's values, by column name. */
|
|
36
|
+
key: Record<string, unknown>;
|
|
37
|
+
/** The values written (for a delete, none), by column name. */
|
|
38
|
+
value: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
/** What `send` may return: the source transaction that applied the write. */
|
|
41
|
+
export interface SendResult {
|
|
42
|
+
/** The source's transaction id, as Electric reports it in `txids`. */
|
|
43
|
+
txid?: string | number | bigint;
|
|
44
|
+
}
|
|
45
|
+
/** Thrown by `send` when the source refuses a write: it is rolled back, not retried. */
|
|
46
|
+
export declare class WriteRejected extends Error {
|
|
47
|
+
constructor(message: string);
|
|
48
|
+
}
|
|
49
|
+
/** Options of {@link createWriteQueue}. */
|
|
50
|
+
export interface WriteQueueOptions {
|
|
51
|
+
/** Hand one write to the source (the app's API); throw `WriteRejected` to refuse it. */
|
|
52
|
+
send: (write: LocalWrite) => Promise<SendResult | void>;
|
|
53
|
+
/** Called when a write is rejected and rolled back, and on each failed send. */
|
|
54
|
+
onError?: (error: Error, write: LocalWrite) => void;
|
|
55
|
+
/** How long to wait before sending again after a failure. Default 2000 ms. */
|
|
56
|
+
retryMs?: number;
|
|
57
|
+
/** Drop a sent write whose change has not come back after this long. Default 60 000 ms. */
|
|
58
|
+
confirmTimeoutMs?: number;
|
|
59
|
+
}
|
|
60
|
+
/** A running write queue. Returned by {@link createWriteQueue}. */
|
|
61
|
+
export interface WriteQueue {
|
|
62
|
+
/** Insert (or replace) a row, here at once and at the source in turn. */
|
|
63
|
+
insert(table: string, value: Record<string, unknown>): Promise<LocalWrite>;
|
|
64
|
+
/** Update the row with `key`, setting `value`'s columns. */
|
|
65
|
+
update(table: string, key: Record<string, unknown>, value: Record<string, unknown>): Promise<LocalWrite>;
|
|
66
|
+
/** Delete the row with `key`. */
|
|
67
|
+
delete(table: string, key: Record<string, unknown>): Promise<LocalWrite>;
|
|
68
|
+
/** Writes not yet confirmed by the sync: pending or sent. */
|
|
69
|
+
pending(): Promise<LocalWrite[]>;
|
|
70
|
+
/** Send whatever is pending now, rather than at the next retry. */
|
|
71
|
+
flush(): Promise<void>;
|
|
72
|
+
/** Stop sending. Pending writes stay recorded, for the next queue. */
|
|
73
|
+
close(): Promise<void>;
|
|
74
|
+
/** @internal The sync's part: called inside each batch's transaction. */
|
|
75
|
+
readonly sync: WriteSync;
|
|
76
|
+
}
|
|
77
|
+
/** @internal What the sync calls. */
|
|
78
|
+
export interface WriteSync {
|
|
79
|
+
/**
|
|
80
|
+
* After a batch was applied to `table` in `tx`: drop the writes it
|
|
81
|
+
* confirms, and apply again the ones still pending.
|
|
82
|
+
*/
|
|
83
|
+
reconcile(tx: ElectricTransaction, table: string, touched: Set<string> | 'all', txids: Set<string>): Promise<void>;
|
|
84
|
+
/** A sync of `table` that can be asked to fetch its shape again (after a rejection). */
|
|
85
|
+
register(table: string, refetch: () => void): () => void;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The key of a row as one string, for matching a write with what the sync
|
|
89
|
+
* brings: Electric sends every value as text (`"1"` for the `1` written).
|
|
90
|
+
*/
|
|
91
|
+
declare function keyText(key: Record<string, unknown>): string;
|
|
92
|
+
/**
|
|
93
|
+
* Start a queue of local writes. Pass it to `syncShapeToTable` /
|
|
94
|
+
* `syncShapesToTables` as `writes`, so that the sync keeps the writes over
|
|
95
|
+
* what it applies and drops them once confirmed.
|
|
96
|
+
*/
|
|
97
|
+
export declare function createWriteQueue(given: WritesDatabase, options: WriteQueueOptions): Promise<WriteQueue>;
|
|
98
|
+
export { keyText as writeKeyText };
|
|
99
|
+
//# sourceMappingURL=writes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writes.d.ts","sourceRoot":"","sources":["../../src/electric/writes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* writes.ts – local writes to synced tables, sent to the app's own API.
|
|
4
|
+
*
|
|
5
|
+
* Electric is a read path: a write goes through the app's API to the source
|
|
6
|
+
* and comes back down the shape. This keeps the wait out of sight:
|
|
7
|
+
*
|
|
8
|
+
* - A write is applied to the synced table at once, and recorded in
|
|
9
|
+
* `ELECTRIC_WRITES` in the same transaction, so it survives a reload.
|
|
10
|
+
* - A sender hands the recorded writes, in order, to `send` (the app's API).
|
|
11
|
+
* A failure is retried; a `WriteRejected` rolls the write back.
|
|
12
|
+
* - Each batch the sync applies is followed, in its transaction, by every
|
|
13
|
+
* write still pending on that table, so the server's older state never
|
|
14
|
+
* shows over a local write.
|
|
15
|
+
* - A sent write is dropped once the source's change has come back: when a
|
|
16
|
+
* change carries the transaction id `send` returned (Electric's `txids`,
|
|
17
|
+
* and the shape server's), or, without one, when the sync brings a change
|
|
18
|
+
* to the same row, or at the latest after `confirmTimeoutMs`.
|
|
19
|
+
*
|
|
20
|
+
* Values are what `exec` takes, keyed by the shape's column names (`title`
|
|
21
|
+
* for the Firebird column `TITLE`): strings, numbers, booleans, `null`,
|
|
22
|
+
* `Date` and `Uint8Array`.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.WriteRejected = void 0;
|
|
26
|
+
exports.createWriteQueue = createWriteQueue;
|
|
27
|
+
exports.writeKeyText = keyText;
|
|
28
|
+
const values_1 = require("./values");
|
|
29
|
+
const serial_1 = require("./serial");
|
|
30
|
+
/** Thrown by `send` when the source refuses a write: it is rolled back, not retried. */
|
|
31
|
+
class WriteRejected extends Error {
|
|
32
|
+
constructor(message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = 'WriteRejected';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.WriteRejected = WriteRejected;
|
|
38
|
+
const WRITES = '"ELECTRIC_WRITES"';
|
|
39
|
+
/** Characters of JSON a write's values may take: the widest UTF-8 VARCHAR a row holds. */
|
|
40
|
+
const MAX_VALUE = 8191;
|
|
41
|
+
/** Values to JSON and back, `Date` and `Uint8Array` included. */
|
|
42
|
+
function encode(value) {
|
|
43
|
+
return JSON.stringify(value, (_, v) => {
|
|
44
|
+
if (v instanceof Uint8Array)
|
|
45
|
+
return { $bytes: Array.from(v) };
|
|
46
|
+
if (typeof v === 'bigint')
|
|
47
|
+
return { $bigint: v.toString() };
|
|
48
|
+
return v;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function decode(text) {
|
|
52
|
+
return JSON.parse(text, (_, v) => {
|
|
53
|
+
if (v && typeof v === 'object' && Array.isArray(v.$bytes))
|
|
54
|
+
return new Uint8Array(v.$bytes);
|
|
55
|
+
if (v && typeof v === 'object' && typeof v.$bigint === 'string')
|
|
56
|
+
return BigInt(v.$bigint);
|
|
57
|
+
if (typeof v === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?Z$/.test(v))
|
|
58
|
+
return new Date(v);
|
|
59
|
+
return v;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The key of a row as one string, for matching a write with what the sync
|
|
64
|
+
* brings: Electric sends every value as text (`"1"` for the `1` written).
|
|
65
|
+
*/
|
|
66
|
+
function keyText(key) {
|
|
67
|
+
return JSON.stringify(Object.keys(key).sort().map((k) => {
|
|
68
|
+
const v = key[k];
|
|
69
|
+
return [k, v === null || v === undefined ? null : v instanceof Date ? v.toISOString() : String(v)];
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Start a queue of local writes. Pass it to `syncShapeToTable` /
|
|
74
|
+
* `syncShapesToTables` as `writes`, so that the sync keeps the writes over
|
|
75
|
+
* what it applies and drops them once confirmed.
|
|
76
|
+
*/
|
|
77
|
+
async function createWriteQueue(given, options) {
|
|
78
|
+
// One transaction at a time with the syncs: see serial.ts.
|
|
79
|
+
const db = (0, serial_1.serialDatabase)(given);
|
|
80
|
+
const retryMs = options.retryMs ?? 2000;
|
|
81
|
+
const confirmTimeoutMs = options.confirmTimeoutMs ?? 60000;
|
|
82
|
+
const report = options.onError ?? ((e) => console.error('[firebird-wasm/electric] write', e));
|
|
83
|
+
await ensureWritesTable(db);
|
|
84
|
+
const keys = new Map();
|
|
85
|
+
/** The primary key of a local table, Firebird column names, from its constraint. */
|
|
86
|
+
const keyOf = async (tx, table) => {
|
|
87
|
+
const name = (0, values_1.firebirdName)(table);
|
|
88
|
+
const known = keys.get(name);
|
|
89
|
+
if (known)
|
|
90
|
+
return known;
|
|
91
|
+
const { rows } = await tx.query('SELECT TRIM(s.RDB$FIELD_NAME) AS F FROM RDB$RELATION_CONSTRAINTS c ' +
|
|
92
|
+
'JOIN RDB$INDEX_SEGMENTS s ON s.RDB$INDEX_NAME = c.RDB$INDEX_NAME ' +
|
|
93
|
+
"WHERE c.RDB$RELATION_NAME = ? AND c.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY' ORDER BY s.RDB$FIELD_POSITION", [name.slice(1, -1).replace(/""/g, '"')]);
|
|
94
|
+
const found = rows.map((r) => String(r['F']));
|
|
95
|
+
if (found.length === 0)
|
|
96
|
+
throw new Error(`Table ${table} has no primary key; local writes need one`);
|
|
97
|
+
keys.set(name, found);
|
|
98
|
+
return found;
|
|
99
|
+
};
|
|
100
|
+
/** Apply one write to its table in `tx`. */
|
|
101
|
+
const applyWrite = async (tx, w) => {
|
|
102
|
+
const table = (0, values_1.firebirdName)(w.table);
|
|
103
|
+
const keyColumns = Object.keys(w.key);
|
|
104
|
+
const where = keyColumns.map((k) => `${(0, values_1.firebirdColumn)(k)} = ?`).join(' AND ');
|
|
105
|
+
const keyValues = keyColumns.map((k) => w.key[k]);
|
|
106
|
+
if (w.operation === 'delete') {
|
|
107
|
+
await tx.exec(`DELETE FROM ${table} WHERE ${where}`, keyValues);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const columns = Object.keys(w.value);
|
|
111
|
+
if (w.operation === 'insert') {
|
|
112
|
+
const all = [...new Set([...keyColumns, ...columns])];
|
|
113
|
+
const values = all.map((c) => (c in w.value ? w.value[c] : w.key[c]));
|
|
114
|
+
await tx.exec(`UPDATE OR INSERT INTO ${table} (${all.map(values_1.firebirdColumn).join(', ')}) VALUES (${all.map(() => '?').join(', ')}) ` +
|
|
115
|
+
`MATCHING (${keyColumns.map(values_1.firebirdColumn).join(', ')})`, values);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (columns.length === 0)
|
|
119
|
+
return;
|
|
120
|
+
await tx.exec(`UPDATE ${table} SET ${columns.map((c) => `${(0, values_1.firebirdColumn)(c)} = ?`).join(', ')} WHERE ${where}`, [...columns.map((c) => w.value[c]), ...keyValues]);
|
|
121
|
+
};
|
|
122
|
+
const load = async (tx, table) => {
|
|
123
|
+
const { rows } = await tx.query(`SELECT ID, TABLE_NAME, OP, KEY_JSON, VALUE_JSON, STATE, TXID, SENT_AT FROM ${WRITES}` +
|
|
124
|
+
(table ? ' WHERE TABLE_NAME = ?' : '') + ' ORDER BY ID', table ? [table] : []);
|
|
125
|
+
return rows.map((r) => ({
|
|
126
|
+
id: Number(r['ID']),
|
|
127
|
+
table: String(r['TABLE_NAME']),
|
|
128
|
+
operation: { I: 'insert', U: 'update', D: 'delete' }[String(r['OP']).trim()],
|
|
129
|
+
key: decode(String(r['KEY_JSON'])),
|
|
130
|
+
value: decode(String(r['VALUE_JSON'] ?? '{}')),
|
|
131
|
+
state: String(r['STATE']).trim(),
|
|
132
|
+
txid: r['TXID'] === null || r['TXID'] === undefined ? null : String(r['TXID']).trim(),
|
|
133
|
+
sentAt: r['SENT_AT'] === null || r['SENT_AT'] === undefined ? null : Number(r['SENT_AT']),
|
|
134
|
+
}));
|
|
135
|
+
};
|
|
136
|
+
const write = async (operation, table, keyGiven, value) => {
|
|
137
|
+
const made = await db.transaction(async (tx) => {
|
|
138
|
+
const keyColumns = await keyOf(tx, table);
|
|
139
|
+
// The key by the shape's names: TITLE is title, "camelCase" stays.
|
|
140
|
+
const shapeName = (c) => (/^[A-Z][A-Z0-9_$]*$/.test(c) ? c.toLowerCase() : c);
|
|
141
|
+
const source = keyGiven ?? value;
|
|
142
|
+
const key = {};
|
|
143
|
+
for (const c of keyColumns) {
|
|
144
|
+
const name = shapeName(c);
|
|
145
|
+
if (!(name in source))
|
|
146
|
+
throw new RangeError(`A write to ${table} needs its key column ${name}`);
|
|
147
|
+
key[name] = source[name];
|
|
148
|
+
}
|
|
149
|
+
const json = encode(value);
|
|
150
|
+
if (json.length > MAX_VALUE) {
|
|
151
|
+
throw new RangeError(`A write's values may take ${MAX_VALUE} characters as JSON; this one takes ${json.length}`);
|
|
152
|
+
}
|
|
153
|
+
const { rows } = await tx.query(`SELECT COALESCE(MAX(ID), 0) + 1 AS N FROM ${WRITES}`);
|
|
154
|
+
const w = { id: Number(rows[0]['N']), table, operation, key, value };
|
|
155
|
+
await applyWrite(tx, w);
|
|
156
|
+
await tx.exec(`INSERT INTO ${WRITES} (ID, TABLE_NAME, OP, KEY_JSON, VALUE_JSON, STATE) VALUES (?, ?, ?, ?, ?, 'P')`, [w.id, table, operation[0].toUpperCase(), encode(key), json]);
|
|
157
|
+
return w;
|
|
158
|
+
});
|
|
159
|
+
kick();
|
|
160
|
+
return made;
|
|
161
|
+
};
|
|
162
|
+
const refetchers = new Map();
|
|
163
|
+
let closed = false;
|
|
164
|
+
let sending = null;
|
|
165
|
+
let timer;
|
|
166
|
+
/** Roll a rejected write back: forget it, and have its table fetched again. */
|
|
167
|
+
const reject = async (w, error) => {
|
|
168
|
+
await db.transaction((tx) => tx.exec(`DELETE FROM ${WRITES} WHERE ID = ?`, [w.id]).then(() => undefined));
|
|
169
|
+
for (const refetch of refetchers.get((0, values_1.firebirdName)(w.table)) ?? [])
|
|
170
|
+
refetch();
|
|
171
|
+
report(error, w);
|
|
172
|
+
};
|
|
173
|
+
/** Send every pending write, in order; stop at the first that fails. */
|
|
174
|
+
const sendAll = async () => {
|
|
175
|
+
for (;;) {
|
|
176
|
+
if (closed)
|
|
177
|
+
return;
|
|
178
|
+
const next = (await db.transaction((tx) => load(tx))).find((w) => w.state === 'P');
|
|
179
|
+
if (!next)
|
|
180
|
+
return;
|
|
181
|
+
let result;
|
|
182
|
+
try {
|
|
183
|
+
result = await options.send({ id: next.id, table: next.table, operation: next.operation, key: next.key, value: next.value });
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
187
|
+
if (error instanceof WriteRejected || error.rejected) {
|
|
188
|
+
await reject(next, error);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
report(error, next);
|
|
192
|
+
if (!closed) {
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
timer = setTimeout(kick, retryMs);
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const txid = result && result.txid !== undefined && result.txid !== null ? String(result.txid) : null;
|
|
199
|
+
await db.transaction((tx) => tx.exec(`UPDATE ${WRITES} SET STATE = 'S', TXID = ?, SENT_AT = ? WHERE ID = ?`, [txid, Date.now(), next.id])
|
|
200
|
+
.then(() => undefined));
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
function kick() {
|
|
204
|
+
if (closed || sending)
|
|
205
|
+
return;
|
|
206
|
+
sending = sendAll()
|
|
207
|
+
.catch((err) => report(err instanceof Error ? err : new Error(String(err)), undefined))
|
|
208
|
+
.finally(() => { sending = null; });
|
|
209
|
+
}
|
|
210
|
+
const sync = {
|
|
211
|
+
async reconcile(tx, table, touched, txids) {
|
|
212
|
+
const name = (0, values_1.firebirdName)(table);
|
|
213
|
+
const writes = (await load(tx)).filter((w) => (0, values_1.firebirdName)(w.table) === name);
|
|
214
|
+
if (writes.length === 0)
|
|
215
|
+
return;
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
for (const w of writes) {
|
|
218
|
+
// Sent, and its change is back: the source's row now holds it.
|
|
219
|
+
const confirmed = w.state === 'S' && ((w.txid !== null && txids.has(w.txid)) ||
|
|
220
|
+
(w.txid === null && (touched === 'all' || touched.has(keyText(w.key)))) ||
|
|
221
|
+
(w.sentAt !== null && now - w.sentAt > confirmTimeoutMs));
|
|
222
|
+
if (confirmed) {
|
|
223
|
+
await tx.exec(`DELETE FROM ${WRITES} WHERE ID = ?`, [w.id]);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
await applyWrite(tx, w);
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
register(table, refetch) {
|
|
230
|
+
const name = (0, values_1.firebirdName)(table);
|
|
231
|
+
const set = refetchers.get(name) ?? new Set();
|
|
232
|
+
set.add(refetch);
|
|
233
|
+
refetchers.set(name, set);
|
|
234
|
+
return () => set.delete(refetch);
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
// Writes left from before (a reload, offline) go out now.
|
|
238
|
+
kick();
|
|
239
|
+
return {
|
|
240
|
+
insert: (table, value) => write('insert', table, null, value),
|
|
241
|
+
update: (table, key, value) => write('update', table, key, value),
|
|
242
|
+
delete: (table, key) => write('delete', table, key, {}),
|
|
243
|
+
pending: () => db.transaction((tx) => load(tx)).then((ws) => ws.map(({ id, table, operation, key, value }) => ({ id, table, operation, key, value }))),
|
|
244
|
+
flush: async () => {
|
|
245
|
+
clearTimeout(timer);
|
|
246
|
+
kick();
|
|
247
|
+
await sending;
|
|
248
|
+
},
|
|
249
|
+
close: async () => {
|
|
250
|
+
closed = true;
|
|
251
|
+
clearTimeout(timer);
|
|
252
|
+
await sending;
|
|
253
|
+
},
|
|
254
|
+
sync,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async function ensureWritesTable(db) {
|
|
258
|
+
for (let attempt = 1;; attempt++) {
|
|
259
|
+
try {
|
|
260
|
+
await db.transaction(async (tx) => {
|
|
261
|
+
const { rows } = await tx.query("SELECT 1 AS X FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = 'ELECTRIC_WRITES'");
|
|
262
|
+
if (rows.length > 0)
|
|
263
|
+
return;
|
|
264
|
+
await tx.exec(`CREATE TABLE ${WRITES} (ID INTEGER NOT NULL PRIMARY KEY, TABLE_NAME VARCHAR(255) NOT NULL, ` +
|
|
265
|
+
// Not a BLOB: FirebirdLite reads a BLOB back as a handle, not text.
|
|
266
|
+
'OP CHAR(1) NOT NULL, KEY_JSON VARCHAR(4000) CHARACTER SET UTF8 NOT NULL, ' +
|
|
267
|
+
`VALUE_JSON VARCHAR(${MAX_VALUE}) CHARACTER SET UTF8, ` +
|
|
268
|
+
"STATE CHAR(1) DEFAULT 'P' NOT NULL, TXID VARCHAR(40), SENT_AT DOUBLE PRECISION)");
|
|
269
|
+
});
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
const made = await db.transaction(async (tx) => (await tx.query("SELECT 1 AS X FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = 'ELECTRIC_WRITES'")).rows.length > 0);
|
|
274
|
+
if (made)
|
|
275
|
+
return;
|
|
276
|
+
if (attempt >= 5)
|
|
277
|
+
throw error;
|
|
278
|
+
await new Promise((resolve) => setTimeout(resolve, 20 * attempt));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
//# sourceMappingURL=writes.js.map
|