Compare commits

...

2 Commits

Author SHA1 Message Date
SikongJueluo
4e71fbffc3 feature: add sortedarray semaphore 2025-10-17 21:41:23 +08:00
a3479865c8 feature: add data type queue 2025-10-17 16:40:44 +08:00
7 changed files with 615 additions and 13 deletions

94
src/lib/Queue.ts Normal file
View File

@@ -0,0 +1,94 @@
export class Node<T> {
public value: T;
public next?: Node<T>
public prev?: Node<T>
constructor(value: T, next?: Node<T>, prev?: Node<T>) {
this.value = value;
this.next = next;
this.prev = prev;
}
}
export class Queue<T> {
private _head?: Node<T>;
private _tail?: Node<T>;
private _size: number;
constructor() {
this._head = undefined;
this._tail = undefined;
this._size = 0;
}
public enqueue(data: T): void {
const node = new Node(data);
if (this._head === undefined) {
this._head = node;
this._tail = node;
} else {
this._tail!.next = node;
node.prev = this._tail;
this._tail = node;
}
this._size++;
}
public dequeue(): T | undefined {
const node = this._head;
if (node === undefined) return undefined;
this._head = this._head!.next;
this._head!.prev = undefined;
this._size--;
return node.value;
}
public clear(): void {
this._head = undefined;
this._tail = undefined;
this._size = 0;
}
public peek(): T | undefined {
if (this._head === undefined) return undefined;
return this._head.value;
}
public size(): number {
return this._size;
}
public toArray(): T[] | undefined {
if (this._size === 0) return undefined;
const array: T[] = [];
let currentNode: Node<T> = this._head!;
for (let i = 0; i < this._size; i++) {
if (currentNode.value !== undefined)
array.push(currentNode.value);
currentNode = currentNode.next!;
}
return array;
}
[Symbol.iterator](): Iterator<T> {
let currentNode = this._head;
return {
next(): IteratorResult<T> {
if (currentNode === undefined) {
return { value: undefined, done: true }
} else {
const data = currentNode.value;
currentNode = currentNode.next;
return { value: data, done: false }
}
}
}
}
}

170
src/lib/Semaphore.ts Normal file
View File

