@tanstack/db-collections
Version:
A collection for (aspirationally) every way of loading your data
194 lines (193 loc) • 5.98 kB
JavaScript
import { ShapeStream, isChangeMessage, isControlMessage } from "@electric-sql/client";
import { Store } from "@tanstack/store";
import DebugModule from "debug";
const debug = DebugModule.debug(`ts/db:electric`);
function isUpToDateMessage(message) {
return isControlMessage(message) && message.headers.control === `up-to-date`;
}
function hasTxids(message) {
return `txids` in message.headers && Array.isArray(message.headers.txids);
}
function electricCollectionOptions(config) {
const seenTxids = new Store(/* @__PURE__ */ new Set([]));
const sync = createElectricSync(
config.shapeOptions,
{
seenTxids
}
);
const awaitTxId = async (txId, timeout = 3e4) => {
debug(`awaitTxId called with txid %d`, txId);
if (typeof txId !== `number`) {
throw new TypeError(
`Expected number in awaitTxId, received ${typeof txId}`
);
}
const hasTxid = seenTxids.state.has(txId);
if (hasTxid) return true;
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
unsubscribe();
reject(new Error(`Timeout waiting for txId: ${txId}`));
}, timeout);
const unsubscribe = seenTxids.subscribe(() => {
if (seenTxids.state.has(txId)) {
debug(`awaitTxId found match for txid %o`, txId);
clearTimeout(timeoutId);
unsubscribe();
resolve(true);
}
});
});
};
const wrappedOnInsert = config.onInsert ? async (params) => {
const handlerResult = await config.onInsert(params) ?? {};
const txid = handlerResult.txid;
if (!txid) {
throw new Error(
`Electric collection onInsert handler must return a txid or array of txids`
);
}
if (Array.isArray(txid)) {
await Promise.all(txid.map((id) => awaitTxId(id)));
} else {
await awaitTxId(txid);
}
return handlerResult;
} : void 0;
const wrappedOnUpdate = config.onUpdate ? async (params) => {
const handlerResult = await config.onUpdate(params) ?? {};
const txid = handlerResult.txid;
if (!txid) {
throw new Error(
`Electric collection onUpdate handler must return a txid or array of txids`
);
}
if (Array.isArray(txid)) {
await Promise.all(txid.map((id) => awaitTxId(id)));
} else {
await awaitTxId(txid);
}
return handlerResult;
} : void 0;
const wrappedOnDelete = config.onDelete ? async (params) => {
const handlerResult = await config.onDelete(params);
if (!handlerResult.txid) {
throw new Error(
`Electric collection onDelete handler must return a txid or array of txids`
);
}
if (Array.isArray(handlerResult.txid)) {
await Promise.all(handlerResult.txid.map((id) => awaitTxId(id)));
} else {
await awaitTxId(handlerResult.txid);
}
return handlerResult;
} : void 0;
const {
shapeOptions: _shapeOptions,
onInsert: _onInsert,
onUpdate: _onUpdate,
onDelete: _onDelete,
...restConfig
} = config;
return {
...restConfig,
sync,
onInsert: wrappedOnInsert,
onUpdate: wrappedOnUpdate,
onDelete: wrappedOnDelete,
utils: {
awaitTxId
}
};
}
function createElectricSync(shapeOptions, options) {
const { seenTxids } = options;
const relationSchema = new Store(void 0);
const getSyncMetadata = () => {
var _a;
const schema = relationSchema.state || `public`;
return {
relation: ((_a = shapeOptions.params) == null ? void 0 : _a.table) ? [schema, shapeOptions.params.table] : void 0
};
};
const abortController = new AbortController();
if (shapeOptions.signal) {
shapeOptions.signal.addEventListener(`abort`, () => {
abortController.abort();
});
if (shapeOptions.signal.aborted) {
abortController.abort();
}
}
let unsubscribeStream;
return {
sync: (params) => {
const { begin, write, commit } = params;
const stream = new ShapeStream({
...shapeOptions,
signal: abortController.signal
});
let transactionStarted = false;
const newTxids = /* @__PURE__ */ new Set();
unsubscribeStream = stream.subscribe((messages) => {
var _a;
let hasUpToDate = false;
for (const message of messages) {
if (hasTxids(message)) {
(_a = message.headers.txids) == null ? void 0 : _a.forEach((txid) => newTxids.add(txid));
}
if (isChangeMessage(message)) {
const schema = message.headers.schema;
if (schema && typeof schema === `string`) {
relationSchema.setState(() => schema);
}
if (!transactionStarted) {
begin();
transactionStarted = true;
}
write({
type: message.headers.operation,
value: message.value,
// Include the primary key and relation info in the metadata
metadata: {
...message.headers
}
});
} else if (isUpToDateMessage(message)) {
hasUpToDate = true;
}
}
if (hasUpToDate) {
if (transactionStarted) {
commit();
transactionStarted = false;
} else {
begin();
commit();
}
seenTxids.setState((currentTxids) => {
const clonedSeen = new Set(currentTxids);
if (newTxids.size > 0) {
debug(`new txids synced from pg %O`, Array.from(newTxids));
}
newTxids.forEach((txid) => clonedSeen.add(txid));
newTxids.clear();
return clonedSeen;
});
}
});
return () => {
unsubscribeStream();
abortController.abort();
};
},
// Expose the getSyncMetadata function
getSyncMetadata
};
}
export {
electricCollectionOptions
};
//# sourceMappingURL=electric.js.map