From bf1eb0312965c3e6d2b718f44175dc73ec84b279 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Sat, 13 Jan 2024 17:30:15 +0100 Subject: [PATCH] Backporting a bunch of optimizations from db-only branch --- common/limited_map.test.ts | 13 ++++++---- common/limited_map.ts | 45 +++++++++++++++++++++------------ plug-api/lib/async.test.ts | 20 +++++++++++++-- plug-api/lib/async.ts | 22 ++++++++++++++++ plug-api/types.ts | 5 ++++ plugos/hooks/event.ts | 36 ++++++++++++++++---------- plugos/hooks/mq.ts | 11 +++++--- plugos/lib/datastore.test.ts | 2 +- plugos/lib/datastore.ts | 26 ++++++++++++++++++- plugos/lib/mq.datastore.ts | 12 +++++++++ plugs/editor/complete.ts | 4 ++- plugs/index/anchor.ts | 5 +++- plugs/index/api.ts | 28 ++++++++++---------- plugs/index/attributes.ts | 12 +++------ plugs/index/builtins.ts | 33 +++++++++++------------- plugs/index/index.plug.yaml | 3 +-- plugs/index/lint.ts | 29 +++++++++++---------- plugs/index/tags.ts | 1 + plugs/query/query.ts | 2 +- plugs/tasks/complete.ts | 4 ++- plugs/template/complete.ts | 1 + server/deps.ts | 13 ++++++++++ web/client.ts | 2 +- web/syscalls/datastore.proxy.ts | 39 +++++++++++++++++++++++++--- 24 files changed, 264 insertions(+), 104 deletions(-) diff --git a/common/limited_map.test.ts b/common/limited_map.test.ts index 2014d35..e70f1ba 100644 --- a/common/limited_map.test.ts +++ b/common/limited_map.test.ts @@ -5,16 +5,19 @@ import { LimitedMap } from "./limited_map.ts"; Deno.test("limited map", async () => { const mp = new LimitedMap(3); mp.set("a", "a"); - mp.set("b", "b"); + mp.set("b", "b", 5); mp.set("c", "c"); - await sleep(2); assertEquals(mp.get("a"), "a"); - await sleep(2); assertEquals(mp.get("b"), "b"); - await sleep(2); assertEquals(mp.get("c"), "c"); // Drops the first key mp.set("d", "d"); - await sleep(2); + // console.log(mp.toJSON()); assertEquals(mp.get("a"), undefined); + await sleep(10); + // "b" should have been dropped + assertEquals(mp.get("b"), undefined); + assertEquals(mp.get("c"), "c"); + + console.log(mp.toJSON()); }); diff --git a/common/limited_map.ts b/common/limited_map.ts index 4561cef..3ba2259 100644 --- a/common/limited_map.ts +++ b/common/limited_map.ts @@ -1,20 +1,36 @@ -type LimitedMapRecord = Record; +type LimitedMapRecord = { value: V; la: number }; export class LimitedMap { - constructor(private maxSize: number, private map: LimitedMapRecord = {}) { + private map: Map>; + + constructor( + private maxSize: number, + initialJson: Record> = {}, + ) { + this.map = new Map(Object.entries(initialJson)); } - set(key: string, value: V) { - if (Object.keys(this.map).length >= this.maxSize) { + /** + * @param key + * @param value + * @param ttl time to live (in ms) + */ + set(key: string, value: V, ttl?: number) { + if (ttl) { + setTimeout(() => { + this.map.delete(key); + }, ttl); + } + if (this.map.size >= this.maxSize) { // Remove the oldest key before adding a new one const oldestKey = this.getOldestKey(); - delete this.map[oldestKey!]; + this.map.delete(oldestKey!); } - this.map[key] = { value, la: Date.now() }; + this.map.set(key, { value, la: Date.now() }); } get(key: string): V | undefined { - const entry = this.map[key]; + const entry = this.map.get(key); if (entry) { // Update the last accessed timestamp entry.la = Date.now(); @@ -24,24 +40,21 @@ export class LimitedMap { } remove(key: string) { - delete this.map[key]; + this.map.delete(key); } toJSON() { - return this.map; + return Object.fromEntries(this.map.entries()); } private getOldestKey(): string | undefined { let oldestKey: string | undefined; let oldestTimestamp: number | undefined; - for (const key in this.map) { - if (Object.prototype.hasOwnProperty.call(this.map, key)) { - const entry = this.map[key]; - if (!oldestTimestamp || entry.la < oldestTimestamp) { - oldestKey = key; - oldestTimestamp = entry.la; - } + for (const [key, entry] of this.map.entries()) { + if (!oldestTimestamp || entry.la < oldestTimestamp) { + oldestKey = key; + oldestTimestamp = entry.la; } } diff --git a/plug-api/lib/async.test.ts b/plug-api/lib/async.test.ts index 5ccdf5a..b7e9288 100644 --- a/plug-api/lib/async.test.ts +++ b/plug-api/lib/async.test.ts @@ -1,5 +1,5 @@ -import { assertEquals } from "../../test_deps.ts"; -import { PromiseQueue, sleep } from "./async.ts"; +import { assert, assertEquals } from "../../test_deps.ts"; +import { batchRequests, PromiseQueue, sleep } from "./async.ts"; Deno.test("PromiseQueue test", async () => { const q = new PromiseQueue(); @@ -24,3 +24,19 @@ Deno.test("PromiseQueue test", async () => { }); assertEquals(wasRun, true); }); + +Deno.test("Batch test", async () => { + // Generate an array with numbers up to 100 + const elements = Array.from(Array(100).keys()); + const multiplied = await batchRequests(elements, async (batch) => { + await sleep(2); + // Batches should be 9 or smaller (last batch will be smaller) + assert(batch.length <= 9); + return batch.map((e) => e * 2); + }, 9); + assertEquals(multiplied, elements.map((e) => e * 2)); + const multiplied2 = await batchRequests(elements, async (batch) => { + return batch.map((e) => e * 2); + }, 10000); + assertEquals(multiplied2, elements.map((e) => e * 2)); +}); diff --git a/plug-api/lib/async.ts b/plug-api/lib/async.ts index f0939b5..e8b5ab4 100644 --- a/plug-api/lib/async.ts +++ b/plug-api/lib/async.ts @@ -67,3 +67,25 @@ export class PromiseQueue { this.run(); // Continue processing the next promise in the queue } } + +export async function batchRequests( + values: I[], + fn: (batch: I[]) => Promise, + batchSize: number, +): Promise { + const results: O[] = []; + // Split values into batches of batchSize + const batches: I[][] = []; + for (let i = 0; i < values.length; i += batchSize) { + batches.push(values.slice(i, i + batchSize)); + } + // Run fn on them in parallel + const batchResults = await Promise.all(batches.map(fn)); + // Flatten the results + for (const batchResult of batchResults) { + if (Array.isArray(batchResult)) { // If fn returns an array, collect them + results.push(...batchResult); + } + } + return results; +} diff --git a/plug-api/types.ts b/plug-api/types.ts index 0786cde..3d6e815 100644 --- a/plug-api/types.ts +++ b/plug-api/types.ts @@ -73,6 +73,11 @@ export type Query = { render?: string; renderAll?: boolean; distinct?: boolean; + + /** + * When set, the DS implementation _may_ cache the result for the given number of seconds. + */ + cacheSecs?: number; }; export type KvQuery = Omit & { diff --git a/plugos/hooks/event.ts b/plugos/hooks/event.ts index 529072d..4bfcf8e 100644 --- a/plugos/hooks/event.ts +++ b/plugos/hooks/event.ts @@ -46,6 +46,7 @@ export class EventHook implements Hook { throw new Error("Event hook is not initialized"); } const responses: any[] = []; + const promises: Promise[] = []; for (const plug of this.system.loadedPlugs.values()) { const manifest = plug.manifest; for ( @@ -60,16 +61,19 @@ export class EventHook implements Hook { ) { // Only dispatch functions that can run in this environment if (await plug.canInvoke(name)) { - try { - const result = await plug.invoke(name, args); - if (result !== undefined) { - responses.push(result); + // Queue the promise + promises.push((async () => { + try { + const result = await plug.invoke(name, args); + if (result !== undefined) { + responses.push(result); + } + } catch (e: any) { + console.error( + `Error dispatching event ${eventName} to plug ${plug.name}: ${e.message}`, + ); } - } catch (e: any) { - console.error( - `Error dispatching event ${eventName} to plug ${plug.name}: ${e.message}`, - ); - } + })()); } } } @@ -79,13 +83,19 @@ export class EventHook implements Hook { const localListeners = this.localListeners.get(eventName); if (localListeners) { for (const localListener of localListeners) { - const result = await Promise.resolve(localListener(...args)); - if (result) { - responses.push(result); - } + // Queue the promise + promises.push((async () => { + const result = await Promise.resolve(localListener(...args)); + if (result) { + responses.push(result); + } + })()); } } + // Wait for all promises to resolve + await Promise.all(promises); + return responses; } diff --git a/plugos/hooks/mq.ts b/plugos/hooks/mq.ts index 08d8309..02564a0 100644 --- a/plugos/hooks/mq.ts +++ b/plugos/hooks/mq.ts @@ -3,6 +3,7 @@ import { System } from "../system.ts"; import { fullQueueName } from "../lib/mq_util.ts"; import { MQMessage } from "$sb/types.ts"; import { MessageQueue } from "../lib/mq.ts"; +import { throttle } from "$sb/lib/async.ts"; type MQSubscription = { queue: string; @@ -24,14 +25,14 @@ export class MQHook implements Hook { this.system = system; system.on({ plugLoaded: () => { - this.reloadQueues(); + this.throttledReloadQueues(); }, plugUnloaded: () => { - this.reloadQueues(); + this.throttledReloadQueues(); }, }); - this.reloadQueues(); + this.throttledReloadQueues(); } stop() { @@ -40,6 +41,10 @@ export class MQHook implements Hook { this.subscriptions = []; } + throttledReloadQueues = throttle(() => { + this.reloadQueues(); + }, 1000); + reloadQueues() { this.stop(); for (const plug of this.system.loadedPlugs.values()) { diff --git a/plugos/lib/datastore.test.ts b/plugos/lib/datastore.test.ts index 15ca0e7..934c95d 100644 --- a/plugos/lib/datastore.test.ts +++ b/plugos/lib/datastore.test.ts @@ -7,7 +7,7 @@ import { assertEquals } from "https://deno.land/std@0.165.0/testing/asserts.ts"; import { PrefixedKvPrimitives } from "./prefixed_kv_primitives.ts"; async function test(db: KvPrimitives) { - const datastore = new DataStore(new PrefixedKvPrimitives(db, ["ds"]), { + const datastore = new DataStore(new PrefixedKvPrimitives(db, ["ds"]), false, { count: (arr: any[]) => arr.length, }); await datastore.set(["user", "peter"], { name: "Peter" }); diff --git a/plugos/lib/datastore.ts b/plugos/lib/datastore.ts index 882e60d..4af3b6e 100644 --- a/plugos/lib/datastore.ts +++ b/plugos/lib/datastore.ts @@ -2,14 +2,18 @@ import { applyQueryNoFilterKV, evalQueryExpression } from "$sb/lib/query.ts"; import { FunctionMap, KV, KvKey, KvQuery } from "$sb/types.ts"; import { builtinFunctions } from "$sb/lib/builtin_query_functions.ts"; import { KvPrimitives } from "./kv_primitives.ts"; +import { LimitedMap } from "../../common/limited_map.ts"; /** * This is the data store class you'll actually want to use, wrapping the primitives * in a more user-friendly way */ export class DataStore { + private cache = new LimitedMap(20); + constructor( readonly kv: KvPrimitives, + private enableCache = false, private functionMap: FunctionMap = builtinFunctions, ) { } @@ -50,6 +54,21 @@ export class DataStore { } async query(query: KvQuery): Promise[]> { + let cacheKey: string | undefined; + const cacheSecs = query.cacheSecs; + // Should we do caching? + if (cacheSecs && this.enableCache) { + // Remove the cacheSecs from the query + query = { ...query, cacheSecs: undefined }; + console.log("Going to cache query", query); + cacheKey = JSON.stringify(query); + const cachedResult = this.cache.get(cacheKey); + if (cachedResult) { + // Let's use the cached result + return cachedResult; + } + } + const results: KV[] = []; let itemCount = 0; // Accumulate results @@ -76,7 +95,12 @@ export class DataStore { } } // Apply order by, limit, and select - return applyQueryNoFilterKV(query, results, this.functionMap); + const finalResult = applyQueryNoFilterKV(query, results, this.functionMap); + if (cacheKey) { + // Store in the cache + this.cache.set(cacheKey, finalResult, cacheSecs! * 1000); + } + return finalResult; } async queryDelete(query: KvQuery): Promise { diff --git a/plugos/lib/mq.datastore.ts b/plugos/lib/mq.datastore.ts index 225bb7f..53cdf0a 100644 --- a/plugos/lib/mq.datastore.ts +++ b/plugos/lib/mq.datastore.ts @@ -23,6 +23,9 @@ export class DataStoreMQ implements MessageQueue { seq = 0; async batchSend(queue: string, bodies: any[]): Promise { + if (bodies.length === 0) { + return; + } const messages: KV[] = bodies.map((body) => { const id = `${Date.now()}-${String(++this.seq).padStart(6, "0")}`; const key = [...queuedPrefix, queue, id]; @@ -54,6 +57,9 @@ export class DataStoreMQ implements MessageQueue { prefix: [...queuedPrefix, queue], limit: ["number", maxItems], }); + if (messages.length === 0) { + return []; + } // Put them in the processing queue await this.ds.batchSet( messages.map((m) => ({ @@ -137,6 +143,9 @@ export class DataStoreMQ implements MessageQueue { } async batchAck(queue: string, ids: string[]) { + if (ids.length === 0) { + return; + } await this.ds.batchDelete( ids.map((id) => [...processingPrefix, queue, id]), ); @@ -152,6 +161,9 @@ export class DataStoreMQ implements MessageQueue { prefix: processingPrefix, filter: ["<", ["attr", "ts"], ["number", now - timeout]], }); + if (messages.length === 0) { + return; + } await this.ds.batchDelete(messages.map((m) => m.key)); const newMessages: KV[] = []; for (const { value: m } of messages) { diff --git a/plugs/editor/complete.ts b/plugs/editor/complete.ts index 41f8c94..5fd0e03 100644 --- a/plugs/editor/complete.ts +++ b/plugs/editor/complete.ts @@ -18,7 +18,9 @@ export async function pageComplete(completeEvent: CompleteEvent) { completeEvent.linePrefix, ); const tagToQuery = isInTemplateContext ? "template" : "page"; - let allPages: PageMeta[] = await queryObjects(tagToQuery, {}); + let allPages: PageMeta[] = await queryObjects(tagToQuery, { + cacheSecs: 5, + }); const prefix = match[1]; if (prefix.startsWith("!")) { // Federation prefix, let's first see if we're matching anything from federation that is locally synced diff --git a/plugs/index/anchor.ts b/plugs/index/anchor.ts index c28ae07..d70909e 100644 --- a/plugs/index/anchor.ts +++ b/plugs/index/anchor.ts @@ -43,7 +43,10 @@ export async function anchorComplete(completeEvent: CompleteEvent) { // "bare" anchor, match any page for completion purposes filter = undefined; } - const allAnchors = await queryObjects("anchor", { filter }); + const allAnchors = await queryObjects("anchor", { + filter, + cacheSecs: 5, + }); return { from: completeEvent.pos - match[1].length, options: allAnchors.map((a) => ({ diff --git a/plugs/index/api.ts b/plugs/index/api.ts index 59dc80a..af8441d 100644 --- a/plugs/index/api.ts +++ b/plugs/index/api.ts @@ -64,7 +64,7 @@ export async function clearIndex(): Promise { /** * Indexes entities in the data store */ -export async function indexObjects( +export function indexObjects( page: string, objects: ObjectValue[], ): Promise { @@ -127,14 +127,12 @@ export async function indexObjects( } } if (allAttributes.size > 0) { - await indexObjects( - page, - [...allAttributes].map(([key, value]) => { - const [tagName, name] = key.split(":"); - const attributeType = value.startsWith("!") - ? value.substring(1) - : value; - return { + [...allAttributes].forEach(([key, value]) => { + const [tagName, name] = key.split(":"); + const attributeType = value.startsWith("!") ? value.substring(1) : value; + kvs.push({ + key: ["attribute", cleanKey(key, page)], + value: { ref: key, tag: "attribute", tagName, @@ -142,11 +140,15 @@ export async function indexObjects( attributeType, readOnly: value.startsWith("!"), page, - }; - }), - ); + } as T, + }); + }); + } + if (kvs.length > 0) { + return batchSet(page, kvs); + } else { + return Promise.resolve(); } - return batchSet(page, kvs); } function cleanKey(ref: string, page: string) { diff --git a/plugs/index/attributes.ts b/plugs/index/attributes.ts index bb59f93..5313bec 100644 --- a/plugs/index/attributes.ts +++ b/plugs/index/attributes.ts @@ -42,23 +42,17 @@ export function determineType(v: any): string { export async function objectAttributeCompleter( attributeCompleteEvent: AttributeCompleteEvent, ): Promise { - const prefixFilter: QueryExpression = ["call", "startsWith", [[ - "attr", - "name", - ], ["string", attributeCompleteEvent.prefix]]]; const attributeFilter: QueryExpression | undefined = attributeCompleteEvent.source === "" - ? prefixFilter - : ["and", prefixFilter, ["=", ["attr", "tagName"], [ - "string", - attributeCompleteEvent.source, - ]]]; + ? undefined + : ["=", ["attr", "tagName"], ["string", attributeCompleteEvent.source]]; const allAttributes = await queryObjects("attribute", { filter: attributeFilter, distinct: true, select: [{ name: "name" }, { name: "attributeType" }, { name: "tag" }, { name: "readOnly", }], + cacheSecs: 5, }); return allAttributes.map((value) => { return { diff --git a/plugs/index/builtins.ts b/plugs/index/builtins.ts index b7f0f9a..0674079 100644 --- a/plugs/index/builtins.ts +++ b/plugs/index/builtins.ts @@ -90,31 +90,28 @@ export const builtins: Record> = { export async function loadBuiltinsIntoIndex() { console.log("Loading builtins attributes into index"); - const allTags: ObjectValue[] = []; + const allObjects: ObjectValue[] = []; for (const [tagName, attributes] of Object.entries(builtins)) { - allTags.push({ + allObjects.push({ ref: tagName, tag: "tag", name: tagName, page: builtinPseudoPage, parent: "builtin", }); - await indexObjects( - builtinPseudoPage, - Object.entries(attributes).map(([name, attributeType]) => { - return { - ref: `${tagName}:${name}`, - tag: "attribute", - tagName, - name, - attributeType: attributeType.startsWith("!") - ? attributeType.substring(1) - : attributeType, - readOnly: attributeType.startsWith("!"), - page: builtinPseudoPage, - }; - }), + allObjects.push( + ...Object.entries(attributes).map(([name, attributeType]) => ({ + ref: `${tagName}:${name}`, + tag: "attribute", + tagName, + name, + attributeType: attributeType.startsWith("!") + ? attributeType.substring(1) + : attributeType, + readOnly: attributeType.startsWith("!"), + page: builtinPseudoPage, + })), ); } - await indexObjects(builtinPseudoPage, allTags); + await indexObjects(builtinPseudoPage, allObjects); } diff --git a/plugs/index/index.plug.yaml b/plugs/index/index.plug.yaml index d1d8c50..3eb52c2 100644 --- a/plugs/index/index.plug.yaml +++ b/plugs/index/index.plug.yaml @@ -23,13 +23,12 @@ functions: env: server query: path: api.ts:query - env: server indexObjects: path: api.ts:indexObjects env: server queryObjects: path: api.ts:queryObjects - env: server + # Note: not setting env: server to allow for client-side datastore query caching getObjectByRef: path: api.ts:getObjectByRef env: server diff --git a/plugs/index/lint.ts b/plugs/index/lint.ts index 82179f9..751848f 100644 --- a/plugs/index/lint.ts +++ b/plugs/index/lint.ts @@ -14,21 +14,24 @@ export async function lintYAML({ tree }: LintEvent): Promise { const diagnostics: LintDiagnostic[] = []; const frontmatter = await extractFrontmatter(tree); const tags = ["page", ...frontmatter.tags || []]; - // Query all readOnly attributes for pages with this tag set - const readOnlyAttributes = await queryObjects("attribute", { - filter: ["and", ["=", ["attr", "tagName"], [ - "array", - tags.map((tag): QueryExpression => ["string", tag]), - ]], [ - "=", - ["attr", "readOnly"], - ["boolean", true], - ]], - distinct: true, - select: [{ name: "name" }], - }); await traverseTreeAsync(tree, async (node) => { if (node.type === "FrontMatterCode") { + // Query all readOnly attributes for pages with this tag set + const readOnlyAttributes = await queryObjects( + "attribute", + { + filter: ["and", ["=", ["attr", "tagName"], [ + "array", + tags.map((tag): QueryExpression => ["string", tag]), + ]], [ + "=", + ["attr", "readOnly"], + ["boolean", true], + ]], + distinct: true, + select: [{ name: "name" }], + }, + ); const lintResult = await lintYaml( renderToText(node), node.from!, diff --git a/plugs/index/tags.ts b/plugs/index/tags.ts index 37e3fa3..5062f7b 100644 --- a/plugs/index/tags.ts +++ b/plugs/index/tags.ts @@ -73,6 +73,7 @@ export async function tagComplete(completeEvent: CompleteEvent) { filter: ["=", ["attr", "parent"], ["string", parent]], select: [{ name: "name" }], distinct: true, + cacheSecs: 5, }); if (parent === "page") { diff --git a/plugs/query/query.ts b/plugs/query/query.ts index b8bc6f5..db9e056 100644 --- a/plugs/query/query.ts +++ b/plugs/query/query.ts @@ -91,7 +91,6 @@ export async function performQuery(parsedQuery: Query, pageObject: PageMeta) { export async function lintQuery( { name, tree }: LintEvent, ): Promise { - const pageObject = await loadPageObject(name); const diagnostics: LintDiagnostic[] = []; await traverseTreeAsync(tree, async (node) => { if (node.type === "FencedCode") { @@ -111,6 +110,7 @@ export async function lintQuery( } const bodyText = codeText.children![0].text!; try { + const pageObject = await loadPageObject(name); const parsedQuery = await parseQuery( await replaceTemplateVars(bodyText, pageObject), ); diff --git a/plugs/tasks/complete.ts b/plugs/tasks/complete.ts index abf2dd2..fd47d8f 100644 --- a/plugs/tasks/complete.ts +++ b/plugs/tasks/complete.ts @@ -9,7 +9,9 @@ export async function completeTaskState(completeEvent: CompleteEvent) { if (!taskMatch) { return null; } - const allStates = await queryObjects("taskstate", {}); + const allStates = await queryObjects("taskstate", { + cacheSecs: 5, + }); const states = [...new Set(allStates.map((s) => s.state))]; return { diff --git a/plugs/template/complete.ts b/plugs/template/complete.ts index 64c6963..571795d 100644 --- a/plugs/template/complete.ts +++ b/plugs/template/complete.ts @@ -56,6 +56,7 @@ export async function templateSlashComplete( "boolean", false, ]]], + cacheSecs: 5, }); return allTemplates.map((template) => ({ label: template.trigger!, diff --git a/server/deps.ts b/server/deps.ts index b6feb82..62fa8d2 100644 --- a/server/deps.ts +++ b/server/deps.ts @@ -9,3 +9,16 @@ export { } from "https://deno.land/x/oak@v12.4.0/mod.ts"; export * as etag from "https://deno.land/x/oak@v12.4.0/etag.ts"; export { oakCors } from "https://deno.land/x/cors@v1.2.2/mod.ts"; + +export { Hono } from "https://deno.land/x/hono@v3.12.2/mod.ts"; +export { + deleteCookie, + getCookie, + setCookie, +} from "https://deno.land/x/hono@v3.12.2/helper.ts"; +export { cors } from "https://deno.land/x/hono@v3.12.2/middleware.ts"; + +export type { + HonoRequest, + // Next, +} from "https://deno.land/x/hono@v3.12.2/mod.ts"; diff --git a/web/client.ts b/web/client.ts index d7d37e3..318175b 100644 --- a/web/client.ts +++ b/web/client.ts @@ -135,7 +135,7 @@ export class Client { `${this.dbPrefix}_state`, ); await stateKvPrimitives.init(); - this.stateDataStore = new DataStore(stateKvPrimitives); + this.stateDataStore = new DataStore(stateKvPrimitives, true); // Setup message queue this.mq = new DataStoreMQ(this.stateDataStore); diff --git a/web/syscalls/datastore.proxy.ts b/web/syscalls/datastore.proxy.ts index 0e65f6c..65bd0e9 100644 --- a/web/syscalls/datastore.proxy.ts +++ b/web/syscalls/datastore.proxy.ts @@ -1,15 +1,48 @@ +import { KvQuery } from "$sb/types.ts"; +import { LimitedMap } from "../../common/limited_map.ts"; import type { SysCallMapping } from "../../plugos/system.ts"; import type { Client } from "../client.ts"; -import { proxySyscalls } from "./util.ts"; +import { proxySyscall, proxySyscalls } from "./util.ts"; export function dataStoreProxySyscalls(client: Client): SysCallMapping { - return proxySyscalls(client, [ + const syscalls = proxySyscalls(client, [ "datastore.delete", "datastore.set", "datastore.batchSet", "datastore.batchDelete", "datastore.batchGet", "datastore.get", - "datastore.query", ]); + // Add a cache for datastore.query + const queryCache = new LimitedMap(5); + syscalls["datastore.query"] = async (ctx, query: KvQuery) => { + let cacheKey: string | undefined; + const cacheSecs = query.cacheSecs; + // Should we do caching? + if (cacheSecs) { + // Remove the cacheSecs from the query + query = { ...query, cacheSecs: undefined }; + cacheKey = JSON.stringify(query); + const cachedResult = queryCache.get(cacheKey); + if (cachedResult) { + // Let's use the cached result + return cachedResult; + } + } + + const result = await proxySyscall( + ctx, + client.httpSpacePrimitives, + "datastore.query", + [ + query, + ], + ); + if (cacheKey) { + // Store in the cache + queryCache.set(cacheKey, result, cacheSecs! * 1000); + } + return result; + }; + return syscalls; }