Skip to content
Merged

Test #151

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to this project are documented here.

## 2.2.1a0 (2026-01-07)

### Fix

- **funcnodes-react-flow**: clean up queued fnw_url import callback
- enhance worker lifecycle management by refining conditions for worker creation

## 2.2.0 (2026-01-07)

### Feat
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "funcnodes-react-flow"
version = "2.2.0"
version = "2.2.1a0"
description = "funcnodes frontend for react flow"
readme = "README.md"
classifiers = [ "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",]
Expand Down
10,471 changes: 5,243 additions & 5,228 deletions src/funcnodes_react_flow/static/funcnodes_react_flow.es.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/funcnodes_react_flow/static/funcnodes_react_flow.iife.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "funcnodes-react-flow-monorepo",
"version": "2.2.0",
"version": "2.2.1a0",
"private": true,
"packageManager": "yarn@4.9.2",
"workspaces": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@linkdlab/funcnodes-react-flow-plugin",
"version": "2.2.0",
"version": "2.2.1a0",
"type": "module",
"description": "Type definitions for FuncNodes React Flow plugins",
"repository": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2024,10 +2024,13 @@ declare class WorkerSyncManager extends AbstractWorkerHandler {
_local_nodeupdates: Map<string, PartialSerializedNodeType>;
_local_groupupdates: Map<string, Partial<NodeGroup>>;
_groupupdatetimer: ReturnType<typeof setTimeout> | undefined;
_after_next_sync: ((worker: FuncNodesWorker) => Promise<void>)[];
constructor(context: WorkerSyncManagerContext);
start(): void;
stop(): void;
stepwise_fullsync(): Promise<void>;
add_after_next_sync(callback: (worker: FuncNodesWorker) => Promise<void>): void;
remove_after_next_sync(callback: (worker: FuncNodesWorker) => Promise<void>): void;
sync_lib(): Promise<void>;
sync_external_worker(): Promise<void>;
sync_funcnodes_plugins(): Promise<void>;
Expand Down
2 changes: 1 addition & 1 deletion src/react/packages/funcnodes-react-flow/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@linkdlab/funcnodes_react_flow",
"version": "2.2.0",
"version": "2.2.1a0",
"description": "Frontend with React Flow for FuncNodes",
"repository": {
"type": "git",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, it, expect, vi } from "vitest";
import * as React from "react";
import { act, render, waitFor } from "@testing-library/react";

import { FuncNodes } from "@/app";
import { FuncNodesWorker } from "@/workers";

vi.mock("@/data-helpers", () => ({
remoteUrlToBase64: vi.fn(async () => "base64:dummy"),
}));

import { remoteUrlToBase64 } from "@/data-helpers";

describe("FuncNodes (fnw_url cleanup)", () => {
it("removes the queued fnw_url import callback on unmount", async () => {
const worker = new FuncNodesWorker({ uuid: "worker-1" });

const { unmount } = render(
<div style={{ height: 600, width: 800 }}>
<FuncNodes
id="test"
useWorkerManager={false}
worker={worker}
fnw_url="https://example.invalid/test.fnw"
header={{ show: false, showmenu: false }}
library={{ show: false }}
flow={{
minimap: false,
static: true,
minZoom: 0.1,
maxZoom: 2,
allowFullScreen: false,
allowExpand: false,
showNodeSettings: false,
}}
/>
</div>
);

const syncManager: any = worker.getSyncManager();

await waitFor(() => {
expect(remoteUrlToBase64).toHaveBeenCalledTimes(1);
});

await waitFor(() => {
expect(syncManager._after_next_sync?.length).toBe(1);
});

act(() => unmount());

expect(syncManager._after_next_sync?.length).toBe(0);

worker.getSyncManager().stop();
worker.getConnectionHealthManager().stop();
worker.getCommunicationManager().stop();
});
});
42 changes: 20 additions & 22 deletions src/react/packages/funcnodes-react-flow/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,17 @@ export const FuncNodes = (
// Effect 3: Manage Worker lifecycle
React.useEffect(() => {
if (!fullProps || !fnrfzst) return;
// Skip if using worker manager or no worker URL provided
if (fullProps.useWorkerManager || !fullProps.worker_url) return;
// Skip if: a) a worker manager is used or b) no worker URL or worker is provided
if (
fullProps.useWorkerManager || // a) a worker manager is used
(!fullProps.worker_url && !fullProps.worker) // b) no worker URL and no worker is provided
)
return;

fullProps.logger?.debug("Worker effect running");

// Check if we need to create a worker
if (!fullProps.worker) {
if (!fullProps.worker && fullProps.worker_url) {
fullProps.logger?.debug("Creating WebSocket worker");

const worker = new WebSocketWorker({
Expand All @@ -129,7 +133,7 @@ export const FuncNodes = (
};
} else {
// Worker already exists, just ensure zustand is set
fullProps.worker.set_zustand(fnrfzst);
fullProps.worker?.set_zustand(fnrfzst);
return; // Explicit return for consistency
}
}, [
Expand All @@ -147,25 +151,20 @@ export const FuncNodes = (
fullProps.logger?.debug("Loading fnw_url data");

let cancelled = false;
const originalOnSyncComplete =
fullProps.worker.getSyncManager().on_sync_complete;
const syncManager = fullProps.worker.getSyncManager();
let afterNextSyncCallback:
| ((worker: FuncNodesWorker) => Promise<void>)
| undefined;

const loadFnwData = async () => {
try {
const fnw_data = await remoteUrlToBase64(fullProps.fnw_url!);

if (!cancelled && fullProps.worker) {
fullProps.worker.getSyncManager().on_sync_complete = async (
worker: FuncNodesWorker
) => {
await worker.update_from_export(fnw_data);
fullProps.worker!.getSyncManager().on_sync_complete =
originalOnSyncComplete;
if (originalOnSyncComplete) {
originalOnSyncComplete(worker);
}
};
}
if (cancelled) return;
afterNextSyncCallback = async (worker: FuncNodesWorker) => {
if (cancelled) return;
await worker.update_from_export(fnw_data);
};
syncManager.add_after_next_sync(afterNextSyncCallback);
} catch (error) {
if (error instanceof Error) {
fullProps.logger?.error("Failed to load fnw_url:", error);
Expand All @@ -182,9 +181,8 @@ export const FuncNodes = (

return () => {
cancelled = true;
if (fullProps.worker) {
fullProps.worker.getSyncManager().on_sync_complete =
originalOnSyncComplete;
if (afterNextSyncCallback) {
syncManager.remove_after_next_sync(afterNextSyncCallback);
}
};
}, [fullProps?.fnw_url, fullProps?.worker]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class WorkerSyncManager extends AbstractWorkerHandler {
_local_nodeupdates: Map<string, PartialSerializedNodeType> = new Map();
_local_groupupdates: Map<string, Partial<NodeGroup>> = new Map();
_groupupdatetimer: ReturnType<typeof setTimeout> | undefined;
_after_next_sync: ((worker: FuncNodesWorker) => Promise<void>)[] = [];
constructor(context: WorkerSyncManagerContext) {
super(context);
this.on_sync_complete = context.on_sync_complete || (async () => {});
Expand Down Expand Up @@ -69,6 +70,22 @@ export class WorkerSyncManager extends AbstractWorkerHandler {
await this.sync_view_state();

await this.on_sync_complete(this.context.worker);
const callbacks = this._after_next_sync.splice(0);
for (const after_next_sync of callbacks) {
await after_next_sync(this.context.worker);
if (this._after_next_sync.includes(after_next_sync)) {
this._after_next_sync.splice(this._after_next_sync.indexOf(after_next_sync), 1);
}
}
}

add_after_next_sync(callback: (worker: FuncNodesWorker) => Promise<void>) {
this._after_next_sync.push(callback);
}
remove_after_next_sync(callback: (worker: FuncNodesWorker) => Promise<void>) {
this._after_next_sync = this._after_next_sync.filter(
(c) => c !== callback
);
}

async sync_lib() {
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.