@@ -0,0 +1,170 @@
import { SortedArray } from "./SortedArray";
const E_CANCELED = new Error("Request canceled");
// const E_INSUFFICIENT_RESOURCES = new Error("Insufficient resources");
interface QueueEntry {
resolve(result: [number, () => void]): void;
reject(error: unknown): void;
weight: number;
}
interface Waiter {
resolve(): void;
}
export class Semaphore {
private _value: number;
private _cancelError: Error;
private _queue = new SortedArray<QueueEntry>();
private _waiters = new SortedArray<Waiter>();
constructor(value: number, cancelError: Error = E_CANCELED) {
if (value < 0) {
throw new Error("Semaphore value must be non-negative");
}
this._value = value;
this._cancelError = cancelError;
}
acquire(weight = 1, priority = 0): Promise<[number, () => void]> {
if (weight <= 0) {
throw new Error(`invalid weight ${weight}: must be positive`);
}
return new Promise((resolve, reject) => {
const entry: QueueEntry = { resolve, reject, weight };
if (this._queue.toArray().length === 0 && weight <= this._value) {
this._dispatchItem(entry);
} else {
this._queue.push({ priority, data: entry });
}
});
}
tryAcquire(weight = 1): (() => void) | null {
if (weight <= 0) {
throw new Error(`invalid weight ${weight}: must be positive`);
}
if (weight > this._value || this._queue.toArray().length > 0) {
return null;
}
this._value -= weight;
return this._newReleaser(weight);
}
async runExclusive<T>(
callback: (value: number) => T | Promise<T>,
weight = 1,
priority = 0,
): Promise<T> {
const [value, release] = await this.acquire(weight, priority);
try {
return await callback(value);
} finally {
release();
}
}
waitForUnlock(weight = 1, priority = 0): Promise<void> {
if (weight <= 0) {
throw new Error(`invalid weight ${weight}: must be positive`);
}
if (this._couldLockImmediately(weight)) {
return Promise.resolve();
}
return new Promise<void>((resolve) => {
const waiter: Waiter = { resolve };
this._waiters.push({ priority, data: waiter });
});
}
isLocked(): boolean {
return this._value <= 0;
}
getValue(): number {
return this._value;
}
setValue(value: number): void {
if (value < 0) {
throw new Error("Semaphore value must be non-negative");
}
this._value = value;
this._dispatchQueue();
}
release(weight = 1): void {
if (weight <= 0) {
throw new Error(`invalid weight ${weight}: must be positive`);
}
this._value += weight;
this._dispatchQueue();
}
cancel(): void {
const queueItems = this._queue.toArray();
queueItems.forEach((entry) => entry.reject(this._cancelError));
this._queue.clear();
const waiters = this._waiters.toArray();
waiters.forEach((waiter) => waiter.resolve());
this._waiters.clear();
}
private _dispatchQueue(): void {
this._drainWaiters();
let next = this._peek();
while (next && next.weight <= this._value) {
const item = this._queue.shift();
if (item) {
this._dispatchItem(item);
}
this._drainWaiters();
next = this._peek();
}
}
private _dispatchItem(item: QueueEntry): void {
const previousValue = this._value;
this._value -= item.weight;
item.resolve([previousValue, this._newReleaser(item.weight)]);
}
private _peek(): QueueEntry | undefined {
const array = this._queue.toArray();
return array.length > 0 ? array[0] : undefined;
}
private _newReleaser(weight: number): () => void {
let called = false;
return () => {
if (called) return;
called = true;
this.release(weight);
};
}
private _drainWaiters(): void {
const waiters = this._waiters.toArray();
if (waiters.length === 0) return;
// If no queue or resources available, resolve all waiters
const hasQueue = this._queue.toArray().length > 0;
if (!hasQueue || this._value > 0) {
waiters.forEach((waiter) => waiter.resolve());
this._waiters.clear();
}
}
private _couldLockImmediately(weight: number): boolean {
return this._queue.toArray().length === 0 && weight <= this._value;
}
}

82
src/lib/SortedArray.ts Normal file
View File

@@ -0,0 +1,82 @@
interface Priority<T> {
priority: number;
data: T;
}
export class SortedArray<T> {
private _data: Priority<T>[];
constructor(data?: Priority<T>[]) {
this._data = data ?? [];
}
private findIndex(priority: number): number {
const target = priority + 1;
let left = 0;
let right = this._data.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (this._data[mid].priority < target) {
left = mid + 1;
} else if (this._data[mid].priority > target) {
right = mid - 1;
} else {
right = mid - 1;
}
}
return left - 1;
}
public push(value: Priority<T>): void {
if (this._data.length === 0) {
this._data.push(value);
return;
} else if (this._data.length === 1) {
if (this._data[0].priority <= value.priority) this._data.push(value);
else this._data = [value, ...this._data];
return;
}
const index = this.findIndex(value.priority);
if (index === this._data.length - 1) {
if (this._data[index].priority <= value.priority) {
this._data.push(value);
} else {
this._data = [
...this._data.slice(0, index),
value,
...this._data.slice(index),
];
}
return;
}
const endIndex = index + 1;
this._data = [
...this._data.slice(0, endIndex),
value,
...this._data.slice(endIndex),
];
}
public shift(): T | undefined {
const value = this._data.shift();
return value?.data;
}
public pop(): T | undefined {
const value = this._data.pop();
return value?.data;
}
public toArray(): T[] {
return this._data.map(({ data }) => data);
}
public clear() {
this._data = [];
}
}

View File

@@ -1,3 +1,7 @@
import { testTimeBasedRotation } from "./testCcLog";
import { testTimeBasedRotation } from "./testCCLog";
import { testSortedArray } from "./testSortedArray";
import { testSemaphore } from "./testSemaphore";
testTimeBasedRotation();
testSortedArray();
testSemaphore();

View File

