Add Store
This commit is contained in:
@@ -14,6 +14,7 @@ Some general-purpose utilities used elsewhere in paravel.
|
|||||||
- `LRUCache` is an implementation of an LRU cache.
|
- `LRUCache` is an implementation of an LRU cache.
|
||||||
- `Worker` is an implementation of an asynchronous queue.
|
- `Worker` is an implementation of an asynchronous queue.
|
||||||
- `Tools` is a collection of general-purpose utility functions.
|
- `Tools` is a collection of general-purpose utility functions.
|
||||||
|
- `Store` is an implementation of svelte-like subscribable stores with extra features.
|
||||||
|
|
||||||
## @coracle.social/util
|
## @coracle.social/util
|
||||||
|
|
||||||
|
|||||||
Generated
+17
-1
@@ -391,6 +391,12 @@
|
|||||||
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
|
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/throttle-debounce": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-pDzSNulqooSKvSNcksnV72nk8p7gRqN8As71Sp28nov1IgmPKWbOEIwAWvBME5pPTtaXJAvG3O4oc76HlQ4kqQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.62.0",
|
"version": "5.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
|
||||||
@@ -3029,6 +3035,14 @@
|
|||||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/throttle-debounce": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/through": {
|
"node_modules/through": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||||
@@ -3323,10 +3337,12 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scure/base": "^1.1.6"
|
"@scure/base": "^1.1.6",
|
||||||
|
"throttle-debounce": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/events": "^3.0.3",
|
"@types/events": "^3.0.3",
|
||||||
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"gts": "^5.0.1",
|
"gts": "^5.0.1",
|
||||||
"tsc-multi": "^1.1.0",
|
"tsc-multi": "^1.1.0",
|
||||||
"typescript": "~5.1.6"
|
"typescript": "~5.1.6"
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ export class Fluent<T> {
|
|||||||
|
|
||||||
forEach = (f: (t: T, i: number) => void) => this.xs.forEach(f)
|
forEach = (f: (t: T, i: number) => void) => this.xs.forEach(f)
|
||||||
|
|
||||||
|
join = (s: string) => this.valueOf().join(s)
|
||||||
|
|
||||||
set = (i: number, x: T) => this.clone([...this.xs.slice(0, i), x, ...this.xs.slice(i + 1)])
|
set = (i: number, x: T) => this.clone([...this.xs.slice(0, i), x, ...this.xs.slice(i + 1)])
|
||||||
|
|
||||||
concat = (xs: T[]) => this.clone(this.xs.concat(xs))
|
concat = (xs: T[]) => this.clone(this.xs.concat(xs))
|
||||||
|
|||||||
@@ -0,0 +1,329 @@
|
|||||||
|
import {throttle} from "throttle-debounce"
|
||||||
|
import {ensurePlural, identity} from "./Tools"
|
||||||
|
|
||||||
|
type Invalidator<T> = (value?: T) => void
|
||||||
|
type Derivable = Readable<any> | Readable<any>[]
|
||||||
|
type Subscriber<T> = (value: T) => void
|
||||||
|
type Unsubscriber = () => void
|
||||||
|
type R = Record<string, any>
|
||||||
|
type M<T> = Map<string, T>
|
||||||
|
|
||||||
|
export interface Readable<T> {
|
||||||
|
get: () => T
|
||||||
|
subscribe(this: void, run: Subscriber<T>, invalidate?: Invalidator<T>): Unsubscriber
|
||||||
|
derived: <U>(f: (v: T) => U) => Readable<U>
|
||||||
|
throttle(t: number): Readable<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Writable<T> implements Readable<T> {
|
||||||
|
private value: T
|
||||||
|
private subs: Subscriber<T>[] = []
|
||||||
|
|
||||||
|
constructor(defaultValue: T, t?: number) {
|
||||||
|
this.value = defaultValue
|
||||||
|
|
||||||
|
if (t) {
|
||||||
|
this.notify = throttle(t, this.notify)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notify = () => {
|
||||||
|
for (const sub of this.subs) {
|
||||||
|
sub(this.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
}
|
||||||
|
|
||||||
|
set(newValue: T) {
|
||||||
|
this.value = newValue
|
||||||
|
this.notify()
|
||||||
|
}
|
||||||
|
|
||||||
|
update(f: (v: T) => T) {
|
||||||
|
this.set(f(this.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAsync(f: (v: T) => Promise<T>) {
|
||||||
|
this.set(await f(this.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(f: Subscriber<T>) {
|
||||||
|
this.subs.push(f)
|
||||||
|
|
||||||
|
f(this.value)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.subs.splice(this.subs.findIndex(x => x === f), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
derived<U>(f: (v: T) => U): Derived<U> {
|
||||||
|
return new Derived<U>(this, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
throttle = (t: number): Derived<T> => {
|
||||||
|
return new Derived<T>(this, identity, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Derived<T> implements Readable<T> {
|
||||||
|
private callerSubs: Subscriber<T>[] = []
|
||||||
|
private mySubs: Unsubscriber[] = []
|
||||||
|
private stores: Derivable
|
||||||
|
private getValue: (values: any) => T
|
||||||
|
private latestValue: T | undefined
|
||||||
|
|
||||||
|
constructor(stores: Derivable, getValue: (values: any) => T, t = 0) {
|
||||||
|
this.stores = stores
|
||||||
|
this.getValue = getValue
|
||||||
|
|
||||||
|
if (t) {
|
||||||
|
this.notify = throttle(t, this.notify)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notify = () => {
|
||||||
|
this.latestValue = undefined
|
||||||
|
this.callerSubs.forEach(f => f(this.get()))
|
||||||
|
}
|
||||||
|
|
||||||
|
getInput() {
|
||||||
|
if (Array.isArray(this.stores)) {
|
||||||
|
return this.stores.map(s => s.get())
|
||||||
|
} else {
|
||||||
|
return this.stores.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get = (): T => {
|
||||||
|
// Recalculate if we're not subscribed, because we won't get notified when deps change
|
||||||
|
if (this.latestValue === undefined || this.mySubs.length === 0) {
|
||||||
|
this.latestValue = this.getValue(this.getInput())
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.latestValue
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(f: Subscriber<T>) {
|
||||||
|
if (this.callerSubs.length === 0) {
|
||||||
|
for (const s of ensurePlural(this.stores)) {
|
||||||
|
this.mySubs.push(s.subscribe(this.notify))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callerSubs.push(f)
|
||||||
|
|
||||||
|
f(this.get())
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.callerSubs.splice(this.callerSubs.findIndex(x => x === f), 1)
|
||||||
|
|
||||||
|
if (this.callerSubs.length === 0) {
|
||||||
|
for (const unsub of this.mySubs.splice(0)) {
|
||||||
|
unsub()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
derived<U>(f: (v: T) => U): Readable<U> {
|
||||||
|
return new Derived(this, f) as Readable<U>
|
||||||
|
}
|
||||||
|
|
||||||
|
throttle = (t: number): Readable<T> => {
|
||||||
|
return new Derived<T>(this, identity, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Key<T extends R> implements Readable<T> {
|
||||||
|
readonly pk: string
|
||||||
|
readonly key: string
|
||||||
|
private base: Writable<M<T>>
|
||||||
|
private store: Readable<T>
|
||||||
|
|
||||||
|
constructor(base: Writable<M<T>>, pk: string, key: string) {
|
||||||
|
if (!(base.get() instanceof Map)) {
|
||||||
|
throw new Error("`key` can only be used on map collections")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pk = pk
|
||||||
|
this.key = key
|
||||||
|
this.base = base
|
||||||
|
this.store = base.derived<T>(m => m.get(key) as T)
|
||||||
|
}
|
||||||
|
|
||||||
|
get = () => this.base.get().get(this.key) as T
|
||||||
|
|
||||||
|
subscribe = (f: Subscriber<T>) => this.store.subscribe(f)
|
||||||
|
|
||||||
|
derived = <U>(f: (v: T) => U) => this.store.derived<U>(f)
|
||||||
|
|
||||||
|
throttle = (t: number) => this.store.throttle(t)
|
||||||
|
|
||||||
|
exists = () => this.base.get().has(this.key)
|
||||||
|
|
||||||
|
update = (f: (v: T) => T) =>
|
||||||
|
this.base.update((m: M<T>) => {
|
||||||
|
if (!this.key) {
|
||||||
|
throw new Error(`Cannot set key: "${this.key}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the pk always get set on the record
|
||||||
|
const {pk, key} = this
|
||||||
|
const oldValue = {...m.get(key), [pk]: key} as T
|
||||||
|
const newValue = {...f(oldValue), [pk]: key}
|
||||||
|
|
||||||
|
m.set(this.key, newValue)
|
||||||
|
|
||||||
|
return m
|
||||||
|
})
|
||||||
|
|
||||||
|
set = (v: T) => this.update(() => v)
|
||||||
|
|
||||||
|
merge = (d: Partial<T>) => this.update(v => ({...v, ...d}))
|
||||||
|
|
||||||
|
remove = () =>
|
||||||
|
this.base.update(m => {
|
||||||
|
m.delete(this.key)
|
||||||
|
|
||||||
|
return m
|
||||||
|
})
|
||||||
|
|
||||||
|
pop = () => {
|
||||||
|
const v = this.get()
|
||||||
|
|
||||||
|
this.remove()
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DerivedKey<T extends R> implements Readable<T> {
|
||||||
|
readonly pk: string
|
||||||
|
readonly key: string
|
||||||
|
private base: Readable<M<T>>
|
||||||
|
private store: Readable<T>
|
||||||
|
|
||||||
|
constructor(base: Readable<M<T>>, pk: string, key: string) {
|
||||||
|
if (!(base.get() instanceof Map)) {
|
||||||
|
throw new Error("`key` can only be used on map collections")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pk = pk
|
||||||
|
this.key = key
|
||||||
|
this.base = base
|
||||||
|
this.store = base.derived<T>(m => m.get(key) as T)
|
||||||
|
}
|
||||||
|
|
||||||
|
get = () => this.base.get().get(this.key) as T
|
||||||
|
|
||||||
|
subscribe = (f: Subscriber<T>) => this.store.subscribe(f)
|
||||||
|
|
||||||
|
derived = <U>(f: (v: T) => U) => this.store.derived<U>(f)
|
||||||
|
|
||||||
|
throttle = (t: number) => this.store.throttle(t)
|
||||||
|
|
||||||
|
exists = () => this.base.get().has(this.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Collection<T extends R> implements Readable<T[]> {
|
||||||
|
readonly pk: string
|
||||||
|
readonly mapStore: Writable<M<T>>
|
||||||
|
readonly listStore: Readable<T[]>
|
||||||
|
|
||||||
|
constructor(pk: string, t?: number) {
|
||||||
|
this.pk = pk
|
||||||
|
this.mapStore = writable(new Map())
|
||||||
|
this.listStore = this.mapStore.derived<T[]>((m: M<T>) => Array.from(m.values()))
|
||||||
|
|
||||||
|
if (t) {
|
||||||
|
this.mapStore.notify = throttle(t, this.mapStore.notify)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get = () => this.listStore.get()
|
||||||
|
|
||||||
|
getMap = () => this.mapStore.get()
|
||||||
|
|
||||||
|
subscribe = (f: Subscriber<T[]>) => this.listStore.subscribe(f)
|
||||||
|
|
||||||
|
derived = <U>(f: (v: T[]) => U) => this.listStore.derived<U>(f)
|
||||||
|
|
||||||
|
throttle = (t: number) => this.listStore.throttle(t)
|
||||||
|
|
||||||
|
key = (k: string) => new Key(this.mapStore, this.pk, k)
|
||||||
|
|
||||||
|
set = (xs: T[]) => {
|
||||||
|
const m = new Map()
|
||||||
|
|
||||||
|
for (const x of xs) {
|
||||||
|
if (!x) {
|
||||||
|
console.error("Empty value passed to collection store")
|
||||||
|
} else if (!x[this.pk]) {
|
||||||
|
console.error(`Value with empty ${this.pk} passed to collection store`, x)
|
||||||
|
} else {
|
||||||
|
m.set(x[this.pk], x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mapStore.set(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
update = (f: (v: T[]) => T[]) => this.set(f(this.get()))
|
||||||
|
|
||||||
|
updateAsync = async (f: (v: T[]) => Promise<T[]>) => this.set(await f(this.get()))
|
||||||
|
|
||||||
|
reject = (f: (v: T) => boolean) => this.update((xs: T[]) => xs.filter(x => !f(x)))
|
||||||
|
|
||||||
|
filter = (f: (v: T) => boolean) => this.update((xs: T[]) => xs.filter(f))
|
||||||
|
|
||||||
|
map = (f: (v: T) => T) => this.update((xs: T[]) => xs.map(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DerivedCollection<T extends R> implements Readable<T[]> {
|
||||||
|
readonly listStore: Derived<T[]>
|
||||||
|
readonly mapStore: Readable<M<T>>
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly pk: string,
|
||||||
|
stores: Derivable,
|
||||||
|
getValue: (values: any) => T[],
|
||||||
|
t = 0,
|
||||||
|
) {
|
||||||
|
this.listStore = new Derived(stores, getValue, t)
|
||||||
|
this.mapStore = new Derived(this.listStore, xs => new Map(xs.map((x: T) => [x[pk], x])))
|
||||||
|
}
|
||||||
|
|
||||||
|
get = () => this.listStore.get()
|
||||||
|
|
||||||
|
getMap = () => this.mapStore.get()
|
||||||
|
|
||||||
|
subscribe = (f: Subscriber<T[]>) => this.listStore.subscribe(f)
|
||||||
|
|
||||||
|
derived = <U>(f: (v: T[]) => U) => this.listStore.derived<U>(f)
|
||||||
|
|
||||||
|
throttle = (t: number) => this.listStore.throttle(t)
|
||||||
|
|
||||||
|
key = (k: string) => new DerivedKey(this.mapStore, this.pk, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const writable = <T>(v: T) => new Writable(v)
|
||||||
|
|
||||||
|
export const derived = <T>(stores: Derivable, getValue: (values: any) => T) =>
|
||||||
|
new Derived(stores, getValue)
|
||||||
|
|
||||||
|
export const readable = <T>(v: T) => derived(new Writable(v), identity) as Readable<T>
|
||||||
|
|
||||||
|
export const derivedCollection = <T extends R>(
|
||||||
|
pk: string,
|
||||||
|
stores: Derivable,
|
||||||
|
getValue: (values: any) => T[],
|
||||||
|
) => new DerivedCollection(pk, stores, getValue)
|
||||||
|
|
||||||
|
export const key = <T extends R>(base: Writable<M<T>>, pk: string, key: string) =>
|
||||||
|
new Key<T>(base, pk, key)
|
||||||
|
|
||||||
|
export const collection = <T extends R>(pk: string) => new Collection<T>(pk)
|
||||||
@@ -28,6 +28,8 @@ export const toIterable = (x: any) => isIterable(x) ? x : [x]
|
|||||||
|
|
||||||
export const stripProtocol = (url: string) => url.replace(/.*:\/\//, "")
|
export const stripProtocol = (url: string) => url.replace(/.*:\/\//, "")
|
||||||
|
|
||||||
|
export const ensurePlural = <T>(x: T | T[]) => (x instanceof Array ? x : [x])
|
||||||
|
|
||||||
export const groupBy = <T>(f: (x: T) => string, xs: T[]) => {
|
export const groupBy = <T>(f: (x: T) => string, xs: T[]) => {
|
||||||
const r: Record<string, T[]> = {}
|
const r: Record<string, T[]> = {}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export * from './Deferred'
|
|||||||
export * from './Emitter'
|
export * from './Emitter'
|
||||||
export * from './Fluent'
|
export * from './Fluent'
|
||||||
export * from './LRUCache'
|
export * from './LRUCache'
|
||||||
export * from './Worker'
|
export * from './Store'
|
||||||
export * from './Tools'
|
export * from './Tools'
|
||||||
|
export * from './Worker'
|
||||||
export {default as normalizeUrl} from './normalize-url'
|
export {default as normalizeUrl} from './normalize-url'
|
||||||
|
|||||||
@@ -27,11 +27,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/events": "^3.0.3",
|
"@types/events": "^3.0.3",
|
||||||
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"gts": "^5.0.1",
|
"gts": "^5.0.1",
|
||||||
"tsc-multi": "^1.1.0",
|
"tsc-multi": "^1.1.0",
|
||||||
"typescript": "~5.1.6"
|
"typescript": "~5.1.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scure/base": "^1.1.6"
|
"@scure/base": "^1.1.6",
|
||||||
|
"throttle-debounce": "^5.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,4 +68,3 @@ export const isChildOf = (child: EventTemplate, parent: Rumor) => {
|
|||||||
|
|
||||||
return getIdAndAddress(parent).some(x => parentIds.includes(x))
|
return getIdAndAddress(parent).some(x => parentIds.includes(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user