Evaluation of signal-libraries and their integration in frontend-frameworks with nested objects using js proxies.
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.
 
 
 
 
 
multi-framework-signals/src/ng-mock/js-land/connector/createSignalObjectForShape.ts

124 lines
3.7 KiB

import updateShape from "src/ng-mock/wasm-land/updateShape";
import type { Connection, Diff, Scope, Shape } from "../types";
import requestShape from "src/ng-mock/wasm-land/requestShape";
import { applyDiff } from "./applyDiff";
import { deepSignal, watch, batch } from "alien-deepsignals";
import type { DeepPatch, DeepSignalObject } from "alien-deepsignals";
import type { CompactShapeType } from "node_modules/@ldo/ldo/dist/types/ShapeType";
import type { LdoCompactBase } from "@ldo/ldo";
interface PoolEntry<T extends LdoCompactBase> {
key: string;
shape: CompactShapeType<T>;
scopeKey: string;
signalObject: DeepSignalObject<T | {}>;
refCount: number;
stopListening: (() => void) | null;
registerCleanup?: (fn: () => void) => void;
connectionId?: string;
ready: Promise<string>; // resolves to connectionId
resolveReady: (id: string) => void;
suspendDeepWatcher: boolean;
}
const pool = new Map<string, PoolEntry<any>>();
function canonicalScope(scope: Scope | undefined): string {
if (scope == null) return "";
return Array.isArray(scope) ? scope.slice().sort().join(",") : String(scope);
}
export function deepPatchesToDiff(patches: DeepPatch[]): Diff {
return patches.map((patch) => {
const path = "/" + patch.path.join("/");
return { ...patch, path };
}) as Diff;
}
export function createSignalObjectForShape<T extends LdoCompactBase>(
shape: CompactShapeType<T>,
scope?: Scope,
poolSignal = true
) {
const scopeKey = canonicalScope(scope);
const key = `${shape}::${scopeKey}`;
if (poolSignal) {
const existing = pool.get(key);
if (existing) {
existing.refCount++;
return buildReturn(existing);
}
}
const signalObject = deepSignal<T | {}>({});
let resolveReady!: (id: string) => void;
const ready = new Promise<string>((res) => (resolveReady = res));
const entry: PoolEntry<T> = {
key,
shape,
scopeKey,
signalObject,
refCount: 1,
stopListening: null,
registerCleanup: undefined,
connectionId: undefined,
ready,
resolveReady,
suspendDeepWatcher: false,
};
if (poolSignal) pool.set(key, entry);
const onUpdateFromDb = (diff: Diff, connectionId: Connection["id"]) => {
console.debug("[shape][diff] applying", connectionId, diff);
entry.suspendDeepWatcher = true;
batch(() => applyDiff(signalObject, diff));
queueMicrotask(() => {
entry.suspendDeepWatcher = false;
});
};
requestShape(shape, scope, onUpdateFromDb).then(
({ connectionId, shapeObject }) => {
entry.connectionId = connectionId;
entry.suspendDeepWatcher = true;
batch(() => {
for (const k of Object.keys(shapeObject)) {
(signalObject as any)[k] = (shapeObject as any)[k];
}
});
const watcher = watch(signalObject, ({ patches }) => {
if (entry.suspendDeepWatcher || !patches.length) return;
const diff = deepPatchesToDiff(patches);
updateShape(connectionId as any, diff as any);
});
entry.stopListening = watcher.stopListening;
entry.registerCleanup = watcher.registerCleanup;
queueMicrotask(() => {
entry.suspendDeepWatcher = false;
});
entry.resolveReady(connectionId);
}
);
function buildReturn(entry: PoolEntry<T>) {
const release = () => {
if (entry.refCount > 0) entry.refCount--;
if (entry.refCount === 0) {
entry.stopListening?.();
if (poolSignal) pool.delete(entry.key);
}
};
return {
signalObject: entry.signalObject,
stop: release,
ready: entry.ready, // Promise<string>
get connectionId() {
return entry.connectionId;
},
registerCleanup: entry.registerCleanup,
};
}
return buildReturn(entry);
}