mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-11-04 19:27:50 +08:00
Compare commits
2 Commits
9d9dcade7b
...
4e71fbffc3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e71fbffc3 | ||
| a3479865c8 |
94
src/lib/Queue.ts
Normal file
94
src/lib/Queue.ts
Normal 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
170
src/lib/Semaphore.ts
Normal 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
82
src/lib/SortedArray.ts
Normal 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 = [];
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
import { testTimeBasedRotation } from "./testCcLog";
|
||||
import { testTimeBasedRotation } from "./testCCLog";
|
||||
import { testSortedArray } from "./testSortedArray";
|
||||
import { testSemaphore } from "./testSemaphore";
|
||||
|
||||
testTimeBasedRotation();
|
||||
testSortedArray();
|
||||
testSemaphore();
|
||||
|
||||
@@ -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
199
src/test/testSemaphore.ts
Normal 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!");
|
||||
}
|
||||
62
src/test/testSortedArray.ts
Normal file
62
src/test/testSortedArray.ts
Normal 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!");
|
||||
}
|
||||
Reference in New Issue
Block a user