import { Atom, Registry } from '@effect-atom/atom'; import * as Effect from 'effect/Effect'; import * as Option from 'effect/Option'; import * as Pipeable from 'effect/Pipeable'; import { type Type } from '@dxos/echo'; import { type MaybePromise, Position } from '@dxos/util'; import * as Graph from './graph'; import * as Node from './node'; /** * Graph builder extension for adding nodes to the graph based on a connection to an existing node. * * @param params.node The existing node the returned nodes will be connected to. */ export type ConnectorExtension = (node: Atom.Atom>) => Atom.Atom[]>; /** * Constrained case of the connector extension for more easily adding actions to the graph. */ export type ActionsExtension = (node: Atom.Atom>) => Atom.Atom>, 'type' | 'nodes' | 'edges'>[]>; /** * Constrained case of the connector extension for more easily adding action groups to the graph. */ export type ActionGroupsExtension = (node: Atom.Atom>) => Atom.Atom, 'type' | 'data' | 'nodes' | 'edges'>[]>; /** * Graph builder extension for adding nodes to the graph based on a node id. * * TODO(wittjosiah): Remove? Superseded by the declared `url` binding — URL resolution no longer * materializes a node from a bare id. Retained alongside `Graph.initialize`, which fires it. */ export type ResolverExtension = (id: string) => Atom.Atom | null>; export type BuilderExtension = Readonly<{ id: string; position?: Position.Position; relation?: Node.RelationInput; /** * URL binding for the nodes this extension's connector produces: the registered prefix key plus how * it resolves. Omitted when the extension's nodes are not URL-addressable. See {@link UrlBinding} and * `path-resolution.ts` for how the key table is derived and used. */ url?: UrlBinding; resolver?: ResolverExtension; connector?: (node: Atom.Atom>) => Atom.Atom[]>; }>; /** * How an extension's nodes map to (and from) the URL pair chain — one binding per extension, holding * the whole URL contract for the nodes it produces. The `kind` is the *resolution tier*: what a pair * with this key resolves against. * * - `'item'` — Resolves against the current anchor (workspace) base, addressed by a variable id. * The default addressable node; may itself have children (e.g. a mailbox). (`doc/`). * - `'singleton'` — Resolves against the current anchor base, but is a single fixed node per anchor, so * it carries no id — its terminal node-id segment is the key itself. (`settings`). * * The anchor and linked tiers are not declared per extension: they are fixed keys of the URL grammar, * configured once on the builder as {@link UrlGrammar}. * * `path` is how the node is located, in one of two forms: * - `string[]` — fixed ancestor node-id segments between the workspace base and the node (the common, * deterministic case): the node is `${Node.RootId}//<...segments>/`. Fixed-depth * dynamic tails beyond the segments are `+`-encoded into the id. * - {@link PathResolver} — a dynamic resolver, for data-dependent shapes (e.g. nested collections at * arbitrary depth) that cannot declare static segments. * * Read by `path-resolution.ts` (which derives the parse table's `hasId`/`anchor` from `kind`) and * consumed by `UrlPath.parse`. */ export type UrlBinding = { key: string; kind: 'item' | 'singleton'; path: string[] | PathResolver; }; /** * The URL grammar the builder resolves and stamps against, configured once at construction. * * The two keys are fixed tiers no extension declares (no connector produces their nodes): `anchorKey` * establishes the base that following pairs resolve against and is consumed as a rebase * (`w/`); `linkedKey` addresses the linked-segment child of the preceding item * (`companion/`), resolved structurally. The separators are the id-encoding conventions: * `linkedPrefix` marks a linked segment (`/~`), and `tailSeparator` joins the * fixed-depth node-id segments between a key's static `path` and the object id into one URL id * (`db/+`) so a fixed-depth nested shape needs no resolver. */ export type UrlGrammar = { anchorKey?: string; linkedKey?: string; linkedPrefix: string; tailSeparator: string; }; /** {@link UrlGrammar} as supplied at construction: the separators fall back to their defaults. */ export type UrlGrammarProps = Partial; /** Params passed to a {@link PathResolver} for a single `(key, id)` URL pair. */ export type PathResolveParams = { /** The id segment from the `(key, id)` pair. */ id: string; /** The workspace segment from the URL. */ workspace: string; /** Qualified id of the workspace base node (`${Node.RootId}/`). */ workspaceBaseId: string; }; /** * Dynamic forward URL resolver for an extension whose node-id shape is data-dependent and so cannot * declare a static {@link UrlBinding.path}. Returns the candidate qualified node id — * `path-resolution.ts` then materializes its ancestors and verifies it — or `null` if the id can't be * located. Must be self-contained (the declaring plugin closes over any services it needs), so * `@dxos/app-graph` stays free of service dependencies. */ export type PathResolver = (params: PathResolveParams) => Effect.Effect; export type BuilderExtensions = BuilderExtension | BuilderExtension[] | BuilderExtensions[]; /** * The `(key, id?)` URL representation of a node under a given {@link UrlBinding} — the reverse of forward * resolution, minus the workspace (always the node id's second segment). A singleton has no id; a * resolver-backed key keeps just the object id; a static path encodes the segments between the path and * the id, `+`-joined (empty when the node sits at the path — a container whose children are the items). */ export declare const urlRepresentation: (nodeId: string, url: UrlBinding, tailSeparator?: string) => { key: string; id?: string; }; /** * A node's own URL pair segment — `/[/]`, with no workspace/anchor prefix — or `undefined` when * the node is not addressable in its own right (a container node sitting at the binding's `path`, whose * children are the addressable items). A full URL is composed by prefixing `/w/`. */ export declare const nodeUrlSegment: (nodeId: string, url: UrlBinding, tailSeparator?: string) => string | undefined; /** * A graph node with its computed {@link nodeUrlSegment} attached at `properties.urlSegment` when the node * is URL-addressable. The core {@link Node.Node} stays URL-agnostic; this is the typed view for reading * the segment — an open properties record with an explicit `urlSegment` field — mirroring how * `@dxos/react-ui-menu` wraps `Node` for menu items. */ export type BuilderNode = Node.Node>; export type GraphBuilderTraverseOptions = { visitor: (node: Node.Node, path: string[]) => MaybePromise; registry?: Registry.Registry; source?: string; relation: Node.RelationInput | Node.RelationInput[]; }; /** * Identifier denoting a GraphBuilder. */ export declare const GraphBuilderTypeId: unique symbol; export type GraphBuilderTypeId = typeof GraphBuilderTypeId; /** * GraphBuilder interface. */ export interface GraphBuilder extends Pipeable.Pipeable { readonly [GraphBuilderTypeId]: GraphBuilderTypeId; readonly graph: Graph.ExpandableGraph; readonly extensions: Atom.Atom>; /** The URL grammar this builder resolves and stamps against (separators always resolved). */ readonly urlGrammar: UrlGrammar; /** Read the currently registered extensions synchronously (used for URL key-table derivation). */ getExtensions(): Record; /** * The id of the extension whose connector produced the given node, if known. Populated as * connectors materialize nodes and cleared on removal; used by `path-resolution.ts` for * reverse (node → URL) mapping. */ getNodeExtensionId(nodeId: string): string | undefined; } /** Construction params: the backing graph's props plus the URL grammar's fixed keys. */ export type GraphBuilderProps = Pick & { urlGrammar?: UrlGrammarProps; }; /** * Creates a new GraphBuilder instance. */ export declare const make: (params?: GraphBuilderProps) => GraphBuilder; /** * Creates a GraphBuilder from a serialized pickle string. */ export declare const from: (pickle?: string, registry?: Registry.Registry, urlGrammar?: UrlGrammarProps) => GraphBuilder; /** * Add extensions to the graph builder. */ export declare function addExtension(builder: GraphBuilder, extensions: BuilderExtensions): GraphBuilder; export declare function addExtension(extensions: BuilderExtensions): (builder: GraphBuilder) => GraphBuilder; /** * Remove an extension from the graph builder. */ export declare function removeExtension(builder: GraphBuilder, id: string): GraphBuilder; export declare function removeExtension(id: string): (builder: GraphBuilder) => GraphBuilder; /** * Explore the graph by traversing it with the given options. */ export declare function explore(builder: GraphBuilder, options: GraphBuilderTraverseOptions, path?: string[]): Promise; export declare function explore(options: GraphBuilderTraverseOptions, path?: string[]): (builder: GraphBuilder) => Promise; /** * Destroy the graph builder and clean up resources. */ export declare function destroy(builder: GraphBuilder): void; export declare function destroy(): (builder: GraphBuilder) => void; /** * Wait for all pending connector updates to be flushed. */ export declare const flush: (builder: GraphBuilder) => Promise; /** * A graph builder extension is used to add nodes to the graph. * * @param params.id The unique id of the extension. * @param params.relation The relation the graph is being expanded from the existing node. * @param params.position Affects the order the extensions are processed in. * @param params.url URL binding for the nodes this extension produces (key + resolution); see {@link UrlBinding}. * @param params.connector A function to add nodes to the graph based on a connection to an existing node. * @param params.actions A function to add actions to the graph based on a connection to an existing node. * @param params.actionGroups A function to add action groups to the graph based on a connection to an existing node. */ export type CreateExtensionRawOptions = { id: string; relation?: Node.RelationInput; position?: Position.Position; url?: UrlBinding; resolver?: ResolverExtension; connector?: ConnectorExtension; actions?: ActionsExtension; actionGroups?: ActionGroupsExtension; }; /** * Create a graph builder extension (low-level API that works directly with Atoms). */ export declare const createExtensionRaw: (extension: CreateExtensionRawOptions) => BuilderExtension[]; /** * Options for creating a graph builder extension with simplified API. * All callbacks must return Effects for dependency injection. * Effects may defect — defects are caught, logged, and the extension returns empty results. * Use Effect.orDie on any failable effects inside callbacks. */ export type CreateExtensionOptions = { id: string; match: (node: Node.Node, get: Atom.Context) => Option.Option; actions?: (matched: TMatched, get: Atom.Context) => Effect.Effect, any>, 'type'>[], never, R>; /** Contribute dropdown action groups (each with nested `actions`) to the matched node; the group's * `type`/`data` are set automatically, so returning `Node.makeActionGroup(...)` output is fine. */ actionGroups?: (matched: TMatched, get: Atom.Context) => Effect.Effect, 'type' | 'data'>[], never, R>; resolver?: (id: string, get: Atom.Context) => Effect.Effect | null, never, R>; connector?: (matched: TMatched, get: Atom.Context) => Effect.Effect[], never, R>; relation?: Node.RelationInput; position?: Position.Position; /** URL binding for the nodes this extension produces (key + resolution); see {@link UrlBinding}. */ url?: UrlBinding; }; /** * Create a graph builder extension with simplified API. * Returns an Effect to allow callbacks to access services via dependency injection. */ export declare const createExtension: (options: CreateExtensionOptions) => Effect.Effect; /** * Create a connector extension from a matcher and factory function. * The factory's data type is inferred from the matcher's return type. */ export declare const createConnector: (matcher: (node: Node.Node, get: Atom.Context) => Option.Option, factory: (data: TData, get: Atom.Context) => Node.NodeArg[]) => ConnectorExtension; /** * Options for creating a type-based extension. * All callbacks must return Effects for dependency injection. * Effects may fail - errors are caught, logged, and the extension returns empty results. */ export type CreateTypeExtensionOptions = { id: string; type: T; actions?: (object: Type.InstanceType, get: Atom.Context) => Effect.Effect>, 'type'>[], never, R>; actionGroups?: (object: Type.InstanceType, get: Atom.Context) => Effect.Effect, 'type' | 'data'>[], never, R>; connector?: (object: Type.InstanceType, get: Atom.Context) => Effect.Effect[], never, R>; relation?: Node.RelationInput; position?: Position.Position; }; /** * Create an extension that matches nodes by schema type. * The entity type is inferred from the schema type and works for both object and relation schemas. * Returns an Effect to allow callbacks to access services via dependency injection. */ export declare const createTypeExtension: (options: CreateTypeExtensionOptions) => Effect.Effect; export declare const flattenExtensions: (extension: BuilderExtensions, acc?: BuilderExtension[]) => BuilderExtension[]; //# sourceMappingURL=graph-builder.d.ts.map