UNPKG

@daiso-tech/core

Version:

The library offers flexible, framework-agnostic solutions for modern web applications, built on adaptable components that integrate seamlessly with popular frameworks like Next Js.

386 lines 14 kB
/** * @module SharedLock */ import {} from "../../../../execution-context/contracts/_module.js"; import {} from "../../../../shared-lock/contracts/_module.js"; import {} from "../../../../time-span/implementations/_module.js"; import { OPTION, optionNone, optionSome, UnexpectedError, } from "../../../../utilities/_module.js"; /** * Note the `MemorySharedLockAdapter` is limited to single process usage and cannot be shared across multiple servers or different processes. * This adapter is meant for easily faking{@link ISharedLockFactory | `ISharedLockFactory`} for testing. * * IMPORT_PATH: `"@daiso-tech/core/shared-lock/memory-shared-lock-adapter"` * @group Adapters */ export class MemorySharedLockAdapter { map; /** * @example * ```ts * import { MemorySharedLockAdapter } from "@daiso-tech/core/shared-lock/memory-shared-lock-adapter"; * * const sharedLockAdapter = new MemorySharedLockAdapter(); * ``` * You can also provide an `Map`. * @example * ```ts * import { MemorySharedLockAdapter } from "@daiso-tech/core/shared-lock/memory-shared-lock-adapter"; * * const map = new Map<any, any>(); * const sharedLockAdapter = new MemorySharedLockAdapter(map); * ``` */ constructor(map = new Map()) { this.map = map; } /** * Removes all in-memory shared-lock data. */ async deInit() { for (const [key, sharedLock] of this.map) { const writerLock = sharedLock.writerLock; if (writerLock !== null && writerLock.hasExpiration) { clearTimeout(writerLock.timeoutId); } const readerSemaphore = sharedLock.readerSemaphore; if (readerSemaphore !== null) { for (const [_, { timeoutId }] of readerSemaphore.slots) { if (timeoutId !== null) { clearTimeout(timeoutId); } } } this.map.delete(key); } return Promise.resolve(); } async acquireWriter(_context, key, lockId, ttl) { const sharedLock = this.map.get(key); const readerSemaphore = sharedLock?.readerSemaphore ?? null; if (readerSemaphore !== null) { return Promise.resolve(false); } let writerLock = sharedLock?.writerLock ?? null; if (writerLock !== null) { return Promise.resolve(writerLock.owner === lockId); } if (ttl === null) { writerLock = { owner: lockId, hasExpiration: false, }; this.map.set(key, { writerLock, readerSemaphore: null, }); } else { const timeoutId = setTimeout(() => { this.map.delete(key); }, ttl.toMilliseconds()); writerLock = { owner: lockId, hasExpiration: true, timeoutId, expiration: ttl.toEndDate(), }; this.map.set(key, { writerLock, readerSemaphore: null, }); } return Promise.resolve(true); } async releaseWriter(_context, key, lockId) { const sharedLock = this.map.get(key); const readerSemaphore = sharedLock?.readerSemaphore ?? null; if (readerSemaphore !== null) { return Promise.resolve(false); } const writerLock = sharedLock?.writerLock ?? null; if (writerLock === null) { return Promise.resolve(false); } if (writerLock.owner !== lockId) { return Promise.resolve(false); } // Check expiration: if expired, cannot release if (writerLock.hasExpiration && writerLock.expiration <= new Date()) { return Promise.resolve(false); } if (writerLock.hasExpiration) { clearTimeout(writerLock.timeoutId); } this.map.delete(key); return Promise.resolve(true); } async forceReleaseWriter(_context, key) { const sharedLock = this.map.get(key); const readerSemaphore = sharedLock?.readerSemaphore ?? null; if (readerSemaphore !== null) { return Promise.resolve(false); } const writerLock = sharedLock?.writerLock ?? null; if (writerLock === null) { return Promise.resolve(false); } // Check expiration: if expired, cannot force release if (writerLock.hasExpiration && writerLock.expiration <= new Date()) { return Promise.resolve(false); } if (writerLock.hasExpiration) { clearTimeout(writerLock.timeoutId); } this.map.delete(key); return Promise.resolve(true); } async refreshWriter(_context, key, lockId, ttl) { const sharedLock = this.map.get(key); const readerSemaphore = sharedLock?.readerSemaphore ?? null; if (readerSemaphore !== null) { return Promise.resolve(false); } const writerLock = sharedLock?.writerLock ?? null; if (writerLock === null) { return Promise.resolve(false); } if (writerLock.owner !== lockId) { return Promise.resolve(false); } // Check expiration: if expired, cannot refresh if (writerLock.hasExpiration && writerLock.expiration <= new Date()) { return Promise.resolve(false); } if (!writerLock.hasExpiration) { return Promise.resolve(false); } clearTimeout(writerLock.timeoutId); const timeoutId = setTimeout(() => { this.map.delete(key); }, ttl.toMilliseconds()); this.map.set(key, { readerSemaphore: null, writerLock: { ...writerLock, timeoutId, }, }); return Promise.resolve(true); } async acquireReader(settings) { const { key, lockId, limit, ttl } = settings; const sharedLock = this.map.get(key); const writerLock = sharedLock?.writerLock ?? null; if (writerLock !== null) { return Promise.resolve(false); } let readerSemaphore = sharedLock?.readerSemaphore ?? null; if (readerSemaphore === null) { readerSemaphore = { limit, slots: new Map(), }; this.map.set(key, { readerSemaphore, writerLock: null, }); } if (readerSemaphore.slots.size >= readerSemaphore.limit) { return Promise.resolve(false); } if (readerSemaphore.slots.has(lockId)) { return Promise.resolve(true); } if (ttl === null) { readerSemaphore.slots.set(lockId, { timeoutId: null, expiration: null, }); } else { const timeoutId = setTimeout(() => { readerSemaphore.slots.delete(lockId); }, ttl.toMilliseconds()); readerSemaphore.slots.set(lockId, { timeoutId, expiration: ttl.toEndDate(), }); } this.map.set(key, { readerSemaphore, writerLock: null, }); return Promise.resolve(true); } async releaseReader(_context, key, lockId) { const sharedLock = this.map.get(key); const writerLock = sharedLock?.writerLock ?? null; if (writerLock !== null) { return Promise.resolve(false); } const readerSemaphore = sharedLock?.readerSemaphore ?? null; if (readerSemaphore === null) { return Promise.resolve(false); } const slot = readerSemaphore.slots.get(lockId); if (slot === undefined) { return Promise.resolve(false); } // Check expiration: if expired, cannot release if (slot.expiration !== null && slot.expiration <= new Date()) { return Promise.resolve(false); } if (slot.timeoutId !== null) { clearTimeout(slot.timeoutId); } readerSemaphore.slots.delete(lockId); this.map.set(key, { readerSemaphore, writerLock: null, }); if (readerSemaphore.slots.size === 0) { this.map.delete(key); } return Promise.resolve(true); } async forceReleaseAllReaders(_context, key) { const sharedLock = this.map.get(key); const writerLock = sharedLock?.writerLock ?? null; if (writerLock !== null) { return Promise.resolve(false); } const readerSemaphore = sharedLock?.readerSemaphore ?? null; if (readerSemaphore === null) { return Promise.resolve(false); } const hasSlots = readerSemaphore.slots.size > 0; for (const [slotId, slot] of readerSemaphore.slots) { // Check expiration: if expired, skip force release if (slot.expiration !== null && slot.expiration <= new Date()) { continue; } clearTimeout(slot.timeoutId ?? undefined); readerSemaphore.slots.delete(slotId); } this.map.delete(key); return Promise.resolve(hasSlots); } async refreshReader(_context, key, lockId, ttl) { const sharedLock = this.map.get(key); const writerLock = sharedLock?.writerLock ?? null; if (writerLock !== null) { return Promise.resolve(false); } const readerSemaphore = sharedLock?.readerSemaphore ?? null; if (!readerSemaphore) { return Promise.resolve(false); } const slot = readerSemaphore.slots.get(lockId); if (slot === undefined) { return Promise.resolve(false); } // Check expiration: if expired, cannot refresh if (slot.expiration !== null && slot.expiration <= new Date()) { return Promise.resolve(false); } if (slot.timeoutId === null) { return Promise.resolve(false); } clearTimeout(slot.timeoutId); const timeoutId = setTimeout(() => { readerSemaphore.slots.delete(lockId); this.map.set(key, { readerSemaphore, writerLock: null, }); }, ttl.toMilliseconds()); readerSemaphore.slots.set(lockId, { timeoutId, expiration: ttl.toEndDate(), }); this.map.set(key, { readerSemaphore, writerLock: null, }); return Promise.resolve(true); } async forceRelease(context, key) { const hasReleasedAllReaders = await this.forceReleaseAllReaders(context, key); const hasReleasedWriter = await this.forceReleaseWriter(context, key); return hasReleasedAllReaders || hasReleasedWriter; } static extractReaderState(writerLock, readerSemaphore) { if (writerLock === null && readerSemaphore !== null && readerSemaphore.slots.size === 0) { return optionSome(null); } if (writerLock === null && readerSemaphore !== null && readerSemaphore.slots.size !== 0) { return optionSome({ writer: null, reader: { limit: readerSemaphore.limit, acquiredSlots: new Map([...readerSemaphore.slots.entries()].map(([key_, value]) => [key_, value.expiration])), }, }); } return optionNone(); } static extractWriterState_(writerLock, readerSemaphore) { if (readerSemaphore === null && writerLock !== null && !writerLock.hasExpiration) { return optionSome({ reader: null, writer: { owner: writerLock.owner, expiration: null, }, }); } if (readerSemaphore === null && writerLock !== null && writerLock.hasExpiration) { return optionSome({ reader: null, writer: { owner: writerLock.owner, expiration: writerLock.expiration, }, }); } return optionNone(); } static extractActiveWriterState(writerLock, readerSemaphore) { const activeWriterStateOption = MemorySharedLockAdapter.extractWriterState_(writerLock, readerSemaphore); if (activeWriterStateOption.type === OPTION.SOME) { return activeWriterStateOption; } if (readerSemaphore === null && writerLock !== null && writerLock.hasExpiration && writerLock.expiration <= new Date()) { return optionSome(null); } return optionNone(); } async getState(_context, key) { const sharedLock = this.map.get(key); if (sharedLock === undefined) { return Promise.resolve(null); } const { writerLock, readerSemaphore } = sharedLock; const writerState = MemorySharedLockAdapter.extractReaderState(writerLock, readerSemaphore); if (writerState.type === OPTION.SOME) { return writerState.value; } const readerState = MemorySharedLockAdapter.extractActiveWriterState(writerLock, readerSemaphore); if (readerState.type === OPTION.SOME) { return readerState.value; } throw new UnexpectedError("Invalid ISharedLockAdapterState, expected either the reader field must be defined or the writer field must be defined, but not both."); } } //# sourceMappingURL=memory-shared-lock-adapter.js.map