feat(infra): computed livedata (#6091)

This commit is contained in:
EYHN
2024-03-13 06:26:05 +00:00
parent cacc2d311e
commit fd9084ea6a
2 changed files with 223 additions and 2 deletions
+118 -1
View File
@@ -132,10 +132,100 @@ export class LiveData<T = unknown> implements InteropObservable<T> {
return data;
}
private static GLOBAL_COMPUTED_RECURSIVE_COUNT = 0;
/**
* @example
* ```ts
* const a = new LiveData('v1');
* const v1 = new LiveData(100);
* const v2 = new LiveData(200);
*
* const v = LiveData.computed(get => {
* return get(a) === 'v1' ? get(v1) : get(v2);
* });
*
* expect(v.value).toBe(100);
* ```
*/
static computed<T>(
compute: (get: <L>(data: LiveData<L>) => L) => T
): LiveData<T> {
return LiveData.from(
new Observable(subscribe => {
const execute = (next: () => void) => {
const subscriptions: Subscription[] = [];
const getfn = <L>(data: LiveData<L>) => {
let value = null as L;
let first = true;
subscriptions.push(
data.subscribe({
error(err) {
subscribe.error(err);
},
next(v) {
value = v;
if (!first) {
next();
}
first = false;
},
})
);
return value;
};
LiveData.GLOBAL_COMPUTED_RECURSIVE_COUNT++;
try {
if (LiveData.GLOBAL_COMPUTED_RECURSIVE_COUNT > 10) {
subscribe.error(new Error('computed recursive limit exceeded'));
} else {
subscribe.next(compute(getfn));
}
} catch (err) {
subscribe.error(err);
} finally {
LiveData.GLOBAL_COMPUTED_RECURSIVE_COUNT--;
}
return () => {
subscriptions.forEach(s => s.unsubscribe());
};
};
let prev = () => {};
const looper = () => {
const dispose = execute(looper);
prev();
prev = dispose;
};
looper();
return () => {
prev();
};
}),
null as any
);
}
private readonly raw: BehaviorSubject<T>;
private readonly ops = new Subject<LiveDataOperation>();
private readonly upstreamSubscription: Subscription | undefined;
/**
* When the upstream Observable of livedata throws an error, livedata will enter poisoned state. This is an
* unrecoverable abnormal state. Any operation on livedata will throw a PoisonedError.
*
* Since the development specification for livedata is not to throw any error, entering the poisoned state usually
* means a programming error.
*/
private isPoisoned = false;
private poisonedError: PoisonedError | null = null;
constructor(
initialValue: T,
upstream:
@@ -155,17 +245,26 @@ export class LiveData<T = unknown> implements InteropObservable<T> {
},
error: err => {
logger.error('uncatched error in livedata', err);
this.isPoisoned = true;
this.poisonedError = new PoisonedError(err);
this.raw.error(this.poisonedError);
},
});
}
}
getValue(): T {
if (this.isPoisoned) {
throw this.poisonedError;
}
this.ops.next('get');
return this.raw.value;
}
setValue(v: T) {
if (this.isPoisoned) {
throw this.poisonedError;
}
this.raw.next(v);
this.ops.next('set');
}
@@ -175,10 +274,13 @@ export class LiveData<T = unknown> implements InteropObservable<T> {
}
set value(v: T) {
this.setValue(v);
this.next(v);
}
next(v: T) {
if (this.isPoisoned) {
throw this.poisonedError;
}
this.setValue(v);
}
@@ -306,6 +408,9 @@ export class LiveData<T = unknown> implements InteropObservable<T> {
}
reactSubscribe = (cb: () => void) => {
if (this.isPoisoned) {
throw this.poisonedError;
}
this.ops.next('watch');
const subscription = this.raw
.pipe(distinctUntilChanged(), skip(1))
@@ -317,6 +422,9 @@ export class LiveData<T = unknown> implements InteropObservable<T> {
};
reactGetSnapshot = () => {
if (this.isPoisoned) {
throw this.poisonedError;
}
this.ops.next('watch');
setImmediate(() => {
this.ops.next('unwatch');
@@ -343,3 +451,12 @@ export type Unwrap<T> =
: T;
export type Flat<T> = T extends LiveData<infer P> ? LiveData<Unwrap<P>> : T;
export class PoisonedError extends Error {
constructor(originalError: any) {
super(
'The livedata is poisoned, original error: ' +
(originalError instanceof Error ? originalError.stack : originalError)
);
}
}