Skip to content

Commit

Permalink
Merge pull request #444 from fastrodev/store
Browse files Browse the repository at this point in the history
Store
  • Loading branch information
ynwd authored Sep 20, 2024
2 parents a7511a3 + 72805e0 commit 51ff24d
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 51 deletions.
63 changes: 45 additions & 18 deletions core/map/map.test.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,43 @@
import { assertEquals } from "../server/deps.ts";
import { Store } from "./map.ts";

Deno.test("Store: set and get value without expiry", async () => {
Deno.test("Store: set and get value without expiry", () => {
const store = new Store<string, number>();
store.set("key1", 100);
const value = await store.get("key1");
const value = store.get("key1");
assertEquals(value, 100);
});

Deno.test("Store: set and get value with expiry", async () => {
const store = new Store<string, number>();
store.set("key2", 200, 1000); // Set with 1 second expiry
const value = await store.get("key2");
const value = store.get("key2");
assertEquals(value, 200);

// Wait for 1.1 seconds to let it expire
await new Promise((resolve) => setTimeout(resolve, 1100));
const expiredValue = await store.get("key2");
const expiredValue = store.get("key2");
assertEquals(expiredValue, undefined);
});

Deno.test("Store: has method returns true for existing key", async () => {
Deno.test("Store: has method returns true for existing key", () => {
const store = new Store<string, number>();
store.set("key3", 300);
assertEquals(await store.has("key3"), true);
assertEquals(store.has("key3"), true);
});

Deno.test("Store: has method returns false for expired key", async () => {
const store = new Store<string, number>();
store.set("key4", 400, 500); // Set with 0.5 seconds expiry
await new Promise((resolve) => setTimeout(resolve, 600)); // Wait for it to expire
assertEquals(await store.has("key4"), false);
assertEquals(store.has("key4"), false);
});

Deno.test("Store: delete method removes key", async () => {
Deno.test("Store: delete method removes key", () => {
const store = new Store<string, number>();
store.set("key5", 500);
assertEquals(store.delete("key5"), true);
assertEquals(await store.has("key5"), false);
assertEquals(store.has("key5"), false);
});

Deno.test("Store: clear method removes all keys", () => {
Expand All @@ -55,46 +55,73 @@ Deno.test("Store: size method returns correct count", () => {
assertEquals(store.size(), 2);
});

Deno.test("Store: commit without options is error", async () => {
try {
const store = new Store<string, number>();
await store.set("key6", 600).commit();
} catch (error) {
assertEquals(error, new Error("Options are needed to commit"));
}
});

Deno.test("Store: set with check", () => {
try {
const store = new Store<string, number>();
store.set("key6", 600);
store.check("key6").set("key6", 700);
} catch (error) {
assertEquals(error, new Error("Key key6 is already used"));
}
});

const d = new Date();
const time = d.getTime();
const token = Deno.env.get("GITHUB_TOKEN");
Deno.test("Store: save it to github", async () => {
const expiringMap = new Store<string, number>({
const store = new Store<string, number>({
owner: "fastrodev",
repo: "fastro",
path: "modules/store/records.json",
branch: "store",
token,
});
expiringMap.set("key1", time);
const r = await expiringMap.commit();
const intervalId = await store.sync(4000);
store.set("key1", time);
const r = await store.commit();
assertEquals(r?.data.content?.name, "records.json");
await new Promise((resolve) => setTimeout(resolve, 4000));
if (intervalId) clearInterval(intervalId);
});

Deno.test("Store: get value from github", async () => {
if (!token) return;
const expiringMap = new Store<string, number>({
const store = new Store<string, number>({
owner: "fastrodev",
repo: "fastro",
path: "modules/store/records.json",
branch: "store",
token,
});
// get kv from github
const g = await expiringMap.get("key1");
const intervalId = await store.sync(4000);
const g = store.get("key1");
assertEquals(g, time);
await new Promise((resolve) => setTimeout(resolve, 6000));
if (intervalId) clearInterval(intervalId);
});

Deno.test("Store: destroy map", async () => {
if (!token) return;
const expiringMap = new Store<string, number>({
const store = new Store<string, number>({
owner: "fastrodev",
repo: "fastro",
path: "modules/store/records.json",
branch: "store",
token,
});
await expiringMap.destroy();
const g = await expiringMap.get("key1");
const intervalId = await store.sync(4000);
await store.destroy();
const g = store.get("key1");
assertEquals(g, undefined);
await new Promise((resolve) => setTimeout(resolve, 4000));
if (intervalId) clearInterval(intervalId);
});
70 changes: 46 additions & 24 deletions core/map/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,29 @@ export class Store<K extends string | number | symbol, V> {
* @param value
* @param ttl
*/
set(key: K, value: V, ttl?: number): void {
set(key: K, value: V, ttl?: number) {
const expiry = ttl ? Date.now() + ttl : undefined;
this.map.set(key, { value, expiry });
return this;
}

/**
* @param key
* @returns boolean indicating whether an element with the specified key exists or not.
*/
check(key: K) {
if (this.map.has(key)) {
throw new Error(`Key ${String(key)} is already used`);
}

return this;
}

/**
* Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.
* @returns - Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.
*/
async get(key: K): Promise<V | undefined> {
await this.refresh();
get(key: K) {
const entry = this.map.get(key);
if (entry) {
if (entry.expiry === undefined || Date.now() < entry.expiry) {
Expand All @@ -52,8 +64,8 @@ export class Store<K extends string | number | symbol, V> {
* @param key
* @returns boolean indicating whether an element with the specified key exists or not.
*/
async has(key: K): Promise<boolean> {
await this.refresh();
has(key: K) {
// await this.refresh();
const entry = this.map.get(key);
if (entry) {
if (entry.expiry === undefined || Date.now() < entry.expiry) {
Expand Down Expand Up @@ -127,7 +139,7 @@ export class Store<K extends string | number | symbol, V> {
if (this.isCommitting) {
throw new Error("Commit in progress, please wait.");
}
if (!this.options) throw new Error("Options is needed.");
if (!this.options) throw new Error("Options are needed to commit");
this.isCommitting = true;
this.cleanUpExpiredEntries();
try {
Expand All @@ -145,9 +157,12 @@ export class Store<K extends string | number | symbol, V> {
}
}

/**
* Delete file from repository
*/
async destroy() {
this.map.clear();
if (!this.options) throw new Error("Options is needed.");
if (!this.options) throw new Error("Options are needed to destroy.");
return await deleteGithubFile({
token: this.options.token,
owner: this.options.owner,
Expand All @@ -157,25 +172,30 @@ export class Store<K extends string | number | symbol, V> {
});
}

startAutoSave(interval: number) {
if (this.saveIntervalId) {
clearInterval(this.saveIntervalId);
/**
* Sync with github repository
* @param interval
* @returns
*/
async sync(interval: number) {
if (await this.syncWithRepo()) {
if (this.saveIntervalId) clearInterval(this.saveIntervalId);
this.saveIntervalId = setInterval(async () => {
await this.syncWithRepo();
if (!this.options || this.map.size === 0) return;
await this.saveToGitHub({
token: this.options.token,
owner: this.options.owner,
repo: this.options.repo,
path: this.options.path,
branch: this.options.branch,
});
}, interval);
}

this.saveIntervalId = setInterval(async () => {
await this.refresh();
if (!this.options || this.map.size === 0) return;
return this.saveToGitHub({
token: this.options.token,
owner: this.options.owner,
repo: this.options.repo,
path: this.options.path,
branch: this.options.branch,
});
}, interval);
return this.saveIntervalId;
}

private async refresh() {
private async syncWithRepo() {
if (this.map.size === 0 && this.options) {
const map = await getMap<K, V>({
token: this.options.token,
Expand All @@ -184,9 +204,11 @@ export class Store<K extends string | number | symbol, V> {
path: this.options.path,
branch: this.options.branch,
});
if (!map) return;
if (!map) return false;
this.map = map;
}

return true;
}

private async saveToGitHub(options: StoreOptions) {
Expand Down
2 changes: 1 addition & 1 deletion core/server/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ if (root) fetchProps(root);
serve = async (options?: { port?: number; onListen?: ListenHandler }) => {
const [s] = await this.#build();
if (s) return Deno.exit();
this.store.startAutoSave(60000);
this.store.sync(30000);

this.#server = Deno.serve({
port: options && options.port ? options.port : 8000,
Expand Down
4 changes: 2 additions & 2 deletions docs/store.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ f.post(

f.get(
"/",
async (_req: HttpRequest, ctx: Context) => {
(_req: HttpRequest, ctx: Context) => {
// get the value
const res = await ctx.store.get("hello");
const res = ctx.store.get("hello");
return Response.json({ value: res });
},
);
Expand Down
4 changes: 2 additions & 2 deletions examples/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ f.post(
// get the value
f.get(
"/",
async (_req: HttpRequest, ctx: Context) => {
const res = await ctx.store.get("hello");
(_req: HttpRequest, ctx: Context) => {
const res = ctx.store.get("hello");
return Response.json({ value: res });
},
);
Expand Down
8 changes: 4 additions & 4 deletions post/store.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ store.set("key1", "hello");
store.set("key2", "hello2", 1000);

// get value
const r1 = await store.get("key1");
const r1 = store.get("key1");
console.log(r1);
const r2 = await store.get("key2");
const r2 = store.get("key2");
console.log(r2);

// clear the map
Expand Down Expand Up @@ -116,9 +116,9 @@ f.post(

f.get(
"/",
async (_req: HttpRequest, ctx: Context) => {
(_req: HttpRequest, ctx: Context) => {
// get the value
const res = await ctx.store.get("hello");
const res = ctx.store.get("hello");
return Response.json({ value: res });
},
);
Expand Down

2 comments on commit 51ff24d

@deno-deploy
Copy link

@deno-deploy deno-deploy bot commented on 51ff24d Sep 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failed to deploy:

UNCAUGHT_EXCEPTION

Error: GITHUB_TOKEN is needed
    at getMap (file:///src/core/map/map.ts:311:43)
    at Store.syncWithRepo (file:///src/core/map/map.ts:200:31)
    at Store.sync (file:///src/core/map/map.ts:181:24)
    at Server.serve (file:///src/core/server/mod.ts:633:16)

@deno-deploy
Copy link

@deno-deploy deno-deploy bot commented on 51ff24d Sep 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failed to deploy:

UNCAUGHT_EXCEPTION

Error: GITHUB_TOKEN is needed
    at getMap (file:///src/core/map/map.ts:311:43)
    at Store.syncWithRepo (file:///src/core/map/map.ts:200:31)
    at Store.sync (file:///src/core/map/map.ts:181:24)
    at Server.serve (file:///src/core/server/mod.ts:633:16)

Please sign in to comment.