Last active
September 25, 2024 19:12
-
-
Save mendes5/15cc092ff189a51880249e0c65e5c87a to your computer and use it in GitHub Desktop.
Simple lock mechanism for cross system sincronization
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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