This commit is contained in:
JOLIMAITRE Matthieu 2024-04-29 10:36:03 +02:00
commit 5a40f83a74
11 changed files with 598 additions and 0 deletions

101
src/lib/storage.ts Normal file
View file

@ -0,0 +1,101 @@
import { assert } from "https://deno.land/std@0.223.0/assert/assert.ts";
import { z } from "https://deno.land/x/zod@v3.23.4/mod.ts";
import { last, log_from } from "./utils.ts";
const log = log_from(import.meta);
export class Storage {
db;
devoirs;
notifications;
constructor(db: Deno.Kv) {
this.db = db;
const devoir_parser = z.object({
subject: z.string(),
date: z.number(),
description: z.string(),
});
this.devoirs = new Manager(db, "devoir", devoir_parser);
const notification_parser = z.object({
devoir_id: z.string(),
message_id: z.string(),
});
this.notifications = new Manager(db, "notification", notification_parser);
}
static async open(path: string) {
const kv = await Deno.openKv(path);
const result = new Storage(kv);
await result.sanity();
return result;
}
async sanity() {
await this.devoirs.sanity();
}
}
class Manager<T> {
db;
label;
parser;
constructor(db: Deno.Kv, label: string, parser: z.ZodType<T>) {
this.db = db;
this.label = label;
this.parser = parser;
}
async add(value: T) {
const id = `${Date.now()}${Math.random()}`;
await this.db.set([this.label, id], value);
return id;
}
async get(id: string) {
const entry = await this.db.get([this.label, id]);
if (entry.value === null) return null;
const parsed = this.parse(entry.value);
if (parsed === null) log(`Could not parse`, this.label, id, entry);
return parsed;
}
parse(value: unknown) {
try {
return this.parser.parse(value);
} catch (_error) {
return null;
}
}
async set(id: string, value: T) {
await this.db.set([this.label, id], value);
}
async update(id: string, operation: (item: T) => unknown) {
const value = await this.get(id);
if (value === null) return;
await operation(value);
await this.set(id, value);
}
async delete(id: string) {
await this.db.delete([this.label, id]);
}
async *list() {
for await (const entry of this.db.list({ prefix: [this.label] })) {
const id = last(entry.key);
assert(typeof id === "string");
const value = this.parse(entry.value);
if (value === null) continue;
yield [id, value as T] as const;
}
}
async sanity() {
for await (const _ of this.list()) _;
}
}

49
src/lib/utils.ts Normal file
View file

@ -0,0 +1,49 @@
import { basename } from "https://deno.land/std@0.223.0/path/basename.ts";
export function log_from(meta: ImportMeta) {
const url = new URL(meta.url);
const file = basename(url.pathname);
return (...args: unknown[]) => console.log(`[${file}]`, ...args);
}
export function last<T>(items: Iterable<T>) {
let result = null as T | null;
for (const item of items) result = item;
return result;
}
export function days_to_ms(days: number) {
const ms_per_day = 24 * 60 * 60 * 1000;
return days * ms_per_day;
}
export async function collect<T>(iterator: AsyncIterable<T>) {
const collected = [] as T[];
for await (const item of iterator) collected.push(item);
return collected;
}
export type Channel<T = void> = ReturnType<typeof channel<T>>;
export function channel<T = void>() {
const queue = [] as T[];
let resolve_next = null as null | ((value: T) => void);
async function receive() {
if (resolve_next !== null) throw new Error("Receiving twice concurently.");
if (queue.length > 0) return queue.splice(0, 1)[0];
const result = await new Promise<T>((resolver) => resolve_next = resolver);
resolve_next = null;
return result;
}
function send(item: T) {
if (resolve_next === null) queue.push(item);
else resolve_next(item);
}
return { send, receive };
}
export async function wait(ms: number) {
await new Promise((resolver) => setTimeout(resolver, ms));
}