export function hasWebLocks() {
return (
typeof navigator !== "undefined" &&
navigator.locks !== undefined &&
typeof navigator.locks.request === "function"
);
}
const syncStates = new Map();
function getSyncState(dbId) {
let state = syncStates.get(dbId);
if (!state) {
state = {
inProgress: false,
result: null,
error: null,
waiters: [],
releaseLock: null,
syncGeneration: 0,
};
syncStates.set(dbId, state);
}
return state;
}
export async function acquireSyncLock(dbId, timeoutMs = 0) {
const state = getSyncState(dbId);
if (state.inProgress) {
return new Promise((resolve, reject) => {
let timeoutId;
if (timeoutMs > 0) {
timeoutId = setTimeout(() => {
const idx = state.waiters.findIndex((w) => w.resolve === onResult);
if (idx !== -1) {
state.waiters.splice(idx, 1);
}
reject(new Error("Sync lock acquisition timed out"));
}, timeoutMs);
}
const onResult = (result) => {
if (timeoutId) clearTimeout(timeoutId);
resolve({ acquired: false, coalescedResult: result });
};
const onError = (error) => {
if (timeoutId) clearTimeout(timeoutId);
reject(error);
};
state.waiters.push({ resolve: onResult, reject: onError });
});
}
state.inProgress = true;
state.result = null;
state.error = null;
state.syncGeneration++;
const currentGeneration = state.syncGeneration;
if (hasWebLocks()) {
const lockName = `miden-sync-${dbId}`;
return new Promise((resolve, reject) => {
let timeoutId;
let timedOut = false;
if (timeoutMs > 0) {
timeoutId = setTimeout(() => {
timedOut = true;
if (state.syncGeneration === currentGeneration) {
state.inProgress = false;
const error = new Error("Sync lock acquisition timed out");
for (const waiter of state.waiters) {
waiter.reject(error);
}
state.waiters = [];
}
reject(new Error("Sync lock acquisition timed out"));
}, timeoutMs);
}
navigator.locks
.request(lockName, { mode: "exclusive" }, async () => {
if (timedOut || state.syncGeneration !== currentGeneration) {
return;
}
if (timeoutId) clearTimeout(timeoutId);
return new Promise((releaseLock) => {
state.releaseLock = releaseLock;
resolve({ acquired: true });
});
})
.catch((err) => {
if (timeoutId) clearTimeout(timeoutId);
if (state.syncGeneration === currentGeneration) {
state.inProgress = false;
}
reject(err instanceof Error ? err : new Error(String(err)));
});
});
} else {
return { acquired: true };
}
}
export function releaseSyncLock(dbId, result) {
const state = getSyncState(dbId);
if (!state.inProgress) {
console.warn("releaseSyncLock called but no sync was in progress");
return;
}
state.result = result;
state.inProgress = false;
for (const waiter of state.waiters) {
waiter.resolve(result);
}
state.waiters = [];
if (state.releaseLock) {
state.releaseLock();
state.releaseLock = null;
}
}
export function releaseSyncLockWithError(dbId, error) {
const state = getSyncState(dbId);
if (!state.inProgress) {
console.warn("releaseSyncLockWithError called but no sync was in progress");
return;
}
state.error = error;
state.inProgress = false;
for (const waiter of state.waiters) {
waiter.reject(error);
}
state.waiters = [];
if (state.releaseLock) {
state.releaseLock();
state.releaseLock = null;
}
}