Backporting a bunch of optimizations from db-only branch
This commit is contained in:
parent
509ece91f0
commit
bf1eb03129
@ -5,16 +5,19 @@ import { LimitedMap } from "./limited_map.ts";
|
||||
Deno.test("limited map", async () => {
|
||||
const mp = new LimitedMap<string>(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());
|
||||
});
|
||||
|
@ -1,20 +1,36 @@
|
||||
type LimitedMapRecord<V> = Record<string, { value: V; la: number }>;
|
||||
type LimitedMapRecord<V> = { value: V; la: number };
|
||||
|
||||
export class LimitedMap<V> {
|
||||
constructor(private maxSize: number, private map: LimitedMapRecord<V> = {}) {
|
||||
private map: Map<string, LimitedMapRecord<V>>;
|
||||
|
||||
constructor(
|
||||
private maxSize: number,
|
||||
initialJson: Record<string, LimitedMapRecord<V>> = {},
|
||||
) {
|
||||
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<V> {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
});
|
||||
|
@ -67,3 +67,25 @@ export class PromiseQueue {
|
||||
this.run(); // Continue processing the next promise in the queue
|
||||
}
|
||||
}
|
||||
|
||||
export async function batchRequests<I, O>(
|
||||
values: I[],
|
||||
fn: (batch: I[]) => Promise<O[]>,
|
||||
batchSize: number,
|
||||
): Promise<O[]> {
|
||||
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;
|
||||
}
|
||||
|
@ -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<Query, "querySource"> & {
|
||||
|
@ -46,6 +46,7 @@ export class EventHook implements Hook<EventHookT> {
|
||||
throw new Error("Event hook is not initialized");
|
||||
}
|
||||
const responses: any[] = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
for (const plug of this.system.loadedPlugs.values()) {
|
||||
const manifest = plug.manifest;
|
||||
for (
|
||||
@ -60,16 +61,19 @@ export class EventHook implements Hook<EventHookT> {
|
||||
) {
|
||||
// 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<EventHookT> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -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<MQHookT> {
|
||||
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<MQHookT> {
|
||||
this.subscriptions = [];
|
||||
}
|
||||
|
||||
throttledReloadQueues = throttle(() => {
|
||||
this.reloadQueues();
|
||||
}, 1000);
|
||||
|
||||
reloadQueues() {
|
||||
this.stop();
|
||||
for (const plug of this.system.loadedPlugs.values()) {
|
||||
|
@ -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" });
|
||||
|
@ -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<any>(20);
|
||||
|
||||
constructor(
|
||||
readonly kv: KvPrimitives,
|
||||
private enableCache = false,
|
||||
private functionMap: FunctionMap = builtinFunctions,
|
||||
) {
|
||||
}
|
||||
@ -50,6 +54,21 @@ export class DataStore {
|
||||
}
|
||||
|
||||
async query<T = any>(query: KvQuery): Promise<KV<T>[]> {
|
||||
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<T>[] = [];
|
||||
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<void> {
|
||||
|
@ -23,6 +23,9 @@ export class DataStoreMQ implements MessageQueue {
|
||||
seq = 0;
|
||||
|
||||
async batchSend(queue: string, bodies: any[]): Promise<void> {
|
||||
if (bodies.length === 0) {
|
||||
return;
|
||||
}
|
||||
const messages: KV<MQMessage>[] = 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<ProcessingMessage>[] = [];
|
||||
for (const { value: m } of messages) {
|
||||
|
@ -18,7 +18,9 @@ export async function pageComplete(completeEvent: CompleteEvent) {
|
||||
completeEvent.linePrefix,
|
||||
);
|
||||
const tagToQuery = isInTemplateContext ? "template" : "page";
|
||||
let allPages: PageMeta[] = await queryObjects<PageMeta>(tagToQuery, {});
|
||||
let allPages: PageMeta[] = await queryObjects<PageMeta>(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
|
||||
|
@ -43,7 +43,10 @@ export async function anchorComplete(completeEvent: CompleteEvent) {
|
||||
// "bare" anchor, match any page for completion purposes
|
||||
filter = undefined;
|
||||
}
|
||||
const allAnchors = await queryObjects<AnchorObject>("anchor", { filter });
|
||||
const allAnchors = await queryObjects<AnchorObject>("anchor", {
|
||||
filter,
|
||||
cacheSecs: 5,
|
||||
});
|
||||
return {
|
||||
from: completeEvent.pos - match[1].length,
|
||||
options: allAnchors.map((a) => ({
|
||||
|
@ -64,7 +64,7 @@ export async function clearIndex(): Promise<void> {
|
||||
/**
|
||||
* Indexes entities in the data store
|
||||
*/
|
||||
export async function indexObjects<T>(
|
||||
export function indexObjects<T>(
|
||||
page: string,
|
||||
objects: ObjectValue<T>[],
|
||||
): Promise<void> {
|
||||
@ -127,14 +127,12 @@ export async function indexObjects<T>(
|
||||
}
|
||||
}
|
||||
if (allAttributes.size > 0) {
|
||||
await indexObjects<AttributeObject>(
|
||||
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<T>(
|
||||
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) {
|
||||
|
@ -42,23 +42,17 @@ export function determineType(v: any): string {
|
||||
export async function objectAttributeCompleter(
|
||||
attributeCompleteEvent: AttributeCompleteEvent,
|
||||
): Promise<AttributeCompletion[]> {
|
||||
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<AttributeObject>("attribute", {
|
||||
filter: attributeFilter,
|
||||
distinct: true,
|
||||
select: [{ name: "name" }, { name: "attributeType" }, { name: "tag" }, {
|
||||
name: "readOnly",
|
||||
}],
|
||||
cacheSecs: 5,
|
||||
});
|
||||
return allAttributes.map((value) => {
|
||||
return {
|
||||
|
@ -90,31 +90,28 @@ export const builtins: Record<string, Record<string, string>> = {
|
||||
|
||||
export async function loadBuiltinsIntoIndex() {
|
||||
console.log("Loading builtins attributes into index");
|
||||
const allTags: ObjectValue<TagObject>[] = [];
|
||||
const allObjects: ObjectValue<any>[] = [];
|
||||
for (const [tagName, attributes] of Object.entries(builtins)) {
|
||||
allTags.push({
|
||||
allObjects.push({
|
||||
ref: tagName,
|
||||
tag: "tag",
|
||||
name: tagName,
|
||||
page: builtinPseudoPage,
|
||||
parent: "builtin",
|
||||
});
|
||||
await indexObjects<AttributeObject>(
|
||||
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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -14,21 +14,24 @@ export async function lintYAML({ tree }: LintEvent): Promise<LintDiagnostic[]> {
|
||||
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<AttributeObject>("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<AttributeObject>(
|
||||
"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!,
|
||||
|
@ -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") {
|
||||
|
@ -91,7 +91,6 @@ export async function performQuery(parsedQuery: Query, pageObject: PageMeta) {
|
||||
export async function lintQuery(
|
||||
{ name, tree }: LintEvent,
|
||||
): Promise<LintDiagnostic[]> {
|
||||
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),
|
||||
);
|
||||
|
@ -9,7 +9,9 @@ export async function completeTaskState(completeEvent: CompleteEvent) {
|
||||
if (!taskMatch) {
|
||||
return null;
|
||||
}
|
||||
const allStates = await queryObjects<TaskStateObject>("taskstate", {});
|
||||
const allStates = await queryObjects<TaskStateObject>("taskstate", {
|
||||
cacheSecs: 5,
|
||||
});
|
||||
const states = [...new Set(allStates.map((s) => s.state))];
|
||||
|
||||
return {
|
||||
|
@ -56,6 +56,7 @@ export async function templateSlashComplete(
|
||||
"boolean",
|
||||
false,
|
||||
]]],
|
||||
cacheSecs: 5,
|
||||
});
|
||||
return allTemplates.map((template) => ({
|
||||
label: template.trigger!,
|
||||
|
@ -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";
|
||||
|
@ -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);
|
||||
|
@ -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<any>(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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user