Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
38 changes: 35 additions & 3 deletions apps/docs/src/content/docs/api/Types/EnvironmentInfo.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,46 @@ Interface representing environment information

### browser

> **browser**: `"Other"` \| `"Chrome"` \| `"Firefox"` \| `"Safari"` \| `"Edge"` \| `"Opera"` \| `"IE"`
> **browser**: `null` \| `LooseAutocomplete`\<`"Chrome"` \| `"Firefox"` \| `"Safari"` \| `"Edge"` \| `"Opera"` \| `"IE"` \| `"Other"`\>

User Browser name
User Browser name (when applicable)

***

### generateId()

> **generateId**: () => `string`

Generates a unique ID

#### Returns

`string`

***

### now()

> **now**: () => `number`

Current timestamp in ms

#### Returns

`number`

***

### os

> **os**: `"MacOS"` \| `"Windows"` \| `"Linux"` \| `"iOS"` \| `"Android"` \| `"Other"`
> **os**: `null` \| `LooseAutocomplete`\<`"MacOS"` \| `"Windows"` \| `"Linux"` \| `"iOS"` \| `"Android"` \| `"Unknown"`\>

User Operating system name

***

### runtime

> **runtime**: `null` \| `LooseAutocomplete`\<`"node"` \| `"web"` \| `"other"`\>