@@ -1,4 +1,4 @@
import { CCLog, MINUTE, HOUR, SECOND } from "@/lib/ccLog";
import { CCLog, MINUTE, HOUR } from "@/lib/ccLog";
// Test the new time-based rotation functionality
function testTimeBasedRotation() {
@@ -9,25 +9,16 @@ function testTimeBasedRotation() {
logger1.info("This is a test message with default interval (1 day)");
// Test with custom interval (1 hour)
const logger2 = new CCLog("test_log_hourly.txt", HOUR);
const logger2 = new CCLog("test_log_hourly.txt", { logInterval: HOUR });
logger2.info("This is a test message with 1-hour interval");
// Test with custom interval (30 minutes)
const logger3 = new CCLog("test_log_30min.txt", 30 * MINUTE);
const logger3 = new CCLog("test_log_30min.txt", { logInterval: 30 * MINUTE });
logger3.info("This is a test message with 30-minute interval");
// Test with custom interval (5 seconds)
const logger4 = new CCLog("test_log_5sec.txt", 5 * SECOND);
logger4.info("This is a test message with 5-second interval");
for (let i = 0; i < 10; i++) {
logger4.info(`This is a test message with 5-second interval ${i}`);
sleep(1);
}
logger1.close();
logger2.close();
logger3.close();
logger4.close();
print("Test completed successfully!");
}

199
src/test/testSemaphore.ts Normal file
View File

@@ -0,0 +1,199 @@
import { Semaphore } from "@/lib/Semaphore";
function assert(condition: boolean, message: string) {
if (!condition) {
error(message);
}
}
async function testBasicAcquireRelease() {
print(" Running test: testBasicAcquireRelease");
const s = new Semaphore(1);
assert(s.getValue() === 1, "Initial value should be 1");
const [, release] = await s.acquire();
assert(s.getValue() === 0, "Value after acquire should be 0");
release();
assert(s.getValue() === 1, "Value after release should be 1");
print(" Test passed: testBasicAcquireRelease");
}
async function testRunExclusive() {
print(" Running test: testRunExclusive");
const s = new Semaphore(1);
let inside = false;
await s.runExclusive(() => {
inside = true;
assert(s.isLocked(), "Should be locked inside runExclusive");
assert(s.getValue() === 0, "Value should be 0 inside runExclusive");
});
assert(inside, "Callback should have been executed");
assert(!s.isLocked(), "Should be unlocked after runExclusive");
assert(s.getValue() === 1, "Value should be 1 after runExclusive");
print(" Test passed: testRunExclusive");
}
function testTryAcquire() {
print(" Running test: testTryAcquire");
const s = new Semaphore(1);
const release1 = s.tryAcquire();
assert(release1 !== null, "tryAcquire should succeed");
assert(s.getValue() === 0, "Value should be 0 after tryAcquire");
const release2 = s.tryAcquire();
assert(release2 === null, "tryAcquire should fail when locked");
release1!();
assert(s.getValue() === 1, "Value should be 1 after release");
const release3 = s.tryAcquire();
assert(release3 !== null, "tryAcquire should succeed again");
release3!();
print(" Test passed: testTryAcquire");
}
async function testQueueing() {
print(" Running test: testQueueing");
const s = new Semaphore(1);
const events: string[] = [];
// Take the lock
const [, release1] = await s.acquire();
events.push("acquired1");
// These two will be queued
await s.acquire().then(([, release]) => {
events.push("acquired2");
sleep(0.1);
release();
events.push("released2");
});
await s.acquire().then(([, release]) => {
events.push("acquired3");
release();
events.push("released3");
});
// Give some time for promises to queue
sleep(0.1);
assert(events.length === 1, "Only first acquire should have completed");
// Release the first lock, allowing the queue to proceed
release1();
events.push("released1");
// Wait for all promises to finish
sleep(0.5);
const expected = [
"acquired1",
"released1",
"acquired2",
"released2",
"acquired3",
"released3",
];
assert(
textutils.serialiseJSON(events) === textutils.serialiseJSON(expected),
`Event order incorrect: got ${textutils.serialiseJSON(events)}`,
);
print(" Test passed: testQueueing");
}
async function testPriority() {
print(" Running test: testPriority");
const s = new Semaphore(1);
const events: string[] = [];
const [, release1] = await s.acquire();
events.push("acquired_main");
// Queue with low priority
await s.acquire(1, 10).then(([, release]) => {
events.push("acquired_low_prio");
release();
});
// Queue with high priority
await s.acquire(1, 1).then(([, release]) => {
events.push("acquired_high_prio");
release();
});
sleep(0.1);
release1();
sleep(0.1);
const expected = ["acquired_main", "acquired_high_prio", "acquired_low_prio"];
assert(
textutils.serialiseJSON(events) === textutils.serialiseJSON(expected),
`Priority order incorrect: got ${textutils.serialiseJSON(events)}`,
);
print(" Test passed: testPriority");
}
async function testWaitForUnlock() {
print(" Running test: testWaitForUnlock");
const s = new Semaphore(1);
let waited = false;
const [, release] = await s.acquire();
assert(s.isLocked(), "Semaphore should be locked");
await s.waitForUnlock().then(() => {
waited = true;
assert(!s.isLocked(), "Should be unlocked when wait is over");
});
sleep(0.1);
assert(!waited, "waitForUnlock should not resolve yet");
release();
sleep(0.1);
assert(waited, "waitForUnlock should have resolved");
print(" Test passed: testWaitForUnlock");
}
async function testCancel() {
print(" Running test: testCancel");
const cancelError = new Error("Canceled for test");
const s = new Semaphore(1, cancelError);
let rejected = false;
const [, release] = await s.acquire();
s.acquire().then(
() => {
assert(false, "acquire should have been rejected");
},
(err) => {
assert(err === cancelError, "acquire rejected with wrong error");
rejected = true;
},
);
sleep(0.1);
s.cancel();
sleep(0.1);
assert(rejected, "pending acquire should have been rejected");
assert(s.getValue() === 0, "cancel should not affect current lock");
release();
assert(s.getValue() === 1, "release should still work");
print(" Test passed: testCancel");
}
export async function testSemaphore() {
print("Testing Semaphore...");
await testBasicAcquireRelease();
await testRunExclusive();
testTryAcquire();
await testQueueing();
await testPriority();
await testWaitForUnlock();
await testCancel();
print("Semaphore tests passed!");
}

View File

@@ -0,0 +1,62 @@
import { SortedArray } from "@/lib/SortedArray";
function assert(condition: boolean, message: string) {
if (!condition) {
error(message);
}
}
function assertDeepEquals(actual: object, expect: object, message: string) {
const jsonExpect = textutils.serialiseJSON(expect, {
allow_repetitions: true,
});
const jsonActual = textutils.serialiseJSON(actual, {
allow_repetitions: true,
});
if (jsonExpect !== jsonActual) {
error(`${message}: expected ${jsonExpect}, got ${jsonActual}`);
}
}
export function testSortedArray() {
print("Testing SortedArray...");
// Test constructor
const sortedArray = new SortedArray<string>();
assert(
sortedArray.toArray().length === 0,
"Constructor: initial length should be 0",
);
// Test push (FIFO)
const fifoArray = new SortedArray<string>([]);
fifoArray.push({ priority: 2, data: "b" });
fifoArray.push({ priority: 1, data: "a" });
fifoArray.push({ priority: 3, data: "c" });
fifoArray.push({ priority: 2, data: "b2" });
assertDeepEquals(fifoArray.toArray(), ["a", "b", "b2", "c"], "Push (FIFO)");
// Test shift
const shiftedValue = fifoArray.shift();
assert(shiftedValue === "a", "Shift: should return the first element");
assertDeepEquals(
fifoArray.toArray(),
["b", "b2", "c"],
"Shift: array should be modified",
);
// Test pop
const poppedValue = fifoArray.pop();
assert(poppedValue === "c", "Pop: should return the last element");
assertDeepEquals(
fifoArray.toArray(),
["b", "b2"],
"Pop: array should be modified",
);
// Test clear
fifoArray.clear();
assert(fifoArray.toArray().length === 0, "Clear: array should be empty");
print("SortedArray tests passed!");
}