UNPKG

@orbitinghail/sqlsync-worker

Version:

SQLSync is a collaborative offline-first wrapper around SQLite. It is designed to synchronize web application state between users, devices, and the edge.

974 lines (954 loc) 39.4 kB
/*! scure-base - MIT License (c) 2022 Paul Miller (paulmillr.com) */ // Utilities function isBytes(a) { return (a instanceof Uint8Array || (a != null && typeof a === 'object' && a.constructor.name === 'Uint8Array')); } /** * @__NO_SIDE_EFFECTS__ */ function chain(...args) { const id = (a) => a; // Wrap call in closure so JIT can inline calls const wrap = (a, b) => (c) => a(b(c)); // Construct chain of args[-1].encode(args[-2].encode([...])) const encode = args.map((x) => x.encode).reduceRight(wrap, id); // Construct chain of args[0].decode(args[1].decode(...)) const decode = args.map((x) => x.decode).reduce(wrap, id); return { encode, decode }; } /** * Encodes integer radix representation to array of strings using alphabet and back * @__NO_SIDE_EFFECTS__ */ function alphabet(alphabet) { return { encode: (digits) => { if (!Array.isArray(digits) || (digits.length && typeof digits[0] !== 'number')) throw new Error('alphabet.encode input should be an array of numbers'); return digits.map((i) => { if (i < 0 || i >= alphabet.length) throw new Error(`Digit index outside alphabet: ${i} (alphabet: ${alphabet.length})`); return alphabet[i]; }); }, decode: (input) => { if (!Array.isArray(input) || (input.length && typeof input[0] !== 'string')) throw new Error('alphabet.decode input should be array of strings'); return input.map((letter) => { if (typeof letter !== 'string') throw new Error(`alphabet.decode: not string element=${letter}`); const index = alphabet.indexOf(letter); if (index === -1) throw new Error(`Unknown letter: "${letter}". Allowed: ${alphabet}`); return index; }); }, }; } /** * @__NO_SIDE_EFFECTS__ */ function join(separator = '') { if (typeof separator !== 'string') throw new Error('join separator should be string'); return { encode: (from) => { if (!Array.isArray(from) || (from.length && typeof from[0] !== 'string')) throw new Error('join.encode input should be array of strings'); for (let i of from) if (typeof i !== 'string') throw new Error(`join.encode: non-string input=${i}`); return from.join(separator); }, decode: (to) => { if (typeof to !== 'string') throw new Error('join.decode input should be string'); return to.split(separator); }, }; } /** * Slow: O(n^2) time complexity * @__NO_SIDE_EFFECTS__ */ function convertRadix(data, from, to) { // base 1 is impossible if (from < 2) throw new Error(`convertRadix: wrong from=${from}, base cannot be less than 2`); if (to < 2) throw new Error(`convertRadix: wrong to=${to}, base cannot be less than 2`); if (!Array.isArray(data)) throw new Error('convertRadix: data should be array'); if (!data.length) return []; let pos = 0; const res = []; const digits = Array.from(data); digits.forEach((d) => { if (d < 0 || d >= from) throw new Error(`Wrong integer: ${d}`); }); while (true) { let carry = 0; let done = true; for (let i = pos; i < digits.length; i++) { const digit = digits[i]; const digitBase = from * carry + digit; if (!Number.isSafeInteger(digitBase) || (from * carry) / from !== carry || digitBase - digit !== from * carry) { throw new Error('convertRadix: carry overflow'); } carry = digitBase % to; const rounded = Math.floor(digitBase / to); digits[i] = rounded; if (!Number.isSafeInteger(rounded) || rounded * to + carry !== digitBase) throw new Error('convertRadix: carry overflow'); if (!done) continue; else if (!rounded) pos = i; else done = false; } res.push(carry); if (done) break; } for (let i = 0; i < data.length - 1 && data[i] === 0; i++) res.push(0); return res.reverse(); } /** * @__NO_SIDE_EFFECTS__ */ function radix(num) { return { encode: (bytes) => { if (!isBytes(bytes)) throw new Error('radix.encode input should be Uint8Array'); return convertRadix(Array.from(bytes), 2 ** 8, num); }, decode: (digits) => { if (!Array.isArray(digits) || (digits.length && typeof digits[0] !== 'number')) throw new Error('radix.decode input should be array of numbers'); return Uint8Array.from(convertRadix(digits, num, 2 ** 8)); }, }; } // base58 code // ----------- const genBase58 = (abc) => chain(radix(58), alphabet(abc), join('')); const base58 = /* @__PURE__ */ genBase58('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'); // randomJournalId generates a random 128 bit (16 byte) JournalId function randomJournalId() { return crypto.getRandomValues(new Uint8Array(16)); } // randomJournalId256 generates a random 256 bit (32 byte) JournalId function randomJournalId256() { return crypto.getRandomValues(new Uint8Array(32)); } // journalIdToString converts a JournalId to a base58 encoded string function journalIdToString(id) { return base58.encode(id); } // journalIdFromString converts a base58 encoded string to a JournalId function journalIdFromString(s) { const bytes = base58.decode(s); if (bytes.length === 16 || bytes.length === 32) { return bytes; } throw new Error(`invalid journal id: ${s}; must be either 16 or 32 bytes`); } /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function __classPrivateFieldGet(receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); } function __classPrivateFieldSet(receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; var sha256 = {exports: {}}; (function (module) { (function (root, factory) { // Hack to make all exports of this module sha256 function object properties. var exports = {}; factory(exports); var sha256 = exports["default"]; for (var k in exports) { sha256[k] = exports[k]; } { module.exports = sha256; } })(commonjsGlobal, function(exports) { exports.__esModule = true; // SHA-256 (+ HMAC and PBKDF2) for JavaScript. // // Written in 2014-2016 by Dmitry Chestnykh. // Public domain, no warranty. // // Functions (accept and return Uint8Arrays): // // sha256(message) -> hash // sha256.hmac(key, message) -> mac // sha256.pbkdf2(password, salt, rounds, dkLen) -> dk // // Classes: // // new sha256.Hash() // new sha256.HMAC(key) // exports.digestLength = 32; exports.blockSize = 64; // SHA-256 constants var K = new Uint32Array([ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 ]); function hashBlocks(w, v, p, pos, len) { var a, b, c, d, e, f, g, h, u, i, j, t1, t2; while (len >= 64) { a = v[0]; b = v[1]; c = v[2]; d = v[3]; e = v[4]; f = v[5]; g = v[6]; h = v[7]; for (i = 0; i < 16; i++) { j = pos + i * 4; w[i] = (((p[j] & 0xff) << 24) | ((p[j + 1] & 0xff) << 16) | ((p[j + 2] & 0xff) << 8) | (p[j + 3] & 0xff)); } for (i = 16; i < 64; i++) { u = w[i - 2]; t1 = (u >>> 17 | u << (32 - 17)) ^ (u >>> 19 | u << (32 - 19)) ^ (u >>> 10); u = w[i - 15]; t2 = (u >>> 7 | u << (32 - 7)) ^ (u >>> 18 | u << (32 - 18)) ^ (u >>> 3); w[i] = (t1 + w[i - 7] | 0) + (t2 + w[i - 16] | 0); } for (i = 0; i < 64; i++) { t1 = (((((e >>> 6 | e << (32 - 6)) ^ (e >>> 11 | e << (32 - 11)) ^ (e >>> 25 | e << (32 - 25))) + ((e & f) ^ (~e & g))) | 0) + ((h + ((K[i] + w[i]) | 0)) | 0)) | 0; t2 = (((a >>> 2 | a << (32 - 2)) ^ (a >>> 13 | a << (32 - 13)) ^ (a >>> 22 | a << (32 - 22))) + ((a & b) ^ (a & c) ^ (b & c))) | 0; h = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0; } v[0] += a; v[1] += b; v[2] += c; v[3] += d; v[4] += e; v[5] += f; v[6] += g; v[7] += h; pos += 64; len -= 64; } return pos; } // Hash implements SHA256 hash algorithm. var Hash = /** @class */ (function () { function Hash() { this.digestLength = exports.digestLength; this.blockSize = exports.blockSize; // Note: Int32Array is used instead of Uint32Array for performance reasons. this.state = new Int32Array(8); // hash state this.temp = new Int32Array(64); // temporary state this.buffer = new Uint8Array(128); // buffer for data to hash this.bufferLength = 0; // number of bytes in buffer this.bytesHashed = 0; // number of total bytes hashed this.finished = false; // indicates whether the hash was finalized this.reset(); } // Resets hash state making it possible // to re-use this instance to hash other data. Hash.prototype.reset = function () { this.state[0] = 0x6a09e667; this.state[1] = 0xbb67ae85; this.state[2] = 0x3c6ef372; this.state[3] = 0xa54ff53a; this.state[4] = 0x510e527f; this.state[5] = 0x9b05688c; this.state[6] = 0x1f83d9ab; this.state[7] = 0x5be0cd19; this.bufferLength = 0; this.bytesHashed = 0; this.finished = false; return this; }; // Cleans internal buffers and re-initializes hash state. Hash.prototype.clean = function () { for (var i = 0; i < this.buffer.length; i++) { this.buffer[i] = 0; } for (var i = 0; i < this.temp.length; i++) { this.temp[i] = 0; } this.reset(); }; // Updates hash state with the given data. // // Optionally, length of the data can be specified to hash // fewer bytes than data.length. // // Throws error when trying to update already finalized hash: // instance must be reset to use it again. Hash.prototype.update = function (data, dataLength) { if (dataLength === void 0) { dataLength = data.length; } if (this.finished) { throw new Error("SHA256: can't update because hash was finished."); } var dataPos = 0; this.bytesHashed += dataLength; if (this.bufferLength > 0) { while (this.bufferLength < 64 && dataLength > 0) { this.buffer[this.bufferLength++] = data[dataPos++]; dataLength--; } if (this.bufferLength === 64) { hashBlocks(this.temp, this.state, this.buffer, 0, 64); this.bufferLength = 0; } } if (dataLength >= 64) { dataPos = hashBlocks(this.temp, this.state, data, dataPos, dataLength); dataLength %= 64; } while (dataLength > 0) { this.buffer[this.bufferLength++] = data[dataPos++]; dataLength--; } return this; }; // Finalizes hash state and puts hash into out. // // If hash was already finalized, puts the same value. Hash.prototype.finish = function (out) { if (!this.finished) { var bytesHashed = this.bytesHashed; var left = this.bufferLength; var bitLenHi = (bytesHashed / 0x20000000) | 0; var bitLenLo = bytesHashed << 3; var padLength = (bytesHashed % 64 < 56) ? 64 : 128; this.buffer[left] = 0x80; for (var i = left + 1; i < padLength - 8; i++) { this.buffer[i] = 0; } this.buffer[padLength - 8] = (bitLenHi >>> 24) & 0xff; this.buffer[padLength - 7] = (bitLenHi >>> 16) & 0xff; this.buffer[padLength - 6] = (bitLenHi >>> 8) & 0xff; this.buffer[padLength - 5] = (bitLenHi >>> 0) & 0xff; this.buffer[padLength - 4] = (bitLenLo >>> 24) & 0xff; this.buffer[padLength - 3] = (bitLenLo >>> 16) & 0xff; this.buffer[padLength - 2] = (bitLenLo >>> 8) & 0xff; this.buffer[padLength - 1] = (bitLenLo >>> 0) & 0xff; hashBlocks(this.temp, this.state, this.buffer, 0, padLength); this.finished = true; } for (var i = 0; i < 8; i++) { out[i * 4 + 0] = (this.state[i] >>> 24) & 0xff; out[i * 4 + 1] = (this.state[i] >>> 16) & 0xff; out[i * 4 + 2] = (this.state[i] >>> 8) & 0xff; out[i * 4 + 3] = (this.state[i] >>> 0) & 0xff; } return this; }; // Returns the final hash digest. Hash.prototype.digest = function () { var out = new Uint8Array(this.digestLength); this.finish(out); return out; }; // Internal function for use in HMAC for optimization. Hash.prototype._saveState = function (out) { for (var i = 0; i < this.state.length; i++) { out[i] = this.state[i]; } }; // Internal function for use in HMAC for optimization. Hash.prototype._restoreState = function (from, bytesHashed) { for (var i = 0; i < this.state.length; i++) { this.state[i] = from[i]; } this.bytesHashed = bytesHashed; this.finished = false; this.bufferLength = 0; }; return Hash; }()); exports.Hash = Hash; // HMAC implements HMAC-SHA256 message authentication algorithm. var HMAC = /** @class */ (function () { function HMAC(key) { this.inner = new Hash(); this.outer = new Hash(); this.blockSize = this.inner.blockSize; this.digestLength = this.inner.digestLength; var pad = new Uint8Array(this.blockSize); if (key.length > this.blockSize) { (new Hash()).update(key).finish(pad).clean(); } else { for (var i = 0; i < key.length; i++) { pad[i] = key[i]; } } for (var i = 0; i < pad.length; i++) { pad[i] ^= 0x36; } this.inner.update(pad); for (var i = 0; i < pad.length; i++) { pad[i] ^= 0x36 ^ 0x5c; } this.outer.update(pad); this.istate = new Uint32Array(8); this.ostate = new Uint32Array(8); this.inner._saveState(this.istate); this.outer._saveState(this.ostate); for (var i = 0; i < pad.length; i++) { pad[i] = 0; } } // Returns HMAC state to the state initialized with key // to make it possible to run HMAC over the other data with the same // key without creating a new instance. HMAC.prototype.reset = function () { this.inner._restoreState(this.istate, this.inner.blockSize); this.outer._restoreState(this.ostate, this.outer.blockSize); return this; }; // Cleans HMAC state. HMAC.prototype.clean = function () { for (var i = 0; i < this.istate.length; i++) { this.ostate[i] = this.istate[i] = 0; } this.inner.clean(); this.outer.clean(); }; // Updates state with provided data. HMAC.prototype.update = function (data) { this.inner.update(data); return this; }; // Finalizes HMAC and puts the result in out. HMAC.prototype.finish = function (out) { if (this.outer.finished) { this.outer.finish(out); } else { this.inner.finish(out); this.outer.update(out, this.digestLength).finish(out); } return this; }; // Returns message authentication code. HMAC.prototype.digest = function () { var out = new Uint8Array(this.digestLength); this.finish(out); return out; }; return HMAC; }()); exports.HMAC = HMAC; // Returns SHA256 hash of data. function hash(data) { var h = (new Hash()).update(data); var digest = h.digest(); h.clean(); return digest; } exports.hash = hash; // Function hash is both available as module.hash and as default export. exports["default"] = hash; // Returns HMAC-SHA256 of data under the key. function hmac(key, data) { var h = (new HMAC(key)).update(data); var digest = h.digest(); h.clean(); return digest; } exports.hmac = hmac; // Fills hkdf buffer like this: // T(1) = HMAC-Hash(PRK, T(0) | info | 0x01) function fillBuffer(buffer, hmac, info, counter) { // Counter is a byte value: check if it overflowed. var num = counter[0]; if (num === 0) { throw new Error("hkdf: cannot expand more"); } // Prepare HMAC instance for new data with old key. hmac.reset(); // Hash in previous output if it was generated // (i.e. counter is greater than 1). if (num > 1) { hmac.update(buffer); } // Hash in info if it exists. if (info) { hmac.update(info); } // Hash in the counter. hmac.update(counter); // Output result to buffer and clean HMAC instance. hmac.finish(buffer); // Increment counter inside typed array, this works properly. counter[0]++; } var hkdfSalt = new Uint8Array(exports.digestLength); // Filled with zeroes. function hkdf(key, salt, info, length) { if (salt === void 0) { salt = hkdfSalt; } if (length === void 0) { length = 32; } var counter = new Uint8Array([1]); // HKDF-Extract uses salt as HMAC key, and key as data. var okm = hmac(salt, key); // Initialize HMAC for expanding with extracted key. // Ensure no collisions with `hmac` function. var hmac_ = new HMAC(okm); // Allocate buffer. var buffer = new Uint8Array(hmac_.digestLength); var bufpos = buffer.length; var out = new Uint8Array(length); for (var i = 0; i < length; i++) { if (bufpos === buffer.length) { fillBuffer(buffer, hmac_, info, counter); bufpos = 0; } out[i] = buffer[bufpos++]; } hmac_.clean(); buffer.fill(0); counter.fill(0); return out; } exports.hkdf = hkdf; // Derives a key from password and salt using PBKDF2-HMAC-SHA256 // with the given number of iterations. // // The number of bytes returned is equal to dkLen. // // (For better security, avoid dkLen greater than hash length - 32 bytes). function pbkdf2(password, salt, iterations, dkLen) { var prf = new HMAC(password); var len = prf.digestLength; var ctr = new Uint8Array(4); var t = new Uint8Array(len); var u = new Uint8Array(len); var dk = new Uint8Array(dkLen); for (var i = 0; i * len < dkLen; i++) { var c = i + 1; ctr[0] = (c >>> 24) & 0xff; ctr[1] = (c >>> 16) & 0xff; ctr[2] = (c >>> 8) & 0xff; ctr[3] = (c >>> 0) & 0xff; prf.reset(); prf.update(salt); prf.update(ctr); prf.finish(u); for (var j = 0; j < len; j++) { t[j] = u[j]; } for (var j = 2; j <= iterations; j++) { prf.reset(); prf.update(u).finish(u); for (var k = 0; k < len; k++) { t[k] ^= u[k]; } } for (var j = 0; j < len && i * len + j < dkLen; j++) { dk[i * len + j] = t[j]; } } for (var i = 0; i < len; i++) { t[i] = u[i] = 0; } for (var i = 0; i < 4; i++) { ctr[i] = 0; } prf.clean(); return dk; } exports.pbkdf2 = pbkdf2; }); } (sha256)); var sha256Exports = sha256.exports; function assertUnreachable(err, x) { throw new Error(`unreachable: ${err}; got ${JSON.stringify(x)}`); } function initWorker(workerUrl) { const type = workerUrl.toString().endsWith(".cjs") ? "classic" : "module"; if (typeof SharedWorker !== "undefined") { const worker = new SharedWorker(workerUrl, { type }); return worker.port; } const worker = new Worker(workerUrl, { type }); // biome-ignore lint/suspicious/noExplicitAny: WebWorker extends MessagePort via duck typing return worker; } const UTF8Encoder = new TextEncoder(); const serializeMutationAsJSON = (mutation) => { const serialized = JSON.stringify(mutation); return UTF8Encoder.encode(serialized); }; function toRows(columns, rows) { const out = []; for (const row of rows) { const obj = {}; for (let i = 0; i < columns.length; i++) { obj[columns[i]] = row[i]; } out.push(obj); } return out; } const pendingPromise = () => { let resolve; const promise = new Promise((r) => { resolve = r; }); // biome-ignore lint/style/noNonNullAssertion: we know resolve is defined because the promise constructor runs syncronously return [promise, resolve]; }; const sha256Digest = (data) => __awaiter(void 0, void 0, void 0, function* () { var _a; if ((_a = crypto === null || crypto === void 0 ? void 0 : crypto.subtle) === null || _a === void 0 ? void 0 : _a.digest) { const hash = yield crypto.subtle.digest("SHA-256", data); return new Uint8Array(hash); } return Promise.resolve(sha256Exports.hash(data)); }); const UTF8_ENCODER = new TextEncoder(); function normalizeQuery(query) { if (typeof query === "string") { return { sql: query, params: [] }; } return query; } /** * Returns a parameterized query object with the given SQL string and parameters. * This function should be used as a template literal tag. * * @example * const query = sql`SELECT * FROM users WHERE id = ${userId}`; * * @param chunks - An array of string literals. * @param params - An array of parameter values to be inserted into the SQL string. * @returns A parameterized query object with the given SQL string and parameters. */ function sql(chunks, ...params) { return { sql: chunks.join("?"), params, }; } function toQueryKey(query) { return __awaiter(this, void 0, void 0, function* () { const queryJson = JSON.stringify([query.sql, query.params]); const encoded = UTF8_ENCODER.encode(queryJson); const hashed = yield sha256Digest(encoded); return base58.encode(new Uint8Array(hashed)); }); } var _SQLSync_instances, _SQLSync_port, _SQLSync_openDocs, _SQLSync_pendingOpens, _SQLSync_msgHandlers, _SQLSync_querySubscriptions, _SQLSync_connectionStatus, _SQLSync_connectionStatusListeners, _SQLSync_handleMessage, _SQLSync_handleDocEvent, _SQLSync_send, _SQLSync_boot, _SQLSync_open, _SQLSync_unsubscribeIfNeeded; const nextHandlerId = (() => { let handlerId = 0; return () => handlerId++; })(); class SQLSync { constructor(workerUrl, wasmUrl, coordinatorUrl) { _SQLSync_instances.add(this); _SQLSync_port.set(this, void 0); _SQLSync_openDocs.set(this, new Set()); _SQLSync_pendingOpens.set(this, new Map()); _SQLSync_msgHandlers.set(this, new Map()); _SQLSync_querySubscriptions.set(this, new Map()); _SQLSync_connectionStatus.set(this, "disconnected"); _SQLSync_connectionStatusListeners.set(this, new Set()); __classPrivateFieldSet(this, _SQLSync_msgHandlers, new Map(), "f"); const port = initWorker(workerUrl); __classPrivateFieldSet(this, _SQLSync_port, port, "f"); // We use a WeakRef here to avoid a circular reference between this.port and this. // This allows the SQLSync object to be garbage collected when it is no longer needed. const weakThis = new WeakRef(this); __classPrivateFieldGet(this, _SQLSync_port, "f").onmessage = (msg) => { const thisRef = weakThis.deref(); if (thisRef) { __classPrivateFieldGet(thisRef, _SQLSync_instances, "m", _SQLSync_handleMessage).call(thisRef, msg); } else { console.log("sqlsync: dropping message; sqlsync object has been garbage collected", msg.data); // clean up the port port.postMessage({ tag: "Close", handlerId: 0 }); port.onmessage = null; return; } }; __classPrivateFieldGet(this, _SQLSync_instances, "m", _SQLSync_boot).call(this, wasmUrl.toString(), coordinatorUrl === null || coordinatorUrl === void 0 ? void 0 : coordinatorUrl.toString()).catch((err) => { // TODO: expose this error to the app in a nicer way // probably through some event handlers on the SQLSync object console.error("sqlsync boot failed", err); throw err; }); } close() { __classPrivateFieldGet(this, _SQLSync_port, "f").onmessage = null; __classPrivateFieldGet(this, _SQLSync_port, "f").postMessage({ tag: "Close", handlerId: 0 }); } query(docId, docType, sql, params) { return __awaiter(this, void 0, void 0, function* () { if (!__classPrivateFieldGet(this, _SQLSync_openDocs, "f").has(docId)) { yield __classPrivateFieldGet(this, _SQLSync_instances, "m", _SQLSync_open).call(this, docId, docType); } const reply = yield __classPrivateFieldGet(this, _SQLSync_instances, "m", _SQLSync_send).call(this, "RecordSet", { tag: "Doc", docId: docId, req: { tag: "Query", sql, params }, }); return toRows(reply.columns, reply.rows); }); } subscribe(docId, docType, query, subscription) { return __awaiter(this, void 0, void 0, function* () { if (!__classPrivateFieldGet(this, _SQLSync_openDocs, "f").has(docId)) { yield __classPrivateFieldGet(this, _SQLSync_instances, "m", _SQLSync_open).call(this, docId, docType); } const queryKey = yield toQueryKey(query); // get or create subscription let subscriptions = __classPrivateFieldGet(this, _SQLSync_querySubscriptions, "f").get(queryKey); if (!subscriptions) { subscriptions = []; __classPrivateFieldGet(this, _SQLSync_querySubscriptions, "f").set(queryKey, subscriptions); } if (subscriptions.indexOf(subscription) === -1) { subscriptions.push(subscription); } else { throw new Error("sqlsync: duplicate subscription"); } // send subscribe request yield __classPrivateFieldGet(this, _SQLSync_instances, "m", _SQLSync_send).call(this, "Ack", { tag: "Doc", docId, req: { tag: "QuerySubscribe", key: queryKey, sql: query.sql, params: query.params }, }); // return unsubscribe function return () => { const subscriptions = __classPrivateFieldGet(this, _SQLSync_querySubscriptions, "f").get(queryKey); if (!subscriptions) { // no subscriptions return; } const idx = subscriptions.indexOf(subscription); if (idx === -1) { // no subscription return; } subscriptions.splice(idx, 1); window.setTimeout(() => { // we want to wait a tiny bit before sending finalizing the unsubscribe // to handle the case that React resubscribes to the same query right away __classPrivateFieldGet(this, _SQLSync_instances, "m", _SQLSync_unsubscribeIfNeeded).call(this, docId, queryKey).catch((err) => { console.error("sqlsync: error unsubscribing", err); }); }, 10); }; }); } mutate(docId, docType, mutation) { return __awaiter(this, void 0, void 0, function* () { if (!__classPrivateFieldGet(this, _SQLSync_openDocs, "f").has(docId)) { yield __classPrivateFieldGet(this, _SQLSync_instances, "m", _SQLSync_open).call(this, docId, docType); } yield __classPrivateFieldGet(this, _SQLSync_instances, "m", _SQLSync_send).call(this, "Ack", { tag: "Doc", docId, req: { tag: "Mutate", mutation: docType.serializeMutation(mutation) }, }); }); } get connectionStatus() { return __classPrivateFieldGet(this, _SQLSync_connectionStatus, "f"); } addConnectionStatusListener(listener) { __classPrivateFieldGet(this, _SQLSync_connectionStatusListeners, "f").add(listener); return () => { __classPrivateFieldGet(this, _SQLSync_connectionStatusListeners, "f").delete(listener); }; } setConnectionEnabled(docId, docType, enabled) { return __awaiter(this, void 0, void 0, function* () { if (!__classPrivateFieldGet(this, _SQLSync_openDocs, "f").has(docId)) { yield __classPrivateFieldGet(this, _SQLSync_instances, "m", _SQLSync_open).call(this, docId, docType); } yield __classPrivateFieldGet(this, _SQLSync_instances, "m", _SQLSync_send).call(this, "Ack", { tag: "Doc", docId, req: { tag: "SetConnectionEnabled", enabled }, }); }); } } _SQLSync_port = new WeakMap(), _SQLSync_openDocs = new WeakMap(), _SQLSync_pendingOpens = new WeakMap(), _SQLSync_msgHandlers = new WeakMap(), _SQLSync_querySubscriptions = new WeakMap(), _SQLSync_connectionStatus = new WeakMap(), _SQLSync_connectionStatusListeners = new WeakMap(), _SQLSync_instances = new WeakSet(), _SQLSync_handleMessage = function _SQLSync_handleMessage(event) { const msg = event.data; if (msg.tag === "Reply") { console.log("sqlsync: received reply", msg.handlerId, msg.reply); const handler = __classPrivateFieldGet(this, _SQLSync_msgHandlers, "f").get(msg.handlerId); if (handler) { handler(msg.reply); } else { console.error("sqlsync: no handler for message", msg); throw new Error("no handler for message"); } } else if (msg.tag === "Event") { __classPrivateFieldGet(this, _SQLSync_instances, "m", _SQLSync_handleDocEvent).call(this, msg.docId, msg.evt); } else { assertUnreachable("unknown message", msg); } }, _SQLSync_handleDocEvent = function _SQLSync_handleDocEvent(docId, evt) { console.log(`sqlsync: doc ${journalIdToString(docId)} received event`, evt); if (evt.tag === "ConnectionStatus") { __classPrivateFieldSet(this, _SQLSync_connectionStatus, evt.status, "f"); for (const listener of __classPrivateFieldGet(this, _SQLSync_connectionStatusListeners, "f")) { listener(evt.status); } } else if (evt.tag === "SubscriptionChanged") { const subscriptions = __classPrivateFieldGet(this, _SQLSync_querySubscriptions, "f").get(evt.key); if (subscriptions) { for (const subscription of subscriptions) { subscription.handleRows(toRows(evt.columns, evt.rows)); } } } else if (evt.tag === "SubscriptionErr") { const subscriptions = __classPrivateFieldGet(this, _SQLSync_querySubscriptions, "f").get(evt.key); if (subscriptions) { for (const subscription of subscriptions) { subscription.handleErr(evt.err); } } } else { assertUnreachable("unknown event", evt); } }, _SQLSync_send = function _SQLSync_send(expectedReplyTag, msg) { return new Promise((resolve, reject) => { const handlerId = nextHandlerId(); const req = Object.assign(Object.assign({}, msg), { handlerId }); console.log("sqlsync: sending message", req.handlerId, req.tag === "Doc" ? req.req : req); __classPrivateFieldGet(this, _SQLSync_msgHandlers, "f").set(handlerId, (msg) => { __classPrivateFieldGet(this, _SQLSync_msgHandlers, "f").delete(handlerId); if (msg.tag === "Err") { reject(msg.err); } else if (msg.tag === expectedReplyTag) { // TODO: is it possible to get Typescript to infer this cast? resolve(msg); } else { console.warn("sqlsync: unexpected reply", msg); reject(new Error(`expected ${expectedReplyTag} reply; got ${msg.tag}`)); } }); __classPrivateFieldGet(this, _SQLSync_port, "f").postMessage(req); }); }, _SQLSync_boot = function _SQLSync_boot(wasmUrl, coordinatorUrl) { return __awaiter(this, void 0, void 0, function* () { yield __classPrivateFieldGet(this, _SQLSync_instances, "m", _SQLSync_send).call(this, "Ack", { tag: "Boot", wasmUrl, coordinatorUrl, }); }); }, _SQLSync_open = function _SQLSync_open(docId, docType) { return __awaiter(this, void 0, void 0, function* () { let openPromise = __classPrivateFieldGet(this, _SQLSync_pendingOpens, "f").get(docId); if (!openPromise) { openPromise = __classPrivateFieldGet(this, _SQLSync_instances, "m", _SQLSync_send).call(this, "Ack", { tag: "Doc", docId, req: { tag: "Open", reducerUrl: docType.reducerUrl.toString(), }, }); __classPrivateFieldGet(this, _SQLSync_pendingOpens, "f").set(docId, openPromise); } yield openPromise; __classPrivateFieldGet(this, _SQLSync_pendingOpens, "f").delete(docId); __classPrivateFieldGet(this, _SQLSync_openDocs, "f").add(docId); }); }, _SQLSync_unsubscribeIfNeeded = function _SQLSync_unsubscribeIfNeeded(docId, queryKey) { return __awaiter(this, void 0, void 0, function* () { const subscriptions = __classPrivateFieldGet(this, _SQLSync_querySubscriptions, "f").get(queryKey); if (Array.isArray(subscriptions) && subscriptions.length === 0) { // query subscription is still registered but has no subscriptions on our side // inform the worker that we are no longer interested in this query __classPrivateFieldGet(this, _SQLSync_querySubscriptions, "f").delete(queryKey); if (__classPrivateFieldGet(this, _SQLSync_openDocs, "f").has(docId)) { yield __classPrivateFieldGet(this, _SQLSync_instances, "m", _SQLSync_send).call(this, "Ack", { tag: "Doc", docId, req: { tag: "QueryUnsubscribe", key: queryKey }, }); } } }); }; export { SQLSync, journalIdFromString, journalIdToString, normalizeQuery, pendingPromise, randomJournalId, randomJournalId256, serializeMutationAsJSON, sql }; //# sourceMappingURL=index.js.map