Skip to content

Commit 172f95f

Browse files
Update method for deassetization (#213)
1 parent 08fbd29 commit 172f95f

File tree

13 files changed

+2606
-88
lines changed

13 files changed

+2606
-88
lines changed

packages/data-sdk/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@eslint/js": "^9.11.1",
5050
"@formant/realtime-sdk": "1.4.3",
5151
"@types/base-64": "^1.0.0",
52+
"@types/fast-json-stable-stringify": "=2.0.0",
5253
"@types/node": "^18.16.3",
5354
"@types/pako": "^2.0.0",
5455
"@typescript-eslint/eslint-plugin": "^8.7.0",
@@ -75,7 +76,9 @@
7576
"base64-js": "^1.5.1",
7677
"date-fns": "^2.30.0",
7778
"eventemitter3": "^5.0.1",
79+
"fast-json-stable-stringify": "=2.0.0",
7880
"google-protobuf": "^3.21.2",
81+
"lru-cache": "^7.14.0",
7982
"lzfjs": "^1.0.1",
8083
"pako": "^2.1.0",
8184
"validator": "^13.11.0"
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import stringify from "fast-json-stable-stringify";
2+
import BaseLruCache from "lru-cache";
3+
4+
export type CacheOptions<V> = BaseLruCache.Options<string, V> & {
5+
name: string;
6+
fastStringify?: boolean;
7+
};
8+
9+
export class LruCache<K, V> {
10+
private cache!: BaseLruCache<string, V>;
11+
private stringify: (_: K) => string;
12+
13+
constructor(private options: CacheOptions<V>) {
14+
this.cache = new BaseLruCache<string, V>({
15+
// if using the dispose callback,
16+
// by default automatically prune expired entries so
17+
// they are delivered consistently and quickly
18+
...(options.dispose || options.disposeAfter
19+
? { ttlAutopurge: true }
20+
: {}),
21+
...options,
22+
dispose: (...args) => {
23+
options.dispose?.(...args);
24+
},
25+
disposeAfter: (...args) => {
26+
options.disposeAfter?.(...args);
27+
},
28+
});
29+
this.stringify = options.fastStringify ? JSON.stringify : stringify;
30+
}
31+
32+
public set(key: K, value: V, ttl?: number): void {
33+
const keyString = this.stringify(key);
34+
if (!this.cache.set(keyString, value, { ttl })) {
35+
const size = this.cache.sizeCalculation
36+
? this.cache.sizeCalculation(value, keyString)
37+
: "unknown";
38+
throw Error(`Value too large (${size} > ${this.cache.max})`);
39+
}
40+
}
41+
42+
public get(key: K): V | undefined {
43+
const keyString = this.stringify(key);
44+
return this.cache.get(keyString);
45+
}
46+
47+
public delete(key: K) {
48+
this.cache.delete(this.stringify(key));
49+
}
50+
51+
public peek(key: K): V | undefined {
52+
return this.cache.peek(this.stringify(key));
53+
}
54+
55+
public size() {
56+
return this.cache.size;
57+
}
58+
59+
public clear() {
60+
this.cache.clear();
61+
}
62+
63+
public forEach(callback: (value: V) => void) {
64+
this.cache.forEach(callback);
65+
}
66+
67+
public purgeStale(): boolean {
68+
return this.cache.purgeStale();
69+
}
70+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { duration } from "./duration";
2+
3+
import { delay } from "./delay";
4+
import { CacheOptions, LruCache } from "./LruCache";
5+
6+
export type PromiseCacheOptions<V> = CacheOptions<Promise<V>> & {
7+
expireRejectedPromiseValues?: boolean;
8+
rejectedPromiseValueTtl?: number;
9+
};
10+
11+
export class PromiseLruCache<K, V> extends LruCache<K, Promise<V>> {
12+
protected expireRejectedPromiseValues: boolean;
13+
protected rejectedPromiseValueTtl: number;
14+
15+
constructor(options: PromiseCacheOptions<V>) {
16+
super(options);
17+
18+
this.expireRejectedPromiseValues =
19+
options.expireRejectedPromiseValues !== undefined
20+
? options.expireRejectedPromiseValues
21+
: true;
22+
23+
this.rejectedPromiseValueTtl =
24+
options.rejectedPromiseValueTtl !== undefined
25+
? options.rejectedPromiseValueTtl
26+
: duration.second;
27+
28+
if (this.rejectedPromiseValueTtl < 0) {
29+
throw new Error("rejectedPromiseValueTtl must not be negative");
30+
}
31+
}
32+
33+
public set(key: K, value: Promise<V>, ttl?: number): void {
34+
super.set(key, value, ttl);
35+
36+
if (this.expireRejectedPromiseValues) {
37+
value.catch(async () => {
38+
await delay(this.rejectedPromiseValueTtl);
39+
if (this.peek(key) === value) {
40+
this.delete(key);
41+
}
42+
});
43+
}
44+
}
45+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { toStringSafe } from "./toStringSafe";
2+
3+
/**
4+
* Convert an error object to plain javascript object, including any custom properties.
5+
*/
6+
export function errorToObject(
7+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8+
error: any
9+
) {
10+
if (!error.stack) {
11+
return { message: toStringSafe(error) };
12+
}
13+
14+
const { name, message, stack, ...meta } = error;
15+
16+
return {
17+
name,
18+
message: message !== undefined ? message : toStringSafe(error),
19+
stack,
20+
meta: meta && Object.keys(meta).length === 0 ? undefined : meta,
21+
};
22+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { errorToObject } from "./errorToObject";
2+
import { toStringSafe } from "./toStringSafe";
3+
4+
/**
5+
* Serialize an error object to a string, including any custom properties.
6+
*/
7+
export function errorToString(error: unknown): string {
8+
const { message, stack, meta } = errorToObject(error);
9+
10+
return `${stack || message || ""}${
11+
meta && Object.keys(meta).length > 0 ? ` -- ${toStringSafe(meta)}` : ""
12+
}`;
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import stringify from "fast-json-stable-stringify";
2+
3+
/**
4+
* Safely stringify input.
5+
* Similar to `JSON.stringify` but
6+
* doesn't throw an error if input includes circular properties.
7+
*
8+
* Warning: output is not guaranteed to be a valid JSON string.
9+
*/
10+
export function toStringSafe(input: unknown): string {
11+
return stringify(input, { cycles: true });
12+
}

packages/data-sdk/src/connector/data/BaseUniverseDataConnector.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
RealtimeButtonConfiguration,
3333
UniverseDataSource,
3434
} from "../model/IUniverseData";
35+
import { DataLoader } from "./loader/DataLoader";
3536
import { QueryStore } from "./queryStore";
3637

3738
export type DeviceId = string;
@@ -68,6 +69,7 @@ export class BasicUniverseDataConnector {
6869
timeChangeListeners: ((time: Date | "live") => void)[] = [];
6970

7071
queryStore: QueryStore = new QueryStore();
72+
dataLoader: any;
7173

7274
setTime(time: Date | "live"): void {
7375
if (time !== "live") {
@@ -86,6 +88,7 @@ export class BasicUniverseDataConnector {
8688

8789
constructor() {
8890
this.time = "live";
91+
this.dataLoader = DataLoader.get();
8992

9093
const dataLoop = async () => {
9194
if (Array.from(this.subscriberLoaders.keys()).length > 0) {

packages/data-sdk/src/connector/data/LiveUniverseData.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ export class LiveUniverseData
303303
deviceId,
304304
source,
305305
async (data: IStreamData[]): Promise<DataResult<T>> => {
306-
let foundUrl: string | undefined;
306+
let found: string | undefined;
307307
let jsonValue: T | undefined;
308308
for (let i = 0; i < data.length; i += 1) {
309309
const _ = data[i];
@@ -313,13 +313,14 @@ export class LiveUniverseData
313313
_.type === "json"
314314
) {
315315
const [_time, value] = _.points[_.points.length - 1];
316-
foundUrl = value as string;
316+
found = value as string;
317317
}
318318
}
319-
if (foundUrl) {
320-
jsonValue = await fetch(foundUrl).then(
321-
(r) => r.json() as Promise<T>
322-
);
319+
if (found?.startsWith("http")) {
320+
const loaded = await this.dataLoader.load(found);
321+
jsonValue = loaded.json;
322+
} else {
323+
jsonValue = JSON.parse(found || "{}");
323324
}
324325
return {
325326
deviceId,

0 commit comments

Comments
 (0)