Rust implementation of NextGraph, a Decentralized and local-first web 3.0 ecosystem
https://nextgraph.org
byzantine-fault-tolerancecrdtsdappsdecentralizede2eeeventual-consistencyjson-ldlocal-firstmarkdownocapoffline-firstp2pp2p-networkprivacy-protectionrdfrich-text-editorself-hostedsemantic-websparqlweb3collaboration
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
188 lines
5.3 KiB
188 lines
5.3 KiB
import { isObject, isPlainObject, isSet, isMap, isArray } from "./utils";
|
|
import { isSignal } from "./core";
|
|
import {
|
|
isDeepSignal,
|
|
subscribeDeepMutations,
|
|
getDeepSignalRootId,
|
|
DeepPatch,
|
|
} from "./deepSignal";
|
|
import { ReactiveFlags } from "./contents";
|
|
|
|
/** Function provided to register a disposer (runs before next callback or on stop). */
|
|
export type RegisterCleanup = (cleanupFn: () => void) => void;
|
|
/** Signature for watchEffect style sources receiving the cleanup registrar. */
|
|
export type WatchEffect = (registerCleanup: RegisterCleanup) => void;
|
|
|
|
/** Options for {@link watch}. */
|
|
export interface WatchOptions {
|
|
/** Trigger the callback immediately with the current value (default: false). */
|
|
immediate?: boolean;
|
|
/** Auto-stop the watcher after the first callback run that delivers patches (or immediate call if no patches). */
|
|
once?: boolean;
|
|
/** Allow legacy/unknown options (ignored) to avoid hard breaks while migrating. */
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
[legacy: string]: any;
|
|
}
|
|
|
|
export interface WatchPatchEvent<Root = any> {
|
|
/** Patch batch that triggered this callback (may be empty for immediate). */
|
|
patches: DeepPatch[];
|
|
/** Previous snapshot (deep-cloned) of the root value before these patches. Undefined on first call. */
|
|
oldValue: Root | undefined;
|
|
/** Current root value (live proxy). */
|
|
newValue: Root;
|
|
}
|
|
|
|
export type WatchPatchCallback<Root = any> = (
|
|
event: WatchPatchEvent<Root>
|
|
) => any;
|
|
|
|
// Internal helper kept for external compatibility.
|
|
export const remove = <T>(arr: T[], el: T): void => {
|
|
const i = arr.indexOf(el);
|
|
if (i > -1) arr.splice(i, 1);
|
|
};
|
|
|
|
/** Observe patch batches on a deep signal root. */
|
|
export function watch<Root = any>(
|
|
source: Root,
|
|
callback: WatchPatchCallback<Root>,
|
|
options: WatchOptions = {}
|
|
) {
|
|
if (!isDeepSignal(source)) {
|
|
throw new Error(
|
|
"watch() now only supports deepSignal roots (patch mode only)"
|
|
);
|
|
}
|
|
const { immediate, once } = options;
|
|
|
|
const rootId = getDeepSignalRootId(source as any)!;
|
|
|
|
let active = true;
|
|
let cleanup: (() => void) | undefined;
|
|
const registerCleanup: RegisterCleanup = (fn) => {
|
|
cleanup = fn;
|
|
};
|
|
const runCleanup = () => {
|
|
if (cleanup) {
|
|
try {
|
|
cleanup();
|
|
} catch {
|
|
/* ignore */
|
|
} finally {
|
|
cleanup = undefined;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Deep clone snapshot helper (JSON clone sufficient for typical reactive plain data)
|
|
const clone = (v: any) => {
|
|
try {
|
|
return JSON.parse(JSON.stringify(v));
|
|
} catch {
|
|
return undefined as any;
|
|
}
|
|
};
|
|
let lastSnapshot: Root | undefined = clone(source);
|
|
|
|
const stopListening = () => {
|
|
if (!active) return;
|
|
active = false;
|
|
runCleanup();
|
|
unsubscribe && unsubscribe();
|
|
};
|
|
|
|
const deliver = (patches: DeepPatch[]) => {
|
|
if (!active) return;
|
|
runCleanup();
|
|
const prev = lastSnapshot;
|
|
const next = source as any as Root; // live proxy
|
|
try {
|
|
callback({
|
|
patches,
|
|
oldValue: prev,
|
|
newValue: next,
|
|
});
|
|
} finally {
|
|
if (active) lastSnapshot = clone(next);
|
|
if (once) stopListening();
|
|
}
|
|
};
|
|
|
|
const unsubscribe = subscribeDeepMutations(rootId, (patches) => {
|
|
if (!patches.length) return; // ignore empty batches
|
|
deliver(patches);
|
|
});
|
|
|
|
if (immediate) {
|
|
// Immediate call with empty patch list (snapshot only)
|
|
deliver([]);
|
|
}
|
|
|
|
return {
|
|
/** Stop listening to future patch batches; idempotent. */
|
|
stopListening,
|
|
/** Register a cleanup callback run before the next invocation / stop. */
|
|
registerCleanup,
|
|
};
|
|
}
|
|
|
|
// observe alias
|
|
export function observe(
|
|
source: any,
|
|
cb: WatchPatchCallback,
|
|
options?: WatchOptions
|
|
) {
|
|
return watch(source, cb, options);
|
|
}
|
|
|
|
// Instrumentation counter for performance tests (number of traverse invocations)
|
|
/** Instrumentation counter tracking total `traverse()` invocations (used in tests). */
|
|
export let __traverseCount = 0; // retained for external tooling/tests although watch no longer uses traversal
|
|
/** Reset the traversal instrumentation counter back to 0. */
|
|
export function __resetTraverseCount() {
|
|
__traverseCount = 0;
|
|
}
|
|
|
|
/**
|
|
* Recursively touch (read) nested properties/entries/values of a reactive structure for dependency collection.
|
|
* Depth-limited; protects against cycles via `seen` set; respects ReactiveFlags.SKIP opt-out.
|
|
*/
|
|
export function traverse(
|
|
value: unknown,
|
|
depth: number = Infinity,
|
|
seen?: Set<unknown>
|
|
): unknown {
|
|
__traverseCount++;
|
|
if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
|
|
return value;
|
|
}
|
|
|
|
seen = seen || new Set();
|
|
if (seen.has(value)) {
|
|
return value;
|
|
}
|
|
seen.add(value);
|
|
depth--;
|
|
if (isSignal(value)) {
|
|
traverse((value as any)(), depth, seen);
|
|
} else if (isArray(value)) {
|
|
for (let i = 0; i < value.length; i++) {
|
|
traverse(value[i], depth, seen);
|
|
}
|
|
} else if (isSet(value) || isMap(value)) {
|
|
value.forEach((v: any) => {
|
|
traverse(v, depth, seen);
|
|
});
|
|
} else if (isPlainObject(value)) {
|
|
for (const key in value) {
|
|
traverse(value[key], depth, seen);
|
|
}
|
|
for (const key of Object.getOwnPropertySymbols(value)) {
|
|
if (Object.prototype.propertyIsEnumerable.call(value, key)) {
|
|
traverse(value[key as any], depth, seen);
|
|
}
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|