reactiveStateBlob: wrap blob state in a proxy to autosave it on changes

Examples (full example at @postpostscript/reactiveStateBlobExample)

import { reactiveStateBlob } from "https://esm.town/v/postpostscript/reactiveStateBlob"

using state = await reactiveStateBlob({
  viewCount: 0,
  rows: [] as {
    x: number;
    y: number;
  }[],
});

state.viewCount += 1;
state.rows.push({
  x: Math.random(),
  y: Math.random(),
});

This infers the key from the name of the val that uses it. To specify it, pass the key option:

Create valusing state = await reactiveStateBlob({ viewCount: 0, rows: [] as { x: number; y: number; }[], }, { key: 'reactiveStateBlobExample.state', });

Updating Schema

If you want to update the schema, or always verify the state that is pulled from the job, pass a function as the first argument:

using state = await reactiveStateBlob((existingState) => {
  return {
    viewCount: (existingState.viewCount ?? 0) as number,
    rows: (existingState.rows ?? []) as {
      x: number;
      y: number;
    }[],
	someNewField: (existingState.someNewField ?? "") as string,
  }
})

Options

using state = await reactiveStateBlob<{
  value: number;
}>({
  value: 0,
}, {
  log: true,      // log when saving
  key: "blobKey", // blob key to fetch/save to
  timeout: 100,   // ms, defaults to 10
  lock: true, // or LockOptions (see https://www.val.town/v/postpostscript/lock#options)
})

See Also

@postpostscript/counter (example at @postpostscript/counterExample)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import { rootValRef } from "https://esm.town/v/andreterron/rootValRef?v=3";
import { disposable } from "https://esm.town/v/postpostscript/disposable";
import { acquireLock, type LockOptions } from "https://esm.town/v/postpostscript/lock";
import {
type PersistentState,
persistentStateLazy,
persistentStateReactive,
} from "https://esm.town/v/postpostscript/persistentState";
import { blob } from "https://esm.town/v/std/blob";
export type PersistentStateGetDefault<T> = Promise<T> | T | ((value: any) => Promise<T> | T);
export type PersistentStateBlobOptions<T> = {
key: string;
getDefault: PersistentStateGetDefault<T>;
timeout?: number;
log?: boolean;
};
export async function reactiveStateBlob<T extends Record<string, unknown>>(
getDefault: PersistentStateGetDefault<T>,
opts?: {
key?: string;
timeout?: number;
log?: boolean;
lock?: boolean | LockOptions;
},
): Promise<T> {
const _key = opts?.key ?? (() => {
const { handle, name } = rootValRef();
return `reactiveStateBlob:${handle}/${name}`;
})();
const stateLock = opts?.lock
? await acquireLock({
id: `lock:${_key}`,
...((opts?.lock && typeof opts.lock === "object") ? opts.lock : {}),
})
: undefined;
const cache = persistentStateBlobReactive({
...opts,
key: _key,
getDefault,
});
return disposable(await cache.get(), (value) => {
return stateLock?.release();
}, true);
}
export function persistentStateBlob<T extends Record<string, unknown>>(
opts: PersistentStateBlobOptions<T>,
): PersistentState<T> {
return {
async get(): Promise<T> {
return normalizeValue(opts.getDefault, await blob.getJSON(opts.key));
},
set(value) {
if (opts.log) {
console.log("persistentStateBlob saving to blob:", opts.key, "=", value);
}
return blob.setJSON(opts.key, value);
},
};
}
export function persistentStateBlobReactive<T extends Record<string, unknown>>(
opts: PersistentStateBlobOptions<T>,
): PersistentState<T> {
return persistentStateReactive(persistentStateBlob(opts));
}
export function persistentStateBlobLazy<T extends Record<string, unknown>>(
opts: PersistentStateBlobOptions<T>,
): PersistentState<T> {
return persistentStateLazy(persistentStateBlob(opts));
}
async function normalizeValue<T>(value: PersistentStateGetDefault<T>, existing: unknown): Promise<T> {
if (value instanceof Function) {
return value(existing);
}
return existing as T ?? value;
}
1
Next