Platform identity for high-level adapter routing
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FlowCore } from '../flow-core';
import type { Edge } from '../types/edge.interface';
import type {
BackgroundConfig,
Expand All @@ -12,18 +13,11 @@ import type {
ZIndexConfig,
ZoomConfig,
} from '../types/flow-config.interface';
import { Point, Size } from '../types/utils';
import { DeepPartial, Point, Size } from '../types/utils';
import { deepMerge } from '../utils';

export const DEFAULT_NODE_MIN_SIZE = { width: 20, height: 20 };

const defaultComputeNodeId = (): string => {
return crypto.randomUUID();
};

const defaultComputeEdgeId = (): string => {
return crypto.randomUUID();
};

const defaultResizeConfig: ResizeConfig = {
getMinNodeSize: (): Size => {
return { ...DEFAULT_NODE_MIN_SIZE };
Expand Down Expand Up @@ -120,18 +114,22 @@ const defaultZIndexConfig: ZIndexConfig = {
/**
* Default configuration for the flow system.
*/
export const defaultFlowConfig: FlowConfig = {
computeNodeId: defaultComputeNodeId,
computeEdgeId: defaultComputeEdgeId,
resize: defaultResizeConfig,
linking: defaultLinkingConfig,
grouping: defaultGroupingConfig,
zoom: defaultZoomConfig,
background: defaultBackgroundConfig,
nodeRotation: defaultNodeRotationConfig,
snapping: defaultNodeDraggingConfig,
selectionMoving: defaultSelectionMovingConfig,
edgeRouting: defaultEdgeRoutingConfig,
zIndex: defaultZIndexConfig,
debugMode: false,
};
export const createFlowConfig = (config: DeepPartial<FlowConfig>, flowCore: FlowCore): FlowConfig =>
deepMerge(
{
computeNodeId: () => flowCore.environment.generateId(),
computeEdgeId: () => flowCore.environment.generateId(),
resize: defaultResizeConfig,
linking: defaultLinkingConfig,
grouping: defaultGroupingConfig,
zoom: defaultZoomConfig,
background: defaultBackgroundConfig,
nodeRotation: defaultNodeRotationConfig,
snapping: defaultNodeDraggingConfig,
selectionMoving: defaultSelectionMovingConfig,
edgeRouting: defaultEdgeRoutingConfig,
zIndex: defaultZIndexConfig,
debugMode: false,
},
config
);
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
import { CommandHandler } from './command-handler/command-handler';
import { FlowCore } from './flow-core';
import { InputEventsRouter } from './input-events';
import type { InputEventsRouter } from './input-events';
import { MiddlewareManager } from './middleware-manager/middleware-manager';
import { mockEdge, mockMetadata, mockNode } from './test-utils';
import { Edge } from './types/edge.interface';
import type { EnvironmentInfo } from './types/environment.interface';
import { mockEdge, mockEnvironment, mockMetadata, mockNode } from './test-utils';
import type { Edge } from './types/edge.interface';
import type { FlowConfig } from './types/flow-config.interface';
import type { Metadata } from './types/metadata.interface';
import type { Middleware } from './types/middleware.interface';
Expand Down Expand Up @@ -77,7 +76,6 @@ describe('FlowCore', () => {
let mockGetEdges: Mock<() => Edge[]>;
let mockGetMetadata: Mock<() => Metadata>;
let mockEventRouter: InputEventsRouter;
const mockEnvironment: EnvironmentInfo = { os: 'MacOS', browser: 'Chrome' };

beforeEach(() => {
mockGetNodes = vi.fn().mockReturnValue([]);
Expand Down Expand Up @@ -260,7 +258,9 @@ describe('FlowCore', () => {

await flowCore.applyUpdate({ nodesToUpdate: [mockNode] }, 'changeSelection');

expect(mockModelAdapter.updateMetadata).toHaveBeenCalledWith({ test: 'abc' });
expect(mockModelAdapter.updateMetadata).toHaveBeenCalledWith({
test: 'abc',
});
expect(mockModelAdapter.updateNodes).toHaveBeenCalledWith([mockNode]);
expect(mockModelAdapter.updateEdges).toHaveBeenCalledWith([mockEdge]);
});
Expand Down Expand Up @@ -316,7 +316,10 @@ describe('FlowCore', () => {

describe('clientToFlowPosition', () => {
it('should convert client position to flow position', () => {
mockGetMetadata.mockReturnValue({ ...mockMetadata, viewport: { x: 200, y: 200, scale: 2 } });
mockGetMetadata.mockReturnValue({
...mockMetadata,
viewport: { x: 200, y: 200, scale: 2 },
});
const clientPosition = { x: 30, y: 30 };
const flowPosition = flowCore.clientToFlowPosition(clientPosition);

Expand All @@ -334,7 +337,10 @@ describe('FlowCore', () => {
customGetFlowOffset
);

mockGetMetadata.mockReturnValue({ ...mockMetadata, viewport: { x: 200, y: 200, scale: 2 } });
mockGetMetadata.mockReturnValue({
...mockMetadata,
viewport: { x: 200, y: 200, scale: 2 },
});
const clientPosition = { x: 30, y: 30 };
const flowPosition = flowCore.clientToFlowPosition(clientPosition);

Expand All @@ -345,7 +351,10 @@ describe('FlowCore', () => {

describe('flowToClientPosition', () => {
it('should convert flow position to client position', () => {
mockGetMetadata.mockReturnValue({ ...mockMetadata, viewport: { x: 200, y: 200, scale: 2 } });
mockGetMetadata.mockReturnValue({
...mockMetadata,
viewport: { x: 200, y: 200, scale: 2 },
});
const flowPosition = { x: -85, y: -85 };
const clientPosition = flowCore.flowToClientPosition(flowPosition);

Expand All @@ -363,7 +372,10 @@ describe('FlowCore', () => {
customGetFlowOffset
);

mockGetMetadata.mockReturnValue({ ...mockMetadata, viewport: { x: 200, y: 200, scale: 2 } });
mockGetMetadata.mockReturnValue({
...mockMetadata,
viewport: { x: 200, y: 200, scale: 2 },
});
const flowPosition = { x: -110, y: -135 };
const clientPosition = flowCore.flowToClientPosition(flowPosition);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ActionStateManager } from './action-state-manager/action-state-manager'
import { CommandHandler } from './command-handler/command-handler';
import { EdgeRoutingManager } from './edge-routing-manager';
import { EventManager } from './event-manager';
import { defaultFlowConfig } from './flow-config/default-flow-config';
import { createFlowConfig } from './flow-config/default-flow-config';
import { InputEventsRouter } from './input-events';
import { LabelBatchProcessor } from './label-batch-processor/label-batch-processor';
import { MiddlewareManager } from './middleware-manager/middleware-manager';
Expand Down Expand Up @@ -45,7 +45,6 @@ export class FlowCore {

readonly commandHandler: CommandHandler;
readonly middlewareManager: MiddlewareManager;
readonly environment: EnvironmentInfo;
readonly spatialHash: SpatialHash;
readonly modelLookup: ModelLookup;
readonly transactionManager: TransactionManager;
Expand All @@ -61,13 +60,13 @@ export class FlowCore {
modelAdapter: ModelAdapter,
private readonly renderer: Renderer,
public readonly inputEventsRouter: InputEventsRouter,
environment: EnvironmentInfo,
public readonly environment: EnvironmentInfo,
middlewares?: MiddlewareChain,
getFlowOffset?: () => Point,
config: DeepPartial<FlowConfig> = {}
) {
this._model = modelAdapter;
this._config = deepMerge(defaultFlowConfig, config);
this._config = createFlowConfig(config, this);
this.environment = environment;
this.commandHandler = new CommandHandler(this);
this.spatialHash = new SpatialHash();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export class MiddlewareExecutor {
history: this.history,
initialUpdate: this.initialStateUpdate,
config: this.flowCore.config,
environment: this.flowCore.environment,
});

private resolveMiddlewares = (): Promise<FlowState | undefined> => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,13 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { mockEnvironment } from '../../../test-utils';
import type { MiddlewareContext } from '../../../types';
import { internalIdMiddleware } from './internal-id-assignment';

let originalCrypto: Crypto | undefined;

describe('InternalIdMiddleware', () => {
let context: MiddlewareContext;
let nextMock: ReturnType<typeof vi.fn>;

beforeEach(() => {
// Ensure crypto.randomUUID exists in test environment
originalCrypto = globalThis.crypto as Crypto | undefined;
if (!originalCrypto || typeof originalCrypto.randomUUID !== 'function') {
// @ts-expect-error Partial stub for tests
globalThis.crypto = {
// Return a valid RFC4122 v4 UUID string
randomUUID: vi.fn().mockReturnValue('550e8400-e29b-41d4-a716-446655440000'),
} as Crypto;
}

nextMock = vi.fn();

context = {
Expand Down Expand Up @@ -47,15 +36,15 @@ describe('InternalIdMiddleware', () => {
getAffectedNodeIds: vi.fn().mockReturnValue([]),
getAffectedEdgeIds: vi.fn().mockReturnValue([]),
},
environment: {
...mockEnvironment,
generateId: vi.fn().mockReturnValue('550e8400-e29b-41d4-a716-446655440000'),
},
} as unknown as MiddlewareContext;
});

afterEach(() => {
// Restore original crypto if we replaced it
if (originalCrypto) {
globalThis.crypto = originalCrypto;
originalCrypto = undefined;
}
vi.restoreAllMocks();
});

it('should not modify state when no nodes are added', async () => {
Expand Down Expand Up @@ -131,7 +120,7 @@ describe('InternalIdMiddleware', () => {

// Mock different UUIDs for sequential calls
const randomUUIDMock = vi
.spyOn(globalThis.crypto!, 'randomUUID')
.spyOn(context.environment, 'generateId')
.mockReturnValueOnce('550e8400-e29b-41d4-a716-446655440000')
.mockReturnValueOnce('6ba7b810-9dad-11d1-80b4-00c04fd430c8');

Expand Down Expand Up @@ -199,7 +188,7 @@ describe('InternalIdMiddleware', () => {

// Use a predictable UUID so the regex is deterministic
const randomUUIDMock = vi
.spyOn(globalThis.crypto!, 'randomUUID')
.spyOn(context.environment, 'generateId')
.mockReturnValue('550e8400-e29b-41d4-a716-446655440000');

await internalIdMiddleware.execute(context, nextMock, () => null);
Expand Down Expand Up @@ -275,7 +264,7 @@ describe('InternalIdMiddleware', () => {

// Mock different UUIDs
const randomUUIDMock = vi
.spyOn(globalThis.crypto!, 'randomUUID')
.spyOn(context.environment, 'generateId')
.mockReturnValueOnce('550e8400-e29b-41d4-a716-446655440000')
.mockReturnValueOnce('6ba7b810-9dad-11d1-80b4-00c04fd430c8');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { FlowStateUpdate, Middleware } from '../../../types';
export const internalIdMiddleware: Middleware = {
name: 'internal-id-assignment',
execute: async (context, next) => {
const { helpers, initialUpdate } = context;
const { helpers, initialUpdate, environment } = context;

if (!helpers.anyNodesAdded()) {
next();
Expand All @@ -22,7 +22,7 @@ export const internalIdMiddleware: Middleware = {
...node,
_internalId:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(node as any)._internalId || `${node.id}-${crypto.randomUUID()}`,
(node as any)._internalId || `${node.id}-${environment.generateId()}`,
}));

const stateUpdate: FlowStateUpdate = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { vi } from 'vitest';
import type { Edge, EdgeLabel, EnvironmentInfo, GroupNode, Metadata, Node, Port } from './types';

export const mockNode: Node = {
Expand Down Expand Up @@ -39,6 +40,9 @@ export const mockMetadata: Metadata = {
export const mockEnvironment: EnvironmentInfo = {
os: 'MacOS',
browser: 'Chrome',
runtime: 'web',
now: vi.fn(),
generateId: vi.fn(),
};

export const mockPort: Port = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import type { LooseAutocomplete } from './utils';

/**
* Interface representing environment information
*
* @category Types
*/
export interface EnvironmentInfo {
/**
* User Operating system name
*/
os: 'MacOS' | 'Windows' | 'Linux' | 'iOS' | 'Android' | 'Other';
/**
* User Browser name
*/
browser: 'Chrome' | 'Firefox' | 'Safari' | 'Edge' | 'Opera' | 'IE' | 'Other';
/** User Operating system name */
os: LooseAutocomplete<'MacOS' | 'Windows' | 'Linux' | 'iOS' | 'Android' | 'Unknown'> | null;
/** User Browser name (when applicable) */
browser: LooseAutocomplete<'Chrome' | 'Firefox' | 'Safari' | 'Edge' | 'Opera' | 'IE' | 'Other'> | null;
/** Platform identity for high-level adapter routing */
runtime: LooseAutocomplete<'web' | 'node' | 'other'> | null;
/** Current timestamp in ms */
now: () => number;
/** Generates a unique ID */
generateId: () => string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ActionStateManager } from '../action-state-manager/action-state-ma
import type { EdgeRoutingManager } from '../edge-routing-manager';
import type { MiddlewareExecutor } from '../middleware-manager/middleware-executor';
import type { Edge } from './edge.interface';
import { EnvironmentInfo } from './environment.interface';
import { FlowConfig } from './flow-config.interface';
import type { Metadata } from './metadata.interface';
import type { Node } from './node.interface';
Expand Down Expand Up @@ -101,6 +102,8 @@ export interface MiddlewareContext {
initialUpdate: FlowStateUpdate;
/** The configuration for the flow diagram */
config: FlowConfig;
/** The environment information */
environment: EnvironmentInfo;
}

/**
Expand Down
Loading