@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
JavaScript
/**
* @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