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 () => {
|
Deno.test("limited map", async () => {
|
||||||
const mp = new LimitedMap<string>(3);
|
const mp = new LimitedMap<string>(3);
|
||||||
mp.set("a", "a");
|
mp.set("a", "a");
|
||||||
mp.set("b", "b");
|
mp.set("b", "b", 5);
|
||||||
mp.set("c", "c");
|
mp.set("c", "c");
|
||||||
await sleep(2);
|
|
||||||
assertEquals(mp.get("a"), "a");
|
assertEquals(mp.get("a"), "a");
|
||||||
await sleep(2);
|
|
||||||
assertEquals(mp.get("b"), "b");
|
assertEquals(mp.get("b"), "b");
|
||||||
await sleep(2);
|
|
||||||
assertEquals(mp.get("c"), "c");
|
assertEquals(mp.get("c"), "c");
|
||||||
// Drops the first key
|
// Drops the first key
|
||||||
mp.set("d", "d");
|
mp.set("d", "d");
|
||||||
await sleep(2);
|
// console.log(mp.toJSON());
|
||||||
assertEquals(mp.get("a"), undefined);
|
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> {
|
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
|
// Remove the oldest key before adding a new one
|
||||||
const oldestKey = this.getOldestKey();
|
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 {
|
get(key: string): V | undefined {
|
||||||
const entry = this.map[key];
|
const entry = this.map.get(key);
|
||||||
if (entry) {
|
if (entry) {
|
||||||
// Update the last accessed timestamp
|
// Update the last accessed timestamp
|
||||||
entry.la = Date.now();
|
entry.la = Date.now();
|
||||||
@ -24,24 +40,21 @@ export class LimitedMap<V> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
remove(key: string) {
|
remove(key: string) {
|
||||||
delete this.map[key];
|
this.map.delete(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return this.map;
|
return Object.fromEntries(this.map.entries());
|
||||||
}
|
}
|
||||||
|
|
||||||
private getOldestKey(): string | undefined {
|
private getOldestKey(): string | undefined {
|
||||||
let oldestKey: string | undefined;
|
let oldestKey: string | undefined;
|
||||||
let oldestTimestamp: number | undefined;
|
let oldestTimestamp: number | undefined;
|
||||||
|
|
||||||
for (const key in this.map) {
|
for (const [key, entry] of this.map.entries()) {
|
||||||
if (Object.prototype.hasOwnProperty.call(this.map, key)) {
|
if (!oldestTimestamp || entry.la < oldestTimestamp) {
|
||||||
const entry = this.map[key];
|
oldestKey = key;
|
||||||
if (!oldestTimestamp || entry.la < oldestTimestamp) {
|
oldestTimestamp = entry.la;
|
||||||
oldestKey = key;
|
|
||||||
oldestTimestamp = entry.la;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { assertEquals } from "../../test_deps.ts";
|
import { assert, assertEquals } from "../../test_deps.ts";
|
||||||
import { PromiseQueue, sleep } from "./async.ts";
|
import { batchRequests, PromiseQueue, sleep } from "./async.ts";
|
||||||
|
|
||||||
Deno.test("PromiseQueue test", async () => {
|
Deno.test("PromiseQueue test", async () => {
|
||||||
const q = new PromiseQueue();
|
const q = new PromiseQueue();
|
||||||
@ -24,3 +24,19 @@ Deno.test("PromiseQueue test", async () => {
|
|||||||
});
|
});
|
||||||
assertEquals(wasRun, true);
|
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
|
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;
|
render?: string;
|
||||||
renderAll?: boolean;
|
renderAll?: boolean;
|
||||||
distinct?: 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"> & {
|
export type KvQuery = Omit<Query, "querySource"> & {
|
||||||
|
@ -46,6 +46,7 @@ export class EventHook implements Hook<EventHookT> {
|
|||||||
throw new Error("Event hook is not initialized");
|
throw new Error("Event hook is not initialized");
|
||||||
}
|
}
|
||||||
const responses: any[] = [];
|
const responses: any[] = [];
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
for (const plug of this.system.loadedPlugs.values()) {
|
for (const plug of this.system.loadedPlugs.values()) {
|
||||||
const manifest = plug.manifest;
|
const manifest = plug.manifest;
|
||||||
for (
|
for (
|
||||||
@ -60,16 +61,19 @@ export class EventHook implements Hook<EventHookT> {
|
|||||||
) {
|
) {
|
||||||
// Only dispatch functions that can run in this environment
|
// Only dispatch functions that can run in this environment
|
||||||
if (await plug.canInvoke(name)) {
|
if (await plug.canInvoke(name)) {
|
||||||
try {
|
// Queue the promise
|
||||||
const result = await plug.invoke(name, args);
|
promises.push((async () => {
|
||||||
if (result !== undefined) {
|
try {
|
||||||
responses.push(result);
|
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);
|
const localListeners = this.localListeners.get(eventName);
|
||||||
if (localListeners) {
|
if (localListeners) {
|
||||||
for (const localListener of localListeners) {
|
for (const localListener of localListeners) {
|
||||||
const result = await Promise.resolve(localListener(...args));
|
// Queue the promise
|
||||||
if (result) {
|
promises.push((async () => {
|
||||||
responses.push(result);
|
const result = await Promise.resolve(localListener(...args));
|
||||||
}
|
if (result) {
|
||||||
|
responses.push(result);
|
||||||
|
}
|
||||||
|
})());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for all promises to resolve
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
return responses;
|
return responses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import { System } from "../system.ts";
|
|||||||
import { fullQueueName } from "../lib/mq_util.ts";
|
import { fullQueueName } from "../lib/mq_util.ts";
|
||||||
import { MQMessage } from "$sb/types.ts";
|
import { MQMessage } from "$sb/types.ts";
|
||||||
import { MessageQueue } from "../lib/mq.ts";
|
import { MessageQueue } from "../lib/mq.ts";
|
||||||
|
import { throttle } from "$sb/lib/async.ts";
|
||||||
|
|
||||||
type MQSubscription = {
|
type MQSubscription = {
|
||||||
queue: string;
|
queue: string;
|
||||||
@ -24,14 +25,14 @@ export class MQHook implements Hook<MQHookT> {
|
|||||||
this.system = system;
|
this.system = system;
|
||||||
system.on({
|
system.on({
|
||||||
plugLoaded: () => {
|
plugLoaded: () => {
|
||||||
this.reloadQueues();
|
this.throttledReloadQueues();
|
||||||
},
|
},
|
||||||
plugUnloaded: () => {
|
plugUnloaded: () => {
|
||||||
this.reloadQueues();
|
this.throttledReloadQueues();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.reloadQueues();
|
this.throttledReloadQueues();
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
@ -40,6 +41,10 @@ export class MQHook implements Hook<MQHookT> {
|
|||||||
this.subscriptions = [];
|
this.subscriptions = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throttledReloadQueues = throttle(() => {
|
||||||
|
this.reloadQueues();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
reloadQueues() {
|
reloadQueues() {
|
||||||
this.stop();
|
this.stop();
|
||||||
for (const plug of this.system.loadedPlugs.values()) {
|
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";
|
import { PrefixedKvPrimitives } from "./prefixed_kv_primitives.ts";
|
||||||
|
|
||||||
async function test(db: KvPrimitives) {
|
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,
|
count: (arr: any[]) => arr.length,
|
||||||
});
|
});
|
||||||
await datastore.set(["user", "peter"], { name: "Peter" });
|
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 { FunctionMap, KV, KvKey, KvQuery } from "$sb/types.ts";
|
||||||
import { builtinFunctions } from "$sb/lib/builtin_query_functions.ts";
|
import { builtinFunctions } from "$sb/lib/builtin_query_functions.ts";
|
||||||
import { KvPrimitives } from "./kv_primitives.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
|
* This is the data store class you'll actually want to use, wrapping the primitives
|
||||||
* in a more user-friendly way
|
* in a more user-friendly way
|
||||||
*/
|
*/
|
||||||
export class DataStore {
|
export class DataStore {
|
||||||
|
private cache = new LimitedMap<any>(20);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly kv: KvPrimitives,
|
readonly kv: KvPrimitives,
|
||||||
|
private enableCache = false,
|
||||||
private functionMap: FunctionMap = builtinFunctions,
|
private functionMap: FunctionMap = builtinFunctions,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@ -50,6 +54,21 @@ export class DataStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async query<T = any>(query: KvQuery): Promise<KV<T>[]> {
|
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>[] = [];
|
const results: KV<T>[] = [];
|
||||||
let itemCount = 0;
|
let itemCount = 0;
|
||||||
// Accumulate results
|
// Accumulate results
|
||||||
@ -76,7 +95,12 @@ export class DataStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Apply order by, limit, and select
|
// 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> {
|
async queryDelete(query: KvQuery): Promise<void> {
|
||||||
|
@ -23,6 +23,9 @@ export class DataStoreMQ implements MessageQueue {
|
|||||||
seq = 0;
|
seq = 0;
|
||||||
|
|
||||||
async batchSend(queue: string, bodies: any[]): Promise<void> {
|
async batchSend(queue: string, bodies: any[]): Promise<void> {
|
||||||
|
if (bodies.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const messages: KV<MQMessage>[] = bodies.map((body) => {
|
const messages: KV<MQMessage>[] = bodies.map((body) => {
|
||||||
const id = `${Date.now()}-${String(++this.seq).padStart(6, "0")}`;
|
const id = `${Date.now()}-${String(++this.seq).padStart(6, "0")}`;
|
||||||
const key = [...queuedPrefix, queue, id];
|
const key = [...queuedPrefix, queue, id];
|
||||||
@ -54,6 +57,9 @@ export class DataStoreMQ implements MessageQueue {
|
|||||||
prefix: [...queuedPrefix, queue],
|
prefix: [...queuedPrefix, queue],
|
||||||
limit: ["number", maxItems],
|
limit: ["number", maxItems],
|
||||||
});
|
});
|
||||||
|
if (messages.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
// Put them in the processing queue
|
// Put them in the processing queue
|
||||||
await this.ds.batchSet(
|
await this.ds.batchSet(
|
||||||
messages.map((m) => ({
|
messages.map((m) => ({
|
||||||
@ -137,6 +143,9 @@ export class DataStoreMQ implements MessageQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async batchAck(queue: string, ids: string[]) {
|
async batchAck(queue: string, ids: string[]) {
|
||||||
|
if (ids.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await this.ds.batchDelete(
|
await this.ds.batchDelete(
|
||||||
ids.map((id) => [...processingPrefix, queue, id]),
|
ids.map((id) => [...processingPrefix, queue, id]),
|
||||||
);
|
);
|
||||||
@ -152,6 +161,9 @@ export class DataStoreMQ implements MessageQueue {
|
|||||||
prefix: processingPrefix,
|
prefix: processingPrefix,
|
||||||
filter: ["<", ["attr", "ts"], ["number", now - timeout]],
|
filter: ["<", ["attr", "ts"], ["number", now - timeout]],
|
||||||
});
|
});
|
||||||
|
if (messages.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await this.ds.batchDelete(messages.map((m) => m.key));
|
await this.ds.batchDelete(messages.map((m) => m.key));
|
||||||
const newMessages: KV<ProcessingMessage>[] = [];
|
const newMessages: KV<ProcessingMessage>[] = [];
|
||||||
for (const { value: m } of messages) {
|
for (const { value: m } of messages) {
|
||||||
|
@ -18,7 +18,9 @@ export async function pageComplete(completeEvent: CompleteEvent) {
|
|||||||
completeEvent.linePrefix,
|
completeEvent.linePrefix,
|
||||||
);
|
);
|
||||||
const tagToQuery = isInTemplateContext ? "template" : "page";
|
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];
|
const prefix = match[1];
|
||||||
if (prefix.startsWith("!")) {
|
if (prefix.startsWith("!")) {
|
||||||
// Federation prefix, let's first see if we're matching anything from federation that is locally synced
|
// 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
|
// "bare" anchor, match any page for completion purposes
|
||||||
filter = undefined;
|
filter = undefined;
|
||||||
}
|
}
|
||||||
const allAnchors = await queryObjects<AnchorObject>("anchor", { filter });
|
const allAnchors = await queryObjects<AnchorObject>("anchor", {
|
||||||
|
filter,
|
||||||
|
cacheSecs: 5,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
from: completeEvent.pos - match[1].length,
|
from: completeEvent.pos - match[1].length,
|
||||||
options: allAnchors.map((a) => ({
|
options: allAnchors.map((a) => ({
|
||||||
|
@ -64,7 +64,7 @@ export async function clearIndex(): Promise<void> {
|
|||||||
/**
|
/**
|
||||||
* Indexes entities in the data store
|
* Indexes entities in the data store
|
||||||
*/
|
*/
|
||||||
export async function indexObjects<T>(
|
export function indexObjects<T>(
|
||||||
page: string,
|
page: string,
|
||||||
objects: ObjectValue<T>[],
|
objects: ObjectValue<T>[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -127,14 +127,12 @@ export async function indexObjects<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (allAttributes.size > 0) {
|
if (allAttributes.size > 0) {
|
||||||
await indexObjects<AttributeObject>(
|
[...allAttributes].forEach(([key, value]) => {
|
||||||
page,
|
const [tagName, name] = key.split(":");
|
||||||
[...allAttributes].map(([key, value]) => {
|
const attributeType = value.startsWith("!") ? value.substring(1) : value;
|
||||||
const [tagName, name] = key.split(":");
|
kvs.push({
|
||||||
const attributeType = value.startsWith("!")
|
key: ["attribute", cleanKey(key, page)],
|
||||||
? value.substring(1)
|
value: {
|
||||||
: value;
|
|
||||||
return {
|
|
||||||
ref: key,
|
ref: key,
|
||||||
tag: "attribute",
|
tag: "attribute",
|
||||||
tagName,
|
tagName,
|
||||||
@ -142,11 +140,15 @@ export async function indexObjects<T>(
|
|||||||
attributeType,
|
attributeType,
|
||||||
readOnly: value.startsWith("!"),
|
readOnly: value.startsWith("!"),
|
||||||
page,
|
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) {
|
function cleanKey(ref: string, page: string) {
|
||||||
|
@ -42,23 +42,17 @@ export function determineType(v: any): string {
|
|||||||
export async function objectAttributeCompleter(
|
export async function objectAttributeCompleter(
|
||||||
attributeCompleteEvent: AttributeCompleteEvent,
|
attributeCompleteEvent: AttributeCompleteEvent,
|
||||||
): Promise<AttributeCompletion[]> {
|
): Promise<AttributeCompletion[]> {
|
||||||
const prefixFilter: QueryExpression = ["call", "startsWith", [[
|
|
||||||
"attr",
|
|
||||||
"name",
|
|
||||||
], ["string", attributeCompleteEvent.prefix]]];
|
|
||||||
const attributeFilter: QueryExpression | undefined =
|
const attributeFilter: QueryExpression | undefined =
|
||||||
attributeCompleteEvent.source === ""
|
attributeCompleteEvent.source === ""
|
||||||
? prefixFilter
|
? undefined
|
||||||
: ["and", prefixFilter, ["=", ["attr", "tagName"], [
|
: ["=", ["attr", "tagName"], ["string", attributeCompleteEvent.source]];
|
||||||
"string",
|
|
||||||
attributeCompleteEvent.source,
|
|
||||||
]]];
|
|
||||||
const allAttributes = await queryObjects<AttributeObject>("attribute", {
|
const allAttributes = await queryObjects<AttributeObject>("attribute", {
|
||||||
filter: attributeFilter,
|
filter: attributeFilter,
|
||||||
distinct: true,
|
distinct: true,
|
||||||
select: [{ name: "name" }, { name: "attributeType" }, { name: "tag" }, {
|
select: [{ name: "name" }, { name: "attributeType" }, { name: "tag" }, {
|
||||||
name: "readOnly",
|
name: "readOnly",
|
||||||
}],
|
}],
|
||||||
|
cacheSecs: 5,
|
||||||
});
|
});
|
||||||
return allAttributes.map((value) => {
|
return allAttributes.map((value) => {
|
||||||
return {
|
return {
|
||||||
|
@ -90,31 +90,28 @@ export const builtins: Record<string, Record<string, string>> = {
|
|||||||
|
|
||||||
export async function loadBuiltinsIntoIndex() {
|
export async function loadBuiltinsIntoIndex() {
|
||||||
console.log("Loading builtins attributes into index");
|
console.log("Loading builtins attributes into index");
|
||||||
const allTags: ObjectValue<TagObject>[] = [];
|
const allObjects: ObjectValue<any>[] = [];
|
||||||
for (const [tagName, attributes] of Object.entries(builtins)) {
|
for (const [tagName, attributes] of Object.entries(builtins)) {
|
||||||
allTags.push({
|
allObjects.push({
|
||||||
ref: tagName,
|
ref: tagName,
|
||||||
tag: "tag",
|
tag: "tag",
|
||||||
name: tagName,
|
name: tagName,
|
||||||
page: builtinPseudoPage,
|
page: builtinPseudoPage,
|
||||||
parent: "builtin",
|
parent: "builtin",
|
||||||
});
|
});
|
||||||
await indexObjects<AttributeObject>(
|
allObjects.push(
|
||||||
builtinPseudoPage,
|
...Object.entries(attributes).map(([name, attributeType]) => ({
|
||||||
Object.entries(attributes).map(([name, attributeType]) => {
|
ref: `${tagName}:${name}`,
|
||||||
return {
|
tag: "attribute",
|
||||||
ref: `${tagName}:${name}`,
|
tagName,
|
||||||
tag: "attribute",
|
name,
|
||||||
tagName,
|
attributeType: attributeType.startsWith("!")
|
||||||
name,
|
? attributeType.substring(1)
|
||||||
attributeType: attributeType.startsWith("!")
|
: attributeType,
|
||||||
? attributeType.substring(1)
|
readOnly: attributeType.startsWith("!"),
|
||||||
: attributeType,
|
page: builtinPseudoPage,
|
||||||
readOnly: attributeType.startsWith("!"),
|
})),
|
||||||
page: builtinPseudoPage,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await indexObjects(builtinPseudoPage, allTags);
|
await indexObjects(builtinPseudoPage, allObjects);
|
||||||
}
|
}
|
||||||
|
@ -23,13 +23,12 @@ functions:
|
|||||||
env: server
|
env: server
|
||||||
query:
|
query:
|
||||||
path: api.ts:query
|
path: api.ts:query
|
||||||
env: server
|
|
||||||
indexObjects:
|
indexObjects:
|
||||||
path: api.ts:indexObjects
|
path: api.ts:indexObjects
|
||||||
env: server
|
env: server
|
||||||
queryObjects:
|
queryObjects:
|
||||||
path: api.ts:queryObjects
|
path: api.ts:queryObjects
|
||||||
env: server
|
# Note: not setting env: server to allow for client-side datastore query caching
|
||||||
getObjectByRef:
|
getObjectByRef:
|
||||||
path: api.ts:getObjectByRef
|
path: api.ts:getObjectByRef
|
||||||
env: server
|
env: server
|
||||||
|
@ -14,21 +14,24 @@ export async function lintYAML({ tree }: LintEvent): Promise<LintDiagnostic[]> {
|
|||||||
const diagnostics: LintDiagnostic[] = [];
|
const diagnostics: LintDiagnostic[] = [];
|
||||||
const frontmatter = await extractFrontmatter(tree);
|
const frontmatter = await extractFrontmatter(tree);
|
||||||
const tags = ["page", ...frontmatter.tags || []];
|
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) => {
|
await traverseTreeAsync(tree, async (node) => {
|
||||||
if (node.type === "FrontMatterCode") {
|
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(
|
const lintResult = await lintYaml(
|
||||||
renderToText(node),
|
renderToText(node),
|
||||||
node.from!,
|
node.from!,
|
||||||
|
@ -73,6 +73,7 @@ export async function tagComplete(completeEvent: CompleteEvent) {
|
|||||||
filter: ["=", ["attr", "parent"], ["string", parent]],
|
filter: ["=", ["attr", "parent"], ["string", parent]],
|
||||||
select: [{ name: "name" }],
|
select: [{ name: "name" }],
|
||||||
distinct: true,
|
distinct: true,
|
||||||
|
cacheSecs: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (parent === "page") {
|
if (parent === "page") {
|
||||||
|
@ -91,7 +91,6 @@ export async function performQuery(parsedQuery: Query, pageObject: PageMeta) {
|
|||||||
export async function lintQuery(
|
export async function lintQuery(
|
||||||
{ name, tree }: LintEvent,
|
{ name, tree }: LintEvent,
|
||||||
): Promise<LintDiagnostic[]> {
|
): Promise<LintDiagnostic[]> {
|
||||||
const pageObject = await loadPageObject(name);
|
|
||||||
const diagnostics: LintDiagnostic[] = [];
|
const diagnostics: LintDiagnostic[] = [];
|
||||||
await traverseTreeAsync(tree, async (node) => {
|
await traverseTreeAsync(tree, async (node) => {
|
||||||
if (node.type === "FencedCode") {
|
if (node.type === "FencedCode") {
|
||||||
@ -111,6 +110,7 @@ export async function lintQuery(
|
|||||||
}
|
}
|
||||||
const bodyText = codeText.children![0].text!;
|
const bodyText = codeText.children![0].text!;
|
||||||
try {
|
try {
|
||||||
|
const pageObject = await loadPageObject(name);
|
||||||
const parsedQuery = await parseQuery(
|
const parsedQuery = await parseQuery(
|
||||||
await replaceTemplateVars(bodyText, pageObject),
|
await replaceTemplateVars(bodyText, pageObject),
|
||||||
);
|
);
|
||||||
|
@ -9,7 +9,9 @@ export async function completeTaskState(completeEvent: CompleteEvent) {
|
|||||||
if (!taskMatch) {
|
if (!taskMatch) {
|
||||||
return null;
|
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))];
|
const states = [...new Set(allStates.map((s) => s.state))];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -56,6 +56,7 @@ export async function templateSlashComplete(
|
|||||||
"boolean",
|
"boolean",
|
||||||
false,
|
false,
|
||||||
]]],
|
]]],
|
||||||
|
cacheSecs: 5,
|
||||||
});
|
});
|
||||||
return allTemplates.map((template) => ({
|
return allTemplates.map((template) => ({
|
||||||
label: template.trigger!,
|
label: template.trigger!,
|
||||||
|
@ -9,3 +9,16 @@ export {
|
|||||||
} from "https://deno.land/x/oak@v12.4.0/mod.ts";
|
} 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 * 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 { 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`,
|
`${this.dbPrefix}_state`,
|
||||||
);
|
);
|
||||||
await stateKvPrimitives.init();
|
await stateKvPrimitives.init();
|
||||||
this.stateDataStore = new DataStore(stateKvPrimitives);
|
this.stateDataStore = new DataStore(stateKvPrimitives, true);
|
||||||
|
|
||||||
// Setup message queue
|
// Setup message queue
|
||||||
this.mq = new DataStoreMQ(this.stateDataStore);
|
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 { SysCallMapping } from "../../plugos/system.ts";
|
||||||
import type { Client } from "../client.ts";
|
import type { Client } from "../client.ts";
|
||||||
import { proxySyscalls } from "./util.ts";
|
import { proxySyscall, proxySyscalls } from "./util.ts";
|
||||||
|
|
||||||
export function dataStoreProxySyscalls(client: Client): SysCallMapping {
|
export function dataStoreProxySyscalls(client: Client): SysCallMapping {
|
||||||
return proxySyscalls(client, [
|
const syscalls = proxySyscalls(client, [
|
||||||
"datastore.delete",
|
"datastore.delete",
|
||||||
"datastore.set",
|
"datastore.set",
|
||||||
"datastore.batchSet",
|
"datastore.batchSet",
|
||||||
"datastore.batchDelete",
|
"datastore.batchDelete",
|
||||||
"datastore.batchGet",
|
"datastore.batchGet",
|
||||||
"datastore.get",
|
"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