Repository: retejs/rete
Branch: main
Commit: 2aae19950180
Files: 32
Total size: 45.2 KB
Directory structure:
gitextract_whez86aj/
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ ├── build-push.yml
│ ├── ci.yml
│ ├── codeql.yml
│ ├── commit-linter.yml
│ ├── release.yml
│ ├── stale.yml
│ └── update-docs.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── eslint.config.mjs
├── package.json
├── postinstall.js
├── rete.config.ts
├── src/
│ ├── editor.ts
│ ├── index.ts
│ ├── presets/
│ │ └── classic.ts
│ ├── scope.ts
│ ├── types.ts
│ ├── utility-types.ts
│ └── utils.ts
├── test/
│ ├── index.perf.ts
│ ├── index.test.ts
│ ├── mocks/
│ │ └── crypto.ts
│ ├── presets/
│ │ └── classic.test.ts
│ ├── scope.test.ts
│ └── utils.test.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
patreon: ni55an
open_collective: rete
================================================
FILE: .github/workflows/build-push.yml
================================================
name: Build and Push
run-name: Build and Push to dist/${{ github.ref_name }}
on:
workflow_dispatch:
jobs:
push:
uses: retejs/.github/.github/workflows/build-push.yml@main
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
workflow_dispatch:
pull_request:
branches: [ "main", "beta" ]
jobs:
ci:
uses: retejs/.github/.github/workflows/ci.yml@main
secrets: inherit
================================================
FILE: .github/workflows/codeql.yml
================================================
name: CodeQL
on:
workflow_dispatch:
push:
branches: [ "main", "beta" ]
pull_request:
branches: [ "main", "beta" ]
jobs:
codeql:
uses: retejs/.github/.github/workflows/codeql.yml@main
================================================
FILE: .github/workflows/commit-linter.yml
================================================
name: Commit linter
on:
pull_request:
branches: [ "main", "beta" ]
jobs:
lint:
uses: retejs/.github/.github/workflows/commit-linter.yml@main
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
workflow_dispatch:
push:
branches: [ "main", "beta" ]
jobs:
release:
uses: retejs/.github/.github/workflows/release.yml@main
secrets: inherit
================================================
FILE: .github/workflows/stale.yml
================================================
name: Close stale issues and PRs
on:
workflow_dispatch:
schedule:
- cron: '30 1 * * 0'
jobs:
stale:
uses: retejs/.github/.github/workflows/stale.yml@main
secrets: inherit
================================================
FILE: .github/workflows/update-docs.yml
================================================
name: Update docs
on:
workflow_dispatch:
push:
branches: [ "main" ]
jobs:
pull:
uses: retejs/.github/.github/workflows/update-docs.yml@main
secrets: inherit
with:
filename: '1.rete'
package: rete
================================================
FILE: .gitignore
================================================
node_modules
.idea/
.vscode/
.sonarlint
.scannerwork
.nyc_output
/coverage
npm-debug.log
/dist
docs
.rete-cli
.sonar
================================================
FILE: CHANGELOG.md
================================================
## [2.0.6](https://github.com/retejs/rete/compare/v2.0.5...v2.0.6) (2025-06-30)
### Bug Fixes
* improve concurrent behavior of removeNode and removeConnection ([4229f1f](https://github.com/retejs/rete/commit/4229f1f0772a581bce7174080fb05870109bdd62))
* optimize node and connection removal logic ([0696cba](https://github.com/retejs/rete/commit/0696cbaa1a1b3fd2a14cc6dd0312a783300d0fe7))
## [2.0.5](https://github.com/retejs/rete/compare/v2.0.4...v2.0.5) (2024-08-30)
### Bug Fixes
* build ([1f852d9](https://github.com/retejs/rete/commit/1f852d9e491522264d97de396a30d5f0faf2a681))
## [2.0.4](https://github.com/retejs/rete/compare/v2.0.3...v2.0.4) (2024-08-30)
### Bug Fixes
* update cli and fix linting errors ([d219f95](https://github.com/retejs/rete/commit/d219f95cb0d46f79e8d7f5d70e4afcd578f35455))
## [2.0.3](https://github.com/retejs/rete/compare/v2.0.2...v2.0.3) (2024-01-27)
### Bug Fixes
* **build:** source maps ([121775c](https://github.com/retejs/rete/commit/121775c90aac1db449b30284ba996eed1da1a03c))
## [2.0.2](https://github.com/retejs/rete/compare/v2.0.1...v2.0.2) (2023-07-24)
### Bug Fixes
* **editor:** return copy of array in getNodes/getConnections ([369e85e](https://github.com/retejs/rete/commit/369e85e5d661cca5e9de86326c2245c0e2f38d5b))
## v2.0.0-beta.8
Improve Scope typing: validate signals in `use` method, infer return type in `emit` method
================================================
FILE: CODE_OF_CONDUCT.md
================================================
Check out the [Code of Conduct](https://retejs.org/docs/code-of-conduct)
================================================
FILE: CONTRIBUTING.md
================================================
Check out the [Contribution guide](https://retejs.org/docs/contribution)
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 "Ni55aN" Vitaliy Stoliarov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
Rete.js
====
[](https://stand-with-ukraine.pp.ua)
[](https://discord.gg/cxSFkPZdsV)
**JavaScript framework for visual programming**

#StandWithUkraine 💙💛
----
#RussiaInvadedUkraine on 24 of February 2022, at 5.00 AM the armed forces of the Russian Federation attacked Ukraine. Please, Stand with Ukraine, stay tuned for updates on Ukraine’s official sources and channels in English and support Ukraine in its fight for freedom and democracy in Europe.
Help to defend Ukraine — donate to [Ukraine’s main charity fund](https://savelife.in.ua/en/donate/)
Help to defend Ukraine — donate to the [fund of the National Bank of Ukraine](https://ukraine.ua/news/donate-to-the-nbu-fund/)
Introduction [🎥](https://youtu.be/xqPLa6P194A)
----
**Rete.js** is a framework for creating visual interfaces and workflows. It provides out-of-the-box solutions for visualization using various libraries and frameworks, as well as solutions for processing graphs based on dataflow and control flow approaches.
Getting started
----
Use [Rete Kit](https://retejs.org/docs/development/rete-kit) to quickly set up a Rete.js application. It lets you select a stack (React.js, Vue.js or Angular, Svelte) and the set of features
```bash
npx rete-kit app
```
Alternatively, you can follow the [complete guide](https://retejs.org/docs/getting-started/)
Documentation
----
- [Introduction](https://retejs.org/docs)
- [Guides](https://retejs.org/docs/guides/basic)
- [Examples](https://retejs.org/examples)
## Sponsors
Thank you to all our sponsors! [Become a sponsor](https://opencollective.com/rete#sponsor)
## Backers
Thank you to all our backers! [Become a backer](https://opencollective.com/rete#backer)
## Contributors
This project exists thanks to all the people who contribute. [Contribute](https://retejs.org/docs/contribution).
## License
[MIT](https://github.com/retejs/rete/blob/main/LICENSE)
================================================
FILE: eslint.config.mjs
================================================
import tseslint from 'typescript-eslint';
import configs from 'rete-cli/configs/eslint.mjs';
export default tseslint.config(
...configs
)
================================================
FILE: package.json
================================================
{
"name": "rete",
"version": "2.0.6",
"description": "JavaScript framework",
"scripts": {
"build": "rete build -c rete.config.ts",
"postinstall": "node postinstall.js",
"doc": "rete doc",
"lint": "rete lint",
"test": "rete test",
"perf": "rete perf"
},
"author": "Vitaliy Stoliarov",
"license": "MIT",
"keywords": [
"dataflow",
"visual programming",
"node editor",
"rete",
"Rete.js"
],
"homepage": "https://retejs.org",
"repository": {
"type": "git",
"url": "https://github.com/retejs/rete.git"
},
"bugs": {
"url": "https://github.com/retejs/rete/issues"
},
"devDependencies": {
"jest-environment-jsdom": "^29.1.2",
"rete-cli": "^2.1.0"
},
"dependencies": {
"@babel/runtime": "^7.21.0"
}
}
================================================
FILE: postinstall.js
================================================
function getRectangle(width, height, color) {
const line = new Array(width).fill(' ').join('')
return new Array(height).fill(color(line)).join('\n')
}
function drawText(x, y, text) {
const save = '\033[s'
const restore = '\033[u'
const up = n => '\033['+n+'A'
const right = n => '\033['+n+'C'
return `${save}${up(y)}${right(x)}${text}${restore}`
}
function black(text) {
return '\x1b[30m' + text + '\x1b[0m'
}
function white(text) {
return '\x1b[37m' + text + '\x1b[0m'
}
function bgBlue(text) {
return '\x1b[44m' + text + '\x1b[0m'
}
function bgYellow(text) {
return '\x1b[43m' + text + '\x1b[0m'
}
const topText = 'Stand with Ukraine'
const bottomText = 'Please check the Rete.js\'s README for details'
const top = getRectangle(50, 5, bgBlue)
const bottom = getRectangle(50, 5, bgYellow)
// eslint-disable-next-line max-len, no-console
console.log(`${top}\n${drawText(16, 3, white(bgBlue(topText)))}${bottom}\n${drawText(2, 3, black(bgYellow(bottomText)))}`)
================================================
FILE: rete.config.ts
================================================
import { ReteOptions } from 'rete-cli'
import copy from 'rollup-plugin-copy'
export default {
input: 'src/index.ts',
name: 'Rete',
globals: {
crypto: 'crypto'
},
plugins: [
copy({
targets: [
{ src: 'postinstall.js', dest: 'dist' }
]
})
]
}
================================================
FILE: src/editor.ts
================================================
import { Scope } from './scope'
import { BaseSchemes } from './types'
/**
* Signal types produced by NodeEditor instance
* @typeParam Scheme - The scheme type
* @priority 10
* @group Primary
*/
export type Root =
| { type: 'nodecreate', data: Scheme['Node'] }
| { type: 'nodecreated', data: Scheme['Node'] }
| { type: 'noderemove', data: Scheme['Node'] }
| { type: 'noderemoved', data: Scheme['Node'] }
| { type: 'connectioncreate', data: Scheme['Connection'] }
| { type: 'connectioncreated', data: Scheme['Connection'] }
| { type: 'connectionremove', data: Scheme['Connection'] }
| { type: 'connectionremoved', data: Scheme['Connection'] }
| { type: 'clear' }
| { type: 'clearcancelled' }
| { type: 'cleared' }
/**
* The NodeEditor class is the entry class. It is used to create and manage nodes and connections.
* @typeParam Scheme - The scheme type
* @priority 7
* @group Primary
*/
export class NodeEditor extends Scope> {
private nodes: Scheme['Node'][] = []
private connections: Scheme['Connection'][] = []
constructor() {
super('NodeEditor')
}
/**
* Get a node by id
* @param id - The node id
* @returns The node or undefined
*/
public getNode(id: Scheme['Node']['id']) {
return this.nodes.find(node => node.id === id)
}
/**
* Get all nodes
* @returns Copy of array with nodes
*/
public getNodes() {
return this.nodes.slice()
}
/**
* Get all connections
* @returns Copy of array with onnections
*/
public getConnections() {
return this.connections.slice()
}
/**
* Get a connection by id
* @param id - The connection id
* @returns The connection or undefined
*/
public getConnection(id: Scheme['Connection']['id']) {
return this.connections.find(connection => connection.id === id)
}
/**
* Add a node
* @param data - The node data
* @returns Whether the node was added
* @throws If the node has already been added
* @emits nodecreate
* @emits nodecreated
*/
async addNode(data: Scheme['Node']) {
if (this.getNode(data.id)) throw new Error('node has already been added')
if (!await this.emit({ type: 'nodecreate', data })) return false
this.nodes.push(data)
await this.emit({ type: 'nodecreated', data })
return true
}
/**
* Add a connection
* @param data - The connection data
* @returns Whether the connection was added
* @throws If the connection has already been added
* @emits connectioncreate
* @emits connectioncreated
*/
async addConnection(data: Scheme['Connection']) {
if (this.getConnection(data.id)) throw new Error('connection has already been added')
if (!await this.emit({ type: 'connectioncreate', data })) return false
this.connections.push(data)
await this.emit({ type: 'connectioncreated', data })
return true
}
/**
* Remove a node
* @param id - The node id
* @returns Whether the node was removed
* @throws If the node cannot be found
* @emits noderemove
* @emits noderemoved
*/
async removeNode(id: Scheme['Node']['id']) {
const node = this.nodes.find(n => n.id === id)
if (!node) throw new Error('cannot find node')
if (!await this.emit({ type: 'noderemove', data: node })) return false
const index = this.nodes.indexOf(node)
this.nodes.splice(index, 1)
await this.emit({ type: 'noderemoved', data: node })
return true
}
/**
* Remove a connection
* @param id - The connection id
* @returns Whether the connection was removed
* @throws If the connection cannot be found
* @emits connectionremove
* @emits connectionremoved
*/
async removeConnection(id: Scheme['Connection']['id']) {
const connection = this.connections.find(c => c.id === id)
if (!connection) throw new Error('cannot find connection')
if (!await this.emit({ type: 'connectionremove', data: connection })) return false
const index = this.connections.indexOf(connection)
this.connections.splice(index, 1)
await this.emit({ type: 'connectionremoved', data: connection })
return true
}
/**
* Clear all nodes and connections
* @returns Whether the editor was cleared
* @emits clear
* @emits clearcancelled
* @emits cleared
*/
async clear() {
if (!await this.emit({ type: 'clear' })) {
await this.emit({ type: 'clearcancelled' })
return false
}
for (const connection of this.connections.slice()) await this.removeConnection(connection.id)
for (const node of this.nodes.slice()) await this.removeNode(node.id)
await this.emit({ type: 'cleared' })
return true
}
}
================================================
FILE: src/index.ts
================================================
export * from './editor'
export * as ClassicPreset from './presets/classic'
export type { CanAssignSignal, NestedScope, Pipe, ScopeAsParameter } from './scope'
export { Scope, Signal } from './scope'
export * from './types'
export * from './utils'
================================================
FILE: src/presets/classic.ts
================================================
/**
* Contains classes for classic scheme such as Node, Input, Output, Control, Socket, Connection
* @module
* @group Primary
*/
import { ConnectionBase, NodeBase } from '../types'
import { getUID } from '../utils'
type PortId = string
/**
* The socket class
* @priority 7
*/
export class Socket {
/**
* @constructor
* @param name Name of the socket
*/
constructor(public name: string) {
}
}
/**
* General port class
*/
export class Port {
/**
* Port id, unique string generated by `getUID` function
*/
id: PortId
/**
* Port index, used for sorting ports. Default is `0`
*/
index?: number
/**
* @constructor
* @param socket Socket instance
* @param label Label of the port
* @param multipleConnections Whether the output port can have multiple connections
*/
constructor(public socket: S, public label?: string, public multipleConnections?: boolean) {
this.id = getUID()
}
}
/**
* The input port class
* @priority 6
*/
export class Input extends Port {
/**
* Control instance
*/
control: Control | null = null
/**
* Whether the control is visible. Can be managed dynamically by extensions. Default is `true`
*/
showControl = true
/**
* @constructor
* @param socket Socket instance
* @param label Label of the input port
* @param multipleConnections Whether the output port can have multiple connections. Default is `false`
*/
constructor(public socket: S, public label?: string, public multipleConnections?: boolean) {
super(socket, label, multipleConnections)
}
/**
* Add control to the input port
* @param control Control instance
*/
addControl(control: Control) {
if (this.control) throw new Error('control already added for this input')
this.control = control
}
/**
* Remove control from the input port
*/
removeControl() {
this.control = null
}
}
/**
* The output port class
* @priority 5
*/
export class Output extends Port {
/**
* @constructor
* @param socket Socket instance
* @param label Label of the output port
* @param multipleConnections Whether the output port can have multiple connections. Default is `true`
*/
constructor(socket: S, label?: string, multipleConnections?: boolean) {
super(socket, label, multipleConnections !== false)
}
}
/**
* General control class
* @priority 5
*/
export class Control {
/**
* Control id, unique string generated by `getUID` function
*/
id: string
/**
* Control index, used for sorting controls. Default is `0`
*/
index?: number
constructor() {
this.id = getUID()
}
}
/**
* Input control options
*/
type InputControlOptions = {
/** Whether the control is readonly. Default is `false` */
readonly?: boolean
/** Initial value of the control */
initial?: N
/** Callback function that is called when the control value changes */
change?: (value: N) => void
}
/**
* The input control class
* @example new InputControl('text', { readonly: true, initial: 'hello' })
*/
export class InputControl extends Control {
value?: N
readonly: boolean
/**
* @constructor
* @param type Type of the control: `text` or `number`
* @param options Control options
*/
constructor(public type: T, public options?: InputControlOptions) {
super()
this.id = getUID()
this.readonly = options?.readonly ?? false
if (typeof options?.initial !== 'undefined') this.value = options.initial
}
/**
* Set control value
* @param value Value to set
*/
setValue(value?: N) {
this.value = value
if (this.options?.change) this.options.change(value!)
}
}
/**
* The node class
* @priority 10
* @example new Node('math')
*/
export class Node<
Inputs extends { [key in string]?: Socket } = { [key in string]?: Socket },
Outputs extends { [key in string]?: Socket } = { [key in string]?: Socket },
Controls extends { [key in string]?: Control } = { [key in string]?: Control }
> implements NodeBase {
/**
* Node id, unique string generated by `getUID` function
*/
id: NodeBase['id']
/**
* Node inputs
*/
inputs: { [key in keyof Inputs]?: Input> } = {}
/**
* Node outputs
*/
outputs: { [key in keyof Outputs]?: Output> } = {}
/**
* Node controls
*/
controls: Controls = {} as Controls
/**
* Whether the node is selected. Default is `false`
*/
selected?: boolean
constructor(public label: string) {
this.id = getUID()
}
hasInput(key: K) {
return Object.prototype.hasOwnProperty.call(this.inputs, key)
}
addInput(key: K, input: Input>) {
if (this.hasInput(key)) throw new Error(`input with key '${String(key)}' already added`)
Object.defineProperty(this.inputs, key, { value: input, enumerable: true, configurable: true })
}
removeInput(key: keyof Inputs) {
delete this.inputs[key]
}
hasOutput(key: K) {
return Object.prototype.hasOwnProperty.call(this.outputs, key)
}
addOutput(key: K, output: Output>) {
if (this.hasOutput(key)) throw new Error(`output with key '${String(key)}' already added`)
Object.defineProperty(this.outputs, key, { value: output, enumerable: true, configurable: true })
}
removeOutput(key: keyof Outputs) {
delete this.outputs[key]
}
hasControl(key: K) {
return Object.prototype.hasOwnProperty.call(this.controls, key)
}
addControl(key: K, control: Controls[K]) {
if (this.hasControl(key)) throw new Error(`control with key '${String(key)}' already added`)
Object.defineProperty(this.controls, key, { value: control, enumerable: true, configurable: true })
}
removeControl(key: keyof Controls) {
delete this.controls[key]
}
}
/**
* The connection class
* @priority 9
*/
export class Connection<
Source extends Node,
Target extends Node
> implements ConnectionBase {
/**
* Connection id, unique string generated by `getUID` function
*/
id: ConnectionBase['id']
/**
* Source node id
*/
source: NodeBase['id']
/**
* Target node id
*/
target: NodeBase['id']
/**
* @constructor
* @param source Source node instance
* @param sourceOutput Source node output key
* @param target Target node instance
* @param targetInput Target node input key
*/
constructor(
source: Source,
public sourceOutput: keyof Source['outputs'],
target: Target,
public targetInput: keyof Target['inputs']
) {
if (!source.outputs[sourceOutput as string]) {
throw new Error(`source node doesn't have output with a key ${String(sourceOutput)}`)
}
if (!target.inputs[targetInput as string]) {
throw new Error(`target node doesn't have input with a key ${String(targetInput)}`)
}
this.id = getUID()
this.source = source.id
this.target = target.id
}
}
================================================
FILE: src/scope.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/naming-convention */
import {
AcceptPartialUnion, CanAssignSignal, GetAssignmentReferences, GetNonAssignableElements, Tail
} from './utility-types'
export type { CanAssignSignal }
/**
* A middleware type that can modify the data
* @typeParam T - The data type
* @param data - The data to be modified
* @returns The modified data or undefined
* @example (data) => data + 1
* @example (data) => undefined // will stop the execution
* @internal
*/
export type Pipe = (data: T) => Promise | undefined | T
export type CanAssignEach = D extends [infer H1, ...infer Tail1]
? (
F extends [infer H2, ...infer Tail2] ?
[CanAssignSignal, ...CanAssignEach]
: []
) : []
export type ScopeAsParameter, Current extends any[]> = (CanAssignEach<[S['__scope']['produces'], ...S['__scope']['parents']], Current>[number] extends true
? S
: 'Argument Scope does not provide expected signals'
)
/**
* Validate the Scope signals and replace the parameter type with an error message if they are not assignable
* @internal
*/
export type NestedScope, Current extends any[]> = (CanAssignEach[number] extends true
? S
: 'Parent signals do not satisfy the connected scope. Please use `.debug($ => $) for detailed assignment error'
)
/**
* Provides 'debug' method to check the detailed assignment error message
* @example .debug($ => $)
* @internal
*/
export function useHelper, Signals>() {
type T1 = S['__scope']['parents'][number]
return {
debug>(_f: (p: GetAssignmentReferences) => T) {
/* placeholder */
}
}
}
/**
* A signal is a middleware chain that can be used to modify the data
* @typeParam T - The data type
* @internal
*/
export class Signal {
pipes: Pipe[] = []
addPipe(pipe: Pipe) {
this.pipes.push(pipe)
}
async emit(context: Context): Promise {
let current: Context | undefined = context
for (const pipe of this.pipes) {
current = await pipe(current) as Context
if (typeof current === 'undefined') return
}
return current
}
}
type Type = (new(...args: any[]) => T) | (abstract new (...args: any[]) => T)
/**
* Base class for all plugins and the core. Provides a signals mechanism to modify the data
*/
export class Scope {
signal = new Signal>()
parent?: any // Parents['length'] extends 0 ? undefined : Scope>
__scope!: {
produces: Produces
parents: Parents
}
constructor(public name: string) { }
addPipe(middleware: Pipe) {
this.signal.addPipe(middleware)
}
use>(scope: NestedScope) {
if (!(scope instanceof Scope)) throw new Error('cannot use non-Scope instance')
scope.setParent(this)
this.addPipe(context => {
return scope.signal.emit(context)
})
return useHelper()
}
setParent(scope: Scope>) {
this.parent = scope
}
emit(context: C): Promise | undefined> {
return this.signal.emit(context) as Promise>
}
hasParent(): boolean {
return Boolean(this.parent)
}
parentScope>(): Scope
parentScope(type: Type): T
parentScope(type?: Type): T {
if (!this.parent) throw new Error('cannot find parent')
if (type && this.parent instanceof type) return this.parent
if (type) throw new Error('actual parent is not instance of type')
return this.parent
}
}
================================================
FILE: src/types.ts
================================================
/**
* Node id type
*/
export type NodeId = string
/**
* Connection id type
* @group Primary
*/
export type ConnectionId = string
/**
* The base node type
* @group Primary
*/
export type NodeBase = { id: NodeId }
/**
* The base connection type
* @group Primary
*/
export type ConnectionBase = { id: ConnectionId, source: NodeId, target: NodeId }
/**
* Get the schemes
* @example GetSchemes
* @group Primary
*/
export type GetSchemes = { Node: NodeData, Connection: ConnectionData }
/**
* The base schemes
* @group Primary
*/
export type BaseSchemes = GetSchemes
================================================
FILE: src/utility-types.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
export type AcceptPartialUnion = T | any
export type Tail = ((...args: T) => void) extends (head: any, ...tail: infer U) => any ? U : never
export type UnionToIntersection = (
U extends never ? never : (arg: U) => never
) extends (arg: infer I) => void
? I
: never
type StrictExcludeInner = 0 extends (
U extends T ? [T] extends [U] ? 0 : never : never
) ? never : T
export type StrictExclude = T extends unknown ? StrictExcludeInner : never
export type UnionToTuple = UnionToIntersection<
T extends never ? never : (t: T) => T
> extends (_: never) => infer W
? [...UnionToTuple>, W]
: []
export type FilterMatch = T extends [infer Head, ...infer _Tail]
? ([Head] extends [V]
? [Head, ...FilterMatch<_Tail, V>]
: FilterMatch<_Tail, V>
) : []
export type CanAssignToAnyOf = FilterMatch, Requires> extends [] ? false : true
export type CanAssignEachTupleElemmentToAnyOf = Requires extends [infer Head, ...infer _Tail]
? CanAssignToAnyOf extends true ?
(_Tail extends []
? true
: CanAssignEachTupleElemmentToAnyOf
) : false
: false
export type CanAssignEachToAnyOf = CanAssignEachTupleElemmentToAnyOf>
export type CanAssignSignal = CanAssignEachToAnyOf
type ReplaceTupleTypes = { [K in keyof T]: U }
export type FilterNever = T extends [infer Head, ...infer _Tail]
? ([Head] extends [never] ? FilterNever<_Tail> : [Head, ...FilterNever<_Tail>])
: []
type KeepIfNonAssignable = CanAssignToAnyOf extends false ? T : never
export type GetAllNonValidElements = T extends [infer Head, ...infer _Tail]
? ([KeepIfNonAssignable, ...GetAllNonValidElements<_Tail, Signals>])
: []
export type GetNonAssignableElements
= FilterNever, Signals>>
export type GetAssignmentReferences = ReplaceTupleTypes
================================================
FILE: src/utils.ts
================================================
const crypto = globalThis.crypto as (typeof globalThis.crypto | typeof import('node:crypto'))
/**
* @returns A unique id
*/
export function getUID(): string {
if ('randomBytes' in crypto) {
return crypto.randomBytes(8).toString('hex')
}
const bytes = crypto.getRandomValues(new Uint8Array(8))
const array = Array.from(bytes)
const hexPairs = array.map(b => b.toString(16).padStart(2, '0'))
return hexPairs.join('')
}
================================================
FILE: test/index.perf.ts
================================================
import { describe, it } from '@jest/globals'
import { BaseSchemes } from '../src'
import { NodeEditor } from '../src/editor'
const iterations = 5
describe('NodeEditor', () => {
for (let iteration = 0; iteration < iterations; iteration++) {
describe(`remove nodes (${iteration})`, () => {
// eslint-disable-next-line init-declarations
let editor!: NodeEditor
const ids = new Array(5_000).fill(0)
// eslint-disable-next-line @typescript-eslint/naming-convention
.map((_, i) => i)
const idsToRemove = [...ids].reverse()
beforeEach(async () => {
editor = new NodeEditor()
await Promise.all(ids.map(id => editor.addNode({ id: `s${id}` })))
})
it('basic', async () => {
await Promise.all(idsToRemove.map(id => editor.removeNode(`s${id}`)))
})
})
}
for (let iteration = 0; iteration < iterations; iteration++) {
describe(`remove connections (${iteration})`, () => {
// eslint-disable-next-line init-declarations
let editor!: NodeEditor
const ids = new Array(3_000).fill(0)
// eslint-disable-next-line @typescript-eslint/naming-convention
.map((_, i) => i)
const idsToRemove = [...ids].reverse()
beforeEach(async () => {
editor = new NodeEditor()
await Promise.all(ids.map(id => editor.addNode({ id: `s${id}` })))
await Promise.all(ids.map(id => editor.addNode({ id: `t${id}` })))
await Promise.all(ids.map(id => editor.addConnection({ id: `c${id}`, source: `s${id}`, target: `t${id}` })))
})
it('basic', async () => {
await Promise.all(idsToRemove.map(id => editor.removeConnection(`c${id}`)))
})
})
}
})
================================================
FILE: test/index.test.ts
================================================
import { describe, expect, it } from '@jest/globals'
import { NodeEditor } from '../src/editor'
describe('NodeEditor', () => {
it('NodeEditor is instantiable', () => {
expect(new NodeEditor()).toBeInstanceOf(NodeEditor)
})
it('addNode should add a node', async () => {
const editor = new NodeEditor()
const nodeData = { id: '1', label: 'Node 1' }
const result = await editor.addNode(nodeData)
const nodes = editor.getNodes()
expect(result).toBe(true)
expect(nodes).toHaveLength(1)
expect(nodes[0]).toEqual(nodeData)
})
it('addNode should not add a node with duplicate id', async () => {
const editor = new NodeEditor()
const nodeData = { id: '1', label: 'Node 1' }
await editor.addNode(nodeData)
await expect(() => editor.addNode(nodeData)).rejects.toThrowError()
})
it('addConnection should add a connection', async () => {
const editor = new NodeEditor()
const connectionData = { id: '1', source: '1', target: '2' }
await editor.addNode({ id: '1' })
await editor.addNode({ id: '2' })
const result = await editor.addConnection(connectionData)
const connections = editor.getConnections()
expect(result).toBe(true)
expect(connections).toHaveLength(1)
expect(connections[0]).toEqual(connectionData)
})
it('addConnection should not add a connection with duplicate id', async () => {
const editor = new NodeEditor()
const connectionData = { id: '1', source: '1', target: '2' }
await editor.addNode({ id: '1' })
await editor.addNode({ id: '2' })
await editor.addConnection(connectionData)
await expect(() => editor.addConnection(connectionData)).rejects.toThrowError()
})
it('removeNode should remove a node', async () => {
const editor = new NodeEditor()
const nodeData = { id: '1', label: 'Node 1' }
await editor.addNode(nodeData)
await editor.removeNode('1')
const nodes = editor.getNodes()
expect(nodes).toHaveLength(0)
})
it('removeNode should remove specified nodes', async () => {
const editor = new NodeEditor()
const ids = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
await Promise.all(ids.map(id => editor.addNode({ id: id })))
const removeIds = ['1', '2', '3', '4', '5']
await Promise.all(removeIds.map(id => editor.removeNode(id)))
const remainIds = editor.getNodes().map(n => n.id)
await editor.clear()
expect(remainIds).toEqual(['6', '7', '8', '9', '10'])
})
it('removeConnection should remove a connection', async () => {
const editor = new NodeEditor()
const connectionData = { id: '1', source: '1', target: '2' }
await editor.addNode({ id: '1' })
await editor.addNode({ id: '2' })
await editor.addConnection(connectionData)
await editor.removeConnection('1')
const connections = editor.getConnections()
expect(connections).toHaveLength(0)
})
it('removeConnection should remove specified connections', async () => {
const editor = new NodeEditor()
const ids = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
await Promise.all(ids.map(id => editor.addNode({ id: `s${id}` })))
await Promise.all(ids.map(id => editor.addNode({ id: `t${id}` })))
await Promise.all(ids.map(id => editor.addConnection({ id: id, source: `s${id}`, target: `t${id}` })))
const removeIds = ['1', '2', '3', '4', '5']
await Promise.all(removeIds.map(id => editor.removeConnection(id)))
const remainIds = editor.getConnections().map(c => c.id)
await editor.clear()
expect(remainIds).toEqual(['6', '7', '8', '9', '10'])
})
it('should clear all nodes and connections', async () => {
const editor = new NodeEditor()
await editor.addNode({ id: '1' })
await editor.addNode({ id: '2' })
await editor.addConnection({ id: '1', source: '1', target: '2' })
await editor.clear()
const nodes = editor.getNodes()
const connections = editor.getConnections()
expect(nodes).toHaveLength(0)
expect(connections).toHaveLength(0)
})
})
================================================
FILE: test/mocks/crypto.ts
================================================
import { jest } from '@jest/globals'
import { Buffer } from 'buffer'
export function mockCrypto(object: Record) {
// eslint-disable-next-line no-undef
Object.defineProperty(globalThis, 'crypto', {
value: object,
writable: true
})
}
export function mockCryptoFromArray(array: Uint8Array) {
mockCrypto({
getRandomValues: jest.fn().mockReturnValue(array)
})
}
export function mockCryptoFromBuffer(buffer: Buffer) {
mockCrypto({
randomBytes: jest.fn().mockReturnValue(buffer)
})
}
export function resetCrypto() {
// eslint-disable-next-line no-undef
Object.defineProperty(globalThis, 'crypto', {
// eslint-disable-next-line no-undefined
value: undefined,
writable: true
})
}
================================================
FILE: test/presets/classic.test.ts
================================================
import { afterEach, beforeEach, describe, expect, it } from '@jest/globals'
import { mockCryptoFromArray, resetCrypto } from '../mocks/crypto'
describe('ClassicPreset', () => {
// eslint-disable-next-line init-declarations
let preset!: typeof import('../../src/presets/classic')
beforeEach(async () => {
mockCryptoFromArray(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]))
preset = await import('../../src/presets/classic')
})
afterEach(() => {
resetCrypto()
})
describe('Node', () => {
it('is instantiable', () => {
expect(new preset.Node('A')).toBeInstanceOf(preset.Node)
})
it('should have an id', () => {
const node = new preset.Node('A')
expect(node.id).toBeDefined()
})
it('should have a label', () => {
const node = new preset.Node('A')
expect(node.label).toBe('A')
})
it('adds Input', () => {
const node = new preset.Node('A')
const input = new preset.Input(new preset.Socket('a'))
node.addInput('a', input)
expect(node.hasInput('a')).toBeTruthy()
expect(node.inputs.a).toBe(input)
})
it('throws error if Input already exists', () => {
const node = new preset.Node('A')
node.addInput('a', new preset.Input(new preset.Socket('a')))
expect(() => node.addInput('a', new preset.Input(new preset.Socket('a')))).toThrow()
})
it('removes Input', () => {
const node = new preset.Node('A')
node.addInput('a', new preset.Input(new preset.Socket('a')))
node.removeInput('a')
expect(node.hasInput('a')).toBeFalsy()
})
it('adds Output', () => {
const node = new preset.Node('A')
const output = new preset.Output(new preset.Socket('a'))
node.addOutput('a', output)
expect(node.hasOutput('a')).toBeTruthy()
expect(node.outputs.a).toBe(output)
})
it('throws error if Output already exists', () => {
const node = new preset.Node('A')
node.addOutput('a', new preset.Output(new preset.Socket('a')))
expect(() => node.addOutput('a', new preset.Output(new preset.Socket('a')))).toThrow()
})
it('removes Output', () => {
const node = new preset.Node('A')
node.addOutput('a', new preset.Output(new preset.Socket('a')))
node.removeOutput('a')
expect(node.hasOutput('a')).toBeFalsy()
})
})
describe('Connection', () => {
it('Connection throws error if input not found', () => {
const a = new preset.Node('A')
const b = new preset.Node('B')
a.addOutput('a', new preset.Output(new preset.Socket('a')))
expect(() => new preset.Connection(a, 'a', b, 'b')).toThrow()
})
it('Connection throws error if output not found', () => {
const a = new preset.Node('A')
const b = new preset.Node('B')
b.addInput('b', new preset.Input(new preset.Socket('b')))
expect(() => new preset.Connection(a, 'a', b, 'b')).toThrow()
})
it('Connection is instantiable', () => {
const a = new preset.Node('A')
const b = new preset.Node('B')
const output = new preset.Output(new preset.Socket('b'))
const input = new preset.Input(new preset.Socket('a'))
a.addOutput('a', output)
b.addInput('b', input)
expect(new preset.Connection(a, 'a', b, 'b')).toBeInstanceOf(preset.Connection)
})
})
describe('Control', () => {
it('adds Control to Node', () => {
const node = new preset.Node('A')
node.addControl('ctrl', new preset.Control())
expect(node.hasControl('ctrl')).toBeTruthy()
})
it('throws error if Control already exists', () => {
const node = new preset.Node('A')
node.addControl('ctrl', new preset.Control())
expect(() => node.addControl('ctrl', new preset.Control())).toThrow()
})
it('removes Control from Node', () => {
const node = new preset.Node('A')
node.addControl('ctrl', new preset.Control())
node.removeControl('ctrl')
expect(node.hasControl('ctrl')).toBeFalsy()
})
it('adds Control to Input', () => {
const input = new preset.Input(new preset.Socket('a'))
input.addControl(new preset.Control())
expect(input.control).toBeTruthy()
})
it('throws error if Control in Input already exists', () => {
const input = new preset.Input(new preset.Socket('a'))
input.addControl(new preset.Control())
expect(() => input.addControl(new preset.Control())).toThrow()
})
it('removes Control from Input', () => {
const input = new preset.Input(new preset.Socket('a'))
input.addControl(new preset.Control())
input.removeControl()
expect(input.control).toBeFalsy()
})
})
})
================================================
FILE: test/scope.test.ts
================================================
import { describe, expect, it, jest } from '@jest/globals'
import { Scope } from '../src/scope'
type Parent = { parent: string }
type Child = { child: number }
describe('Scope', () => {
it('should create a new Scope instance', () => {
const scope = new Scope('test')
expect(scope).toBeInstanceOf(Scope)
})
it('doesnt have a parent by default', () => {
const scope = new Scope('test')
expect(scope.hasParent()).toBeFalsy()
})
describe('parent-child', () => {
it('should set a parent scope', () => {
const parent = new Scope('parent')
const child = new Scope('child')
child.setParent(parent)
expect(child.parentScope()).toBe(parent)
})
it('should use a nested scope', () => {
const parent = new Scope('parent')
const child = new Scope('child')
parent.use(child)
expect(child.hasParent()).toBeTruthy()
expect(child.parentScope()).toBe(parent)
})
it('should throw an error when using a non-Scope instance', () => {
const parent = new Scope('parent')
const child = { signal: { emit: jest.fn() } }
expect(() => parent.use(child as any)).toThrowError('cannot use non-Scope instance')
})
it('should throw an error when trying to access a parent without one', () => {
const scope = new Scope('test')
expect(() => scope.parentScope()).toThrowError('cannot find parent')
})
it('should throw an error when trying to access a parent with the wrong type', () => {
class WrongScope extends Scope { }
const parent = new Scope('parent')
const child = new Scope('child')
parent.use(child)
expect(() => child.parentScope(WrongScope)).toThrowError('actual parent is not instance of type')
})
})
describe('addPipe', () => {
it('should emit a signal', async () => {
const scope = new Scope('test')
const pipe = jest.fn<() => Parent>()
scope.addPipe(pipe)
await scope.emit({ parent: 'test' })
expect(pipe).toHaveBeenCalledWith({ parent: 'test' })
})
it('should return a promise from emit', () => {
const scope = new Scope('test')
const signal = jest.fn<() => Parent>()
scope.addPipe(signal)
const result = scope.emit({ parent: 'test' })
expect(result).toBeInstanceOf(Promise)
})
it('should return the result of the signal', async () => {
const scope = new Scope('test')
const signal = jest.fn<() => Parent>().mockReturnValue({ parent: 'test-result' })
scope.addPipe(signal)
const result = await scope.emit({ parent: 'test' })
expect(result).toEqual({ parent: 'test-result' })
})
it('should return undefined if the signal returns undefined', async () => {
const scope = new Scope('test')
// eslint-disable-next-line no-undefined
const signal = jest.fn().mockReturnValue(undefined)
scope.addPipe(signal)
const result = await scope.emit('test')
expect(result).toBeUndefined()
})
it('should return the result of the signal with a parent', async () => {
const parent = new Scope('parent')
const child = new Scope('child')
const signal = jest.fn<() => Parent>().mockReturnValue({ parent: 'test-parent' })
parent.addPipe(signal)
parent.use(child)
const result = await child.emit({ child: 1 })
expect(result).toEqual({ child: 1 })
})
it('should return the result of the signal with a parent and child', async () => {
const parent = new Scope('parent')
const child = new Scope('child')
const signal = jest.fn<() => Child>().mockReturnValue({ child: 1 })
parent.use(child)
child.addPipe(signal)
const result = await child.emit({ child: 2 })
expect(result).toEqual({ child: 1 })
})
it('should transfer signals from parent to child', async () => {
const parent = new Scope('parent')
const child = new Scope('child')
const parentSignal = jest.fn<() => Parent>().mockReturnValue({ parent: 'test-parent' })
const childSignal = jest.fn<() => Child>()
parent.addPipe(parentSignal)
child.addPipe(childSignal)
parent.use(child)
await parent.emit({ parent: 'test-parent' })
expect(childSignal).toHaveBeenCalledWith({ parent: 'test-parent' })
})
it('should prevent execution of child signal if parent signal returns undefined', async () => {
const parent = new Scope('parent')
const child = new Scope('child')
// eslint-disable-next-line no-undefined
const parentSignal = jest.fn<() => Parent | undefined>().mockReturnValue(undefined)
const childSignal = jest.fn<() => Child>()
parent.addPipe(parentSignal)
child.addPipe(childSignal)
parent.use(child)
await parent.emit({ parent: 'test-parent' })
expect(childSignal).not.toHaveBeenCalled()
})
})
})
================================================
FILE: test/utils.test.ts
================================================
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals'
import { Buffer } from 'buffer'
import { mockCryptoFromArray, mockCryptoFromBuffer, resetCrypto } from './mocks/crypto'
describe('getUID', () => {
beforeEach(() => {
jest.resetModules()
})
afterEach(() => {
resetCrypto()
})
it('should return a unique id based on crypto.getRandomValues', async () => {
mockCryptoFromArray(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]))
const { getUID } = await import('../src/utils')
const uid = getUID()
expect(uid).toHaveLength(16)
})
it('should return a unique id based on crypto.randomBytes', async () => {
mockCryptoFromBuffer(Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]))
const { getUID } = await import('../src/utils')
const uid = getUID()
expect(uid).toHaveLength(16)
})
})
================================================
FILE: tsconfig.json
================================================
{
"extends": "rete-cli/configs/tsconfig.json",
"compilerOptions": {
"target": "es5",
"downlevelIteration": true,
"isolatedModules": false,
"lib": []
},
"include": ["src", "test"]
}