Skip to content

Instantly share code, notes, and snippets.

@mendes5
Last active September 25, 2024 19:12
Show Gist options
  • Save mendes5/15cc092ff189a51880249e0c65e5c87a to your computer and use it in GitHub Desktop.
Save mendes5/15cc092ff189a51880249e0c65e5c87a to your computer and use it in GitHub Desktop.
Simple lock mechanism for cross system sincronization
// constants.js
const authLock = createFence();
// somewhere in auth.js
setInterval(() => {
authLock.lockFence(updateToken(refreshToken));
}, refreshTokenTime);
// Somewhre that wants to read the most up-to-date token:
const configureHeaders = async (headers) => {
// instant every time, unless theres a refresh token in progress
await authLock.fence();
headers['Authorization'] = getToken(); // get up to date token
}
const a = Promise.withResolvers();
const b = Promise.withResolvers();
const c = Promise.withResolvers();
// Fence creation
const x = createFence();
// Those are resolved immediately
x.waitFence().then(() => console.log('Noop 1'));
x.waitFence().then(() => console.log('Noop 2'));
x.waitFence().then(() => console.log('Noop 3'));
// Lock the fence
x.lockFence(a.promise);
// Those will block
x.waitFence().then(() => console.log('A 1'));
x.waitFence().then(() => console.log('A 2'));
x.waitFence().then(() => console.log('A 3'));
// Chain a lock
x.lockFence(b.promise);
// Those also lock
x.waitFence().then(() => console.log('B 1'));
x.waitFence().then(() => console.log('B 2'));
x.waitFence().then(() => console.log('B 3'));
// Chain one more for good measure
x.lockFence(c.promise);
// Those lock too
x.waitFence().then(() => console.log('C 1'));
x.waitFence().then(() => console.log('C 2'));
x.waitFence().then(() => console.log('C 3'));
// The order in which the locks are added or removed doesn't matter
// it only matters when they all are resolved
// so its like a Promise.all but the array can grow over time
// Unlock C (nothing happens, A and B are still pending)
c.resolve();
// Unlock A (nothing happens, B is still pending)
a.resolve();
// Unlock B
b.resolve();
// Logs:
// A 1
// A 2
// A 3
// B 1
// B 2
// B 3
// C 1
// C 2
// C 3
// The fence can still be used indefinitely
type Fence = {
waitFence: () => Promise<void>;
lockFence: (promise: Promise<unknown>) => void;
};
export const createFence = (): Fence => {
const fenceId = Math.random().toString(16).slice(2);
const logFence = (...msg: unknown[]) => console.log(`fence:${fenceId}`, ...msg);
logFence('Created');
const lockQueue = new Set<Promise<unknown>>();
interface PromiseWithResolvers<T> {
promise: Promise<T>;
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: unknown) => void;
}
type Lock = {
current: PromiseWithResolvers<void> | null;
};
const mainLock: Lock = { current: null };
const waitFence = () => {
if (mainLock.current?.promise) {
logFence('Wait called, and is locked, waiting');
mainLock.current?.promise.then(() => {
logFence('A wait lock was released');
});
return mainLock.current?.promise;
}
logFence('Wait called, but is unlocked, executing');
return Promise.resolve();
};
const lockFence = (promise: Promise<unknown>) => {
if (!(promise instanceof Promise)) {
logFence('Tried to lock a fence without a promise, skipping');
return;
}
if (promise === mainLock.current?.promise) {
console.warn('Tried to lock a fence with the current held lock, this would create a deadlock.');
}
for (const lock of lockQueue.values()) {
if (promise === lock) {
console.warn('Tried to lock a fence with a lock already in the queue, this would create a deadlock.');
return;
}
}
if (!mainLock.current?.promise) {
logFence('Created new lock');
mainLock.current = Promise.withResolvers();
}
lockQueue.add(promise);
logFence('Updated lock queue (1 new lock)', lockQueue);
promise.finally(() => {
lockQueue.delete(promise);
logFence('Updated lock queue (1 lock removed)', lockQueue);
if (lockQueue.size === 0) {
logFence(`Lock queue empty, releasing all locks`);
mainLock.current?.resolve();
mainLock.current = null;
} else {
logFence(`One of the promises was resolved but theres still ${lockQueue.size} locks pending.`);
}
});
};
return {
waitFence,
lockFence,
};
};
export const enterFenceScope = async (fence: Fence, scope: () => Promise<void>) => {
await fence.waitFence();
const promise = scope();
fence.lockFence(promise);
return promise;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment