@clayroach/effect 3.19.14-source-capture.8 → 3.19.14-source-trace.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SourceLocation/package.json +6 -0
- package/dist/cjs/Effect.js +2 -28
- package/dist/cjs/Effect.js.map +1 -1
- package/dist/cjs/FiberRef.js +12 -1
- package/dist/cjs/FiberRef.js.map +1 -1
- package/dist/cjs/Layer.js +2 -24
- package/dist/cjs/Layer.js.map +1 -1
- package/dist/cjs/RuntimeFlags.js +1 -29
- package/dist/cjs/RuntimeFlags.js.map +1 -1
- package/dist/cjs/SourceLocation.js +60 -0
- package/dist/cjs/SourceLocation.js.map +1 -0
- package/dist/cjs/Tracer.js +1 -15
- package/dist/cjs/Tracer.js.map +1 -1
- package/dist/cjs/Utils.js +1 -1
- package/dist/cjs/Utils.js.map +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/internal/clock.js +1 -1
- package/dist/cjs/internal/clock.js.map +1 -1
- package/dist/cjs/internal/core.js +17 -50
- package/dist/cjs/internal/core.js.map +1 -1
- package/dist/cjs/internal/effect/circular.js +18 -30
- package/dist/cjs/internal/effect/circular.js.map +1 -1
- package/dist/cjs/internal/fiberRuntime.js +16 -65
- package/dist/cjs/internal/fiberRuntime.js.map +1 -1
- package/dist/cjs/internal/layer/circular.js +1 -5
- package/dist/cjs/internal/layer/circular.js.map +1 -1
- package/dist/cjs/internal/layer.js +1 -3
- package/dist/cjs/internal/layer.js.map +1 -1
- package/dist/cjs/internal/logger.js +25 -2
- package/dist/cjs/internal/logger.js.map +1 -1
- package/dist/cjs/internal/runtimeFlags.js +2 -11
- package/dist/cjs/internal/runtimeFlags.js.map +1 -1
- package/dist/cjs/internal/tracer.js +1 -114
- package/dist/cjs/internal/tracer.js.map +1 -1
- package/dist/dts/Config.d.ts +2 -2
- package/dist/dts/Config.d.ts.map +1 -1
- package/dist/dts/Effect.d.ts +8 -29
- package/dist/dts/Effect.d.ts.map +1 -1
- package/dist/dts/FiberRef.d.ts +12 -0
- package/dist/dts/FiberRef.d.ts.map +1 -1
- package/dist/dts/Layer.d.ts +0 -22
- package/dist/dts/Layer.d.ts.map +1 -1
- package/dist/dts/RuntimeFlags.d.ts +0 -28
- package/dist/dts/RuntimeFlags.d.ts.map +1 -1
- package/dist/dts/SourceLocation.d.ts +88 -0
- package/dist/dts/SourceLocation.d.ts.map +1 -0
- package/dist/dts/Tracer.d.ts +0 -15
- package/dist/dts/Tracer.d.ts.map +1 -1
- package/dist/dts/index.d.ts +6 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/internal/core.d.ts.map +1 -1
- package/dist/dts/internal/layer.d.ts.map +1 -1
- package/dist/dts/internal/runtimeFlags.d.ts.map +1 -1
- package/dist/esm/Effect.js +0 -26
- package/dist/esm/Effect.js.map +1 -1
- package/dist/esm/FiberRef.js +11 -0
- package/dist/esm/FiberRef.js.map +1 -1
- package/dist/esm/Layer.js +0 -22
- package/dist/esm/Layer.js.map +1 -1
- package/dist/esm/RuntimeFlags.js +0 -28
- package/dist/esm/RuntimeFlags.js.map +1 -1
- package/dist/esm/SourceLocation.js +51 -0
- package/dist/esm/SourceLocation.js.map +1 -0
- package/dist/esm/Tracer.js +0 -14
- package/dist/esm/Tracer.js.map +1 -1
- package/dist/esm/Utils.js +1 -1
- package/dist/esm/Utils.js.map +1 -1
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal/clock.js +1 -1
- package/dist/esm/internal/clock.js.map +1 -1
- package/dist/esm/internal/core.js +12 -45
- package/dist/esm/internal/core.js.map +1 -1
- package/dist/esm/internal/effect/circular.js +18 -30
- package/dist/esm/internal/effect/circular.js.map +1 -1
- package/dist/esm/internal/fiberRuntime.js +13 -60
- package/dist/esm/internal/fiberRuntime.js.map +1 -1
- package/dist/esm/internal/layer/circular.js +0 -4
- package/dist/esm/internal/layer/circular.js.map +1 -1
- package/dist/esm/internal/layer.js +0 -2
- package/dist/esm/internal/layer.js.map +1 -1
- package/dist/esm/internal/logger.js +25 -2
- package/dist/esm/internal/logger.js.map +1 -1
- package/dist/esm/internal/runtimeFlags.js +1 -9
- package/dist/esm/internal/runtimeFlags.js.map +1 -1
- package/dist/esm/internal/tracer.js +0 -111
- package/dist/esm/internal/tracer.js.map +1 -1
- package/package.json +12 -1
- package/src/Arbitrary.ts +1101 -0
- package/src/Array.ts +3589 -0
- package/src/BigDecimal.ts +1349 -0
- package/src/BigInt.ts +643 -0
- package/src/Boolean.ts +287 -0
- package/src/Brand.ts +360 -0
- package/src/Cache.ts +281 -0
- package/src/Cause.ts +1555 -0
- package/src/Channel.ts +2355 -0
- package/src/ChildExecutorDecision.ts +146 -0
- package/src/Chunk.ts +1495 -0
- package/src/Clock.ts +111 -0
- package/src/Config.ts +542 -0
- package/src/ConfigError.ts +270 -0
- package/src/ConfigProvider.ts +333 -0
- package/src/ConfigProviderPathPatch.ts +100 -0
- package/src/Console.ts +226 -0
- package/src/Context.ts +585 -0
- package/src/Cron.ts +706 -0
- package/src/Data.ts +596 -0
- package/src/DateTime.ts +1686 -0
- package/src/DefaultServices.ts +34 -0
- package/src/Deferred.ts +301 -0
- package/src/Differ.ts +450 -0
- package/src/Duration.ts +1000 -0
- package/src/Effect.ts +14817 -0
- package/src/Effectable.ts +107 -0
- package/src/Either.ts +1040 -0
- package/src/Encoding.ts +195 -0
- package/src/Equal.ts +98 -0
- package/src/Equivalence.ts +235 -0
- package/src/ExecutionPlan.ts +308 -0
- package/src/ExecutionStrategy.ts +119 -0
- package/src/Exit.ts +467 -0
- package/src/FastCheck.ts +9 -0
- package/src/Fiber.ts +744 -0
- package/src/FiberHandle.ts +540 -0
- package/src/FiberId.ts +195 -0
- package/src/FiberMap.ts +656 -0
- package/src/FiberRef.ts +444 -0
- package/src/FiberRefs.ts +204 -0
- package/src/FiberRefsPatch.ts +105 -0
- package/src/FiberSet.ts +491 -0
- package/src/FiberStatus.ts +108 -0
- package/src/Function.ts +1222 -0
- package/src/GlobalValue.ts +53 -0
- package/src/Graph.ts +3732 -0
- package/src/GroupBy.ts +103 -0
- package/src/HKT.ts +45 -0
- package/src/Hash.ts +195 -0
- package/src/HashMap.ts +519 -0
- package/src/HashRing.ts +317 -0
- package/src/HashSet.ts +2346 -0
- package/src/Inspectable.ts +287 -0
- package/src/Iterable.ts +1119 -0
- package/src/JSONSchema.ts +1044 -0
- package/src/KeyedPool.ts +167 -0
- package/src/Layer.ts +1228 -0
- package/src/LayerMap.ts +436 -0
- package/src/List.ts +977 -0
- package/src/LogLevel.ts +285 -0
- package/src/LogSpan.ts +25 -0
- package/src/Logger.ts +702 -0
- package/src/Mailbox.ts +268 -0
- package/src/ManagedRuntime.ts +180 -0
- package/src/Match.ts +1477 -0
- package/src/MergeDecision.ts +95 -0
- package/src/MergeState.ts +172 -0
- package/src/MergeStrategy.ts +107 -0
- package/src/Metric.ts +780 -0
- package/src/MetricBoundaries.ts +69 -0
- package/src/MetricHook.ts +151 -0
- package/src/MetricKey.ts +224 -0
- package/src/MetricKeyType.ts +262 -0
- package/src/MetricLabel.ts +47 -0
- package/src/MetricPair.ts +71 -0
- package/src/MetricPolling.ts +148 -0
- package/src/MetricRegistry.ts +48 -0
- package/src/MetricState.ts +257 -0
- package/src/Micro.ts +4405 -0
- package/src/ModuleVersion.ts +18 -0
- package/src/MutableHashMap.ts +411 -0
- package/src/MutableHashSet.ts +706 -0
- package/src/MutableList.ts +297 -0
- package/src/MutableQueue.ts +227 -0
- package/src/MutableRef.ts +202 -0
- package/src/NonEmptyIterable.ts +32 -0
- package/src/Number.ts +1071 -0
- package/src/Option.ts +2170 -0
- package/src/Order.ts +373 -0
- package/src/Ordering.ts +111 -0
- package/src/ParseResult.ts +2031 -0
- package/src/PartitionedSemaphore.ts +200 -0
- package/src/Pipeable.ts +566 -0
- package/src/Pool.ts +204 -0
- package/src/Predicate.ts +1405 -0
- package/src/Pretty.ts +205 -0
- package/src/PrimaryKey.ts +23 -0
- package/src/PubSub.ts +182 -0
- package/src/Queue.ts +644 -0
- package/src/Random.ts +204 -0
- package/src/RateLimiter.ts +138 -0
- package/src/RcMap.ts +141 -0
- package/src/RcRef.ts +122 -0
- package/src/Readable.ts +93 -0
- package/src/Record.ts +1274 -0
- package/src/RedBlackTree.ts +421 -0
- package/src/Redacted.ts +144 -0
- package/src/Ref.ts +180 -0
- package/src/RegExp.ts +38 -0
- package/src/Reloadable.ts +127 -0
- package/src/Request.ts +347 -0
- package/src/RequestBlock.ts +118 -0
- package/src/RequestResolver.ts +366 -0
- package/src/Resource.ts +119 -0
- package/src/Runtime.ts +383 -0
- package/src/RuntimeFlags.ts +336 -0
- package/src/RuntimeFlagsPatch.ts +183 -0
- package/src/STM.ts +2045 -0
- package/src/Schedule.ts +2219 -0
- package/src/ScheduleDecision.ts +62 -0
- package/src/ScheduleInterval.ts +151 -0
- package/src/ScheduleIntervals.ts +122 -0
- package/src/Scheduler.ts +353 -0
- package/src/Schema.ts +10914 -0
- package/src/SchemaAST.ts +3043 -0
- package/src/Scope.ts +204 -0
- package/src/ScopedCache.ts +151 -0
- package/src/ScopedRef.ts +117 -0
- package/src/Secret.ts +88 -0
- package/src/SingleProducerAsyncInput.ts +67 -0
- package/src/Sink.ts +1461 -0
- package/src/SortedMap.ts +287 -0
- package/src/SortedSet.ts +390 -0
- package/src/SourceLocation.ts +108 -0
- package/src/Stream.ts +6468 -0
- package/src/StreamEmit.ts +136 -0
- package/src/StreamHaltStrategy.ts +123 -0
- package/src/Streamable.ts +45 -0
- package/src/String.ts +778 -0
- package/src/Struct.ts +243 -0
- package/src/Subscribable.ts +100 -0
- package/src/SubscriptionRef.ts +298 -0
- package/src/Supervisor.ts +240 -0
- package/src/Symbol.ts +29 -0
- package/src/SynchronizedRef.ts +270 -0
- package/src/TArray.ts +495 -0
- package/src/TDeferred.ts +100 -0
- package/src/TMap.ts +515 -0
- package/src/TPriorityQueue.ts +223 -0
- package/src/TPubSub.ts +200 -0
- package/src/TQueue.ts +432 -0
- package/src/TRandom.ts +129 -0
- package/src/TReentrantLock.ts +224 -0
- package/src/TRef.ts +178 -0
- package/src/TSemaphore.ts +129 -0
- package/src/TSet.ts +365 -0
- package/src/TSubscriptionRef.ts +192 -0
- package/src/Take.ts +258 -0
- package/src/TestAnnotation.ts +158 -0
- package/src/TestAnnotationMap.ts +119 -0
- package/src/TestAnnotations.ts +117 -0
- package/src/TestClock.ts +556 -0
- package/src/TestConfig.ts +47 -0
- package/src/TestContext.ts +36 -0
- package/src/TestLive.ts +53 -0
- package/src/TestServices.ts +390 -0
- package/src/TestSized.ts +55 -0
- package/src/Tracer.ts +182 -0
- package/src/Trie.ts +840 -0
- package/src/Tuple.ts +305 -0
- package/src/Types.ts +353 -0
- package/src/Unify.ts +113 -0
- package/src/UpstreamPullRequest.ts +117 -0
- package/src/UpstreamPullStrategy.ts +121 -0
- package/src/Utils.ts +809 -0
- package/src/index.ts +1568 -0
- package/src/internal/array.ts +8 -0
- package/src/internal/blockedRequests.ts +520 -0
- package/src/internal/cache.ts +733 -0
- package/src/internal/cause.ts +1050 -0
- package/src/internal/channel/channelExecutor.ts +1200 -0
- package/src/internal/channel/channelState.ts +134 -0
- package/src/internal/channel/childExecutorDecision.ts +96 -0
- package/src/internal/channel/continuation.ts +200 -0
- package/src/internal/channel/mergeDecision.ts +113 -0
- package/src/internal/channel/mergeState.ts +120 -0
- package/src/internal/channel/mergeStrategy.ts +72 -0
- package/src/internal/channel/singleProducerAsyncInput.ts +259 -0
- package/src/internal/channel/subexecutor.ts +229 -0
- package/src/internal/channel/upstreamPullRequest.ts +84 -0
- package/src/internal/channel/upstreamPullStrategy.ts +87 -0
- package/src/internal/channel.ts +2603 -0
- package/src/internal/clock.ts +95 -0
- package/src/internal/completedRequestMap.ts +9 -0
- package/src/internal/concurrency.ts +54 -0
- package/src/internal/config.ts +716 -0
- package/src/internal/configError.ts +304 -0
- package/src/internal/configProvider/pathPatch.ts +97 -0
- package/src/internal/configProvider.ts +799 -0
- package/src/internal/console.ts +153 -0
- package/src/internal/context.ts +337 -0
- package/src/internal/core-effect.ts +2293 -0
- package/src/internal/core-stream.ts +998 -0
- package/src/internal/core.ts +3189 -0
- package/src/internal/data.ts +36 -0
- package/src/internal/dataSource.ts +327 -0
- package/src/internal/dateTime.ts +1277 -0
- package/src/internal/defaultServices/console.ts +100 -0
- package/src/internal/defaultServices.ts +163 -0
- package/src/internal/deferred.ts +46 -0
- package/src/internal/differ/chunkPatch.ts +211 -0
- package/src/internal/differ/contextPatch.ts +232 -0
- package/src/internal/differ/hashMapPatch.ts +220 -0
- package/src/internal/differ/hashSetPatch.ts +176 -0
- package/src/internal/differ/orPatch.ts +311 -0
- package/src/internal/differ/readonlyArrayPatch.ts +210 -0
- package/src/internal/differ.ts +200 -0
- package/src/internal/doNotation.ts +80 -0
- package/src/internal/effect/circular.ts +895 -0
- package/src/internal/effectable.ts +131 -0
- package/src/internal/either.ts +110 -0
- package/src/internal/encoding/base64.ts +286 -0
- package/src/internal/encoding/base64Url.ts +29 -0
- package/src/internal/encoding/common.ts +51 -0
- package/src/internal/encoding/hex.ts +315 -0
- package/src/internal/errors.ts +7 -0
- package/src/internal/executionPlan.ts +114 -0
- package/src/internal/executionStrategy.ts +74 -0
- package/src/internal/fiber.ts +388 -0
- package/src/internal/fiberId.ts +267 -0
- package/src/internal/fiberMessage.ts +82 -0
- package/src/internal/fiberRefs/patch.ts +144 -0
- package/src/internal/fiberRefs.ts +297 -0
- package/src/internal/fiberRuntime.ts +3842 -0
- package/src/internal/fiberScope.ts +71 -0
- package/src/internal/fiberStatus.ts +119 -0
- package/src/internal/groupBy.ts +530 -0
- package/src/internal/hashMap/array.ts +49 -0
- package/src/internal/hashMap/bitwise.ts +32 -0
- package/src/internal/hashMap/config.ts +14 -0
- package/src/internal/hashMap/keySet.ts +8 -0
- package/src/internal/hashMap/node.ts +391 -0
- package/src/internal/hashMap.ts +586 -0
- package/src/internal/hashSet.ts +323 -0
- package/src/internal/keyedPool.ts +244 -0
- package/src/internal/layer/circular.ts +214 -0
- package/src/internal/layer.ts +1483 -0
- package/src/internal/logSpan.ts +20 -0
- package/src/internal/logger-circular.ts +24 -0
- package/src/internal/logger.ts +522 -0
- package/src/internal/mailbox.ts +561 -0
- package/src/internal/managedRuntime/circular.ts +6 -0
- package/src/internal/managedRuntime.ts +134 -0
- package/src/internal/matcher.ts +652 -0
- package/src/internal/metric/boundaries.ts +75 -0
- package/src/internal/metric/hook.ts +483 -0
- package/src/internal/metric/key.ts +167 -0
- package/src/internal/metric/keyType.ts +238 -0
- package/src/internal/metric/label.ts +41 -0
- package/src/internal/metric/pair.ts +48 -0
- package/src/internal/metric/polling.ts +149 -0
- package/src/internal/metric/registry.ts +187 -0
- package/src/internal/metric/state.ts +290 -0
- package/src/internal/metric.ts +577 -0
- package/src/internal/opCodes/cause.ts +35 -0
- package/src/internal/opCodes/channel.ts +83 -0
- package/src/internal/opCodes/channelChildExecutorDecision.ts +17 -0
- package/src/internal/opCodes/channelMergeDecision.ts +11 -0
- package/src/internal/opCodes/channelMergeState.ts +17 -0
- package/src/internal/opCodes/channelMergeStrategy.ts +11 -0
- package/src/internal/opCodes/channelState.ts +23 -0
- package/src/internal/opCodes/channelUpstreamPullRequest.ts +11 -0
- package/src/internal/opCodes/channelUpstreamPullStrategy.ts +11 -0
- package/src/internal/opCodes/config.ts +65 -0
- package/src/internal/opCodes/configError.ts +35 -0
- package/src/internal/opCodes/continuation.ts +11 -0
- package/src/internal/opCodes/deferred.ts +11 -0
- package/src/internal/opCodes/effect.ts +89 -0
- package/src/internal/opCodes/layer.ts +59 -0
- package/src/internal/opCodes/streamHaltStrategy.ts +23 -0
- package/src/internal/option.ts +80 -0
- package/src/internal/pool.ts +432 -0
- package/src/internal/pubsub.ts +1762 -0
- package/src/internal/query.ts +204 -0
- package/src/internal/queue.ts +766 -0
- package/src/internal/random.ts +161 -0
- package/src/internal/rateLimiter.ts +93 -0
- package/src/internal/rcMap.ts +285 -0
- package/src/internal/rcRef.ts +192 -0
- package/src/internal/redBlackTree/iterator.ts +200 -0
- package/src/internal/redBlackTree/node.ts +68 -0
- package/src/internal/redBlackTree.ts +1245 -0
- package/src/internal/redacted.ts +73 -0
- package/src/internal/ref.ts +171 -0
- package/src/internal/reloadable.ts +140 -0
- package/src/internal/request.ts +177 -0
- package/src/internal/resource.ts +76 -0
- package/src/internal/ringBuffer.ts +68 -0
- package/src/internal/runtime.ts +558 -0
- package/src/internal/runtimeFlags.ts +178 -0
- package/src/internal/runtimeFlagsPatch.ts +103 -0
- package/src/internal/schedule/decision.ts +47 -0
- package/src/internal/schedule/interval.ts +101 -0
- package/src/internal/schedule/intervals.ts +180 -0
- package/src/internal/schedule.ts +2199 -0
- package/src/internal/schema/errors.ts +191 -0
- package/src/internal/schema/schemaId.ts +106 -0
- package/src/internal/schema/util.ts +50 -0
- package/src/internal/scopedCache.ts +644 -0
- package/src/internal/scopedRef.ts +118 -0
- package/src/internal/secret.ts +89 -0
- package/src/internal/singleShotGen.ts +35 -0
- package/src/internal/sink.ts +2120 -0
- package/src/internal/stack.ts +10 -0
- package/src/internal/stm/core.ts +817 -0
- package/src/internal/stm/entry.ts +59 -0
- package/src/internal/stm/journal.ts +123 -0
- package/src/internal/stm/opCodes/stm.ts +71 -0
- package/src/internal/stm/opCodes/stmState.ts +17 -0
- package/src/internal/stm/opCodes/strategy.ts +17 -0
- package/src/internal/stm/opCodes/tExit.ts +29 -0
- package/src/internal/stm/opCodes/tryCommit.ts +11 -0
- package/src/internal/stm/stm.ts +1453 -0
- package/src/internal/stm/stmState.ts +136 -0
- package/src/internal/stm/tArray.ts +550 -0
- package/src/internal/stm/tDeferred.ts +81 -0
- package/src/internal/stm/tExit.ts +190 -0
- package/src/internal/stm/tMap.ts +824 -0
- package/src/internal/stm/tPriorityQueue.ts +267 -0
- package/src/internal/stm/tPubSub.ts +551 -0
- package/src/internal/stm/tQueue.ts +393 -0
- package/src/internal/stm/tRandom.ts +140 -0
- package/src/internal/stm/tReentrantLock.ts +352 -0
- package/src/internal/stm/tRef.ts +195 -0
- package/src/internal/stm/tSemaphore.ts +113 -0
- package/src/internal/stm/tSet.ts +259 -0
- package/src/internal/stm/tSubscriptionRef.ts +286 -0
- package/src/internal/stm/tryCommit.ts +34 -0
- package/src/internal/stm/txnId.ts +14 -0
- package/src/internal/stm/versioned.ts +4 -0
- package/src/internal/stream/debounceState.ts +57 -0
- package/src/internal/stream/emit.ts +123 -0
- package/src/internal/stream/haltStrategy.ts +94 -0
- package/src/internal/stream/handoff.ts +187 -0
- package/src/internal/stream/handoffSignal.ts +59 -0
- package/src/internal/stream/pull.ts +34 -0
- package/src/internal/stream/sinkEndReason.ts +30 -0
- package/src/internal/stream/zipAllState.ts +88 -0
- package/src/internal/stream/zipChunksState.ts +56 -0
- package/src/internal/stream.ts +8801 -0
- package/src/internal/string-utils.ts +107 -0
- package/src/internal/subscriptionRef.ts +138 -0
- package/src/internal/supervisor/patch.ts +190 -0
- package/src/internal/supervisor.ts +303 -0
- package/src/internal/synchronizedRef.ts +114 -0
- package/src/internal/take.ts +199 -0
- package/src/internal/testing/sleep.ts +27 -0
- package/src/internal/testing/suspendedWarningData.ts +85 -0
- package/src/internal/testing/warningData.ts +94 -0
- package/src/internal/tracer.ts +150 -0
- package/src/internal/trie.ts +722 -0
- package/src/internal/version.ts +7 -0
package/src/Graph.ts
ADDED
|
@@ -0,0 +1,3732 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @experimental
|
|
3
|
+
* @since 3.18.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as Data from "./Data.js"
|
|
7
|
+
import * as Equal from "./Equal.js"
|
|
8
|
+
import { dual } from "./Function.js"
|
|
9
|
+
import * as Hash from "./Hash.js"
|
|
10
|
+
import type { Inspectable } from "./Inspectable.js"
|
|
11
|
+
import { format, NodeInspectSymbol } from "./Inspectable.js"
|
|
12
|
+
import * as Option from "./Option.js"
|
|
13
|
+
import type { Pipeable } from "./Pipeable.js"
|
|
14
|
+
import { pipeArguments } from "./Pipeable.js"
|
|
15
|
+
import type { Mutable } from "./Types.js"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Unique identifier for Graph instances.
|
|
19
|
+
*
|
|
20
|
+
* @since 3.18.0
|
|
21
|
+
* @category symbol
|
|
22
|
+
*/
|
|
23
|
+
export const TypeId: "~effect/Graph" = "~effect/Graph" as const
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Type identifier for Graph instances.
|
|
27
|
+
*
|
|
28
|
+
* @since 3.18.0
|
|
29
|
+
* @category symbol
|
|
30
|
+
*/
|
|
31
|
+
export type TypeId = typeof TypeId
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Node index for node identification using plain numbers.
|
|
35
|
+
*
|
|
36
|
+
* @since 3.18.0
|
|
37
|
+
* @category models
|
|
38
|
+
*/
|
|
39
|
+
export type NodeIndex = number
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Edge index for edge identification using plain numbers.
|
|
43
|
+
*
|
|
44
|
+
* @since 3.18.0
|
|
45
|
+
* @category models
|
|
46
|
+
*/
|
|
47
|
+
export type EdgeIndex = number
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Edge data containing source, target, and user data.
|
|
51
|
+
*
|
|
52
|
+
* @since 3.18.0
|
|
53
|
+
* @category models
|
|
54
|
+
*/
|
|
55
|
+
export class Edge<E> extends Data.Class<{
|
|
56
|
+
readonly source: NodeIndex
|
|
57
|
+
readonly target: NodeIndex
|
|
58
|
+
readonly data: E
|
|
59
|
+
}> {}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Graph type for distinguishing directed and undirected graphs.
|
|
63
|
+
*
|
|
64
|
+
* @since 3.18.0
|
|
65
|
+
* @category models
|
|
66
|
+
*/
|
|
67
|
+
export type Kind = "directed" | "undirected"
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Graph prototype interface.
|
|
71
|
+
*
|
|
72
|
+
* @since 3.18.0
|
|
73
|
+
* @category models
|
|
74
|
+
*/
|
|
75
|
+
export interface Proto<out N, out E> extends Iterable<readonly [NodeIndex, N]>, Equal.Equal, Pipeable, Inspectable {
|
|
76
|
+
readonly [TypeId]: TypeId
|
|
77
|
+
readonly nodes: Map<NodeIndex, N>
|
|
78
|
+
readonly edges: Map<EdgeIndex, Edge<E>>
|
|
79
|
+
readonly adjacency: Map<NodeIndex, Array<EdgeIndex>>
|
|
80
|
+
readonly reverseAdjacency: Map<NodeIndex, Array<EdgeIndex>>
|
|
81
|
+
nextNodeIndex: NodeIndex
|
|
82
|
+
nextEdgeIndex: EdgeIndex
|
|
83
|
+
isAcyclic: Option.Option<boolean>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Immutable graph interface.
|
|
88
|
+
*
|
|
89
|
+
* @since 3.18.0
|
|
90
|
+
* @category models
|
|
91
|
+
*/
|
|
92
|
+
export interface Graph<out N, out E, T extends Kind = "directed"> extends Proto<N, E> {
|
|
93
|
+
readonly type: T
|
|
94
|
+
readonly mutable: false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Mutable graph interface.
|
|
99
|
+
*
|
|
100
|
+
* @since 3.18.0
|
|
101
|
+
* @category models
|
|
102
|
+
*/
|
|
103
|
+
export interface MutableGraph<out N, out E, T extends Kind = "directed"> extends Proto<N, E> {
|
|
104
|
+
readonly type: T
|
|
105
|
+
readonly mutable: true
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Directed graph type alias.
|
|
110
|
+
*
|
|
111
|
+
* @since 3.18.0
|
|
112
|
+
* @category models
|
|
113
|
+
*/
|
|
114
|
+
export type DirectedGraph<N, E> = Graph<N, E, "directed">
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Undirected graph type alias.
|
|
118
|
+
*
|
|
119
|
+
* @since 3.18.0
|
|
120
|
+
* @category models
|
|
121
|
+
*/
|
|
122
|
+
export type UndirectedGraph<N, E> = Graph<N, E, "undirected">
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Mutable directed graph type alias.
|
|
126
|
+
*
|
|
127
|
+
* @since 3.18.0
|
|
128
|
+
* @category models
|
|
129
|
+
*/
|
|
130
|
+
export type MutableDirectedGraph<N, E> = MutableGraph<N, E, "directed">
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Mutable undirected graph type alias.
|
|
134
|
+
*
|
|
135
|
+
* @since 3.18.0
|
|
136
|
+
* @category models
|
|
137
|
+
*/
|
|
138
|
+
export type MutableUndirectedGraph<N, E> = MutableGraph<N, E, "undirected">
|
|
139
|
+
|
|
140
|
+
// =============================================================================
|
|
141
|
+
// Proto Objects
|
|
142
|
+
// =============================================================================
|
|
143
|
+
|
|
144
|
+
/** @internal */
|
|
145
|
+
const ProtoGraph = {
|
|
146
|
+
[TypeId]: TypeId,
|
|
147
|
+
[Symbol.iterator](this: Graph<any, any>) {
|
|
148
|
+
return this.nodes[Symbol.iterator]()
|
|
149
|
+
},
|
|
150
|
+
[NodeInspectSymbol](this: Graph<any, any>) {
|
|
151
|
+
return this.toJSON()
|
|
152
|
+
},
|
|
153
|
+
[Equal.symbol](this: Graph<any, any>, that: Equal.Equal): boolean {
|
|
154
|
+
if (isGraph(that)) {
|
|
155
|
+
if (
|
|
156
|
+
this.nodes.size !== that.nodes.size ||
|
|
157
|
+
this.edges.size !== that.edges.size ||
|
|
158
|
+
this.type !== that.type
|
|
159
|
+
) {
|
|
160
|
+
return false
|
|
161
|
+
}
|
|
162
|
+
// Compare nodes
|
|
163
|
+
for (const [nodeIndex, nodeData] of this.nodes) {
|
|
164
|
+
if (!that.nodes.has(nodeIndex)) {
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
const otherNodeData = that.nodes.get(nodeIndex)!
|
|
168
|
+
if (!Equal.equals(nodeData, otherNodeData)) {
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Compare edges
|
|
173
|
+
for (const [edgeIndex, edgeData] of this.edges) {
|
|
174
|
+
if (!that.edges.has(edgeIndex)) {
|
|
175
|
+
return false
|
|
176
|
+
}
|
|
177
|
+
const otherEdge = that.edges.get(edgeIndex)!
|
|
178
|
+
if (!Equal.equals(edgeData, otherEdge)) {
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return true
|
|
183
|
+
}
|
|
184
|
+
return false
|
|
185
|
+
},
|
|
186
|
+
[Hash.symbol](this: Graph<any, any>): number {
|
|
187
|
+
let hash = Hash.string("Graph")
|
|
188
|
+
hash = hash ^ Hash.string(this.type)
|
|
189
|
+
hash = hash ^ Hash.number(this.nodes.size)
|
|
190
|
+
hash = hash ^ Hash.number(this.edges.size)
|
|
191
|
+
for (const [nodeIndex, nodeData] of this.nodes) {
|
|
192
|
+
hash = hash ^ (Hash.hash(nodeIndex) + Hash.hash(nodeData))
|
|
193
|
+
}
|
|
194
|
+
for (const [edgeIndex, edgeData] of this.edges) {
|
|
195
|
+
hash = hash ^ (Hash.hash(edgeIndex) + Hash.hash(edgeData))
|
|
196
|
+
}
|
|
197
|
+
return hash
|
|
198
|
+
},
|
|
199
|
+
toJSON(this: Graph<any, any>) {
|
|
200
|
+
return {
|
|
201
|
+
_id: "Graph",
|
|
202
|
+
nodeCount: this.nodes.size,
|
|
203
|
+
edgeCount: this.edges.size,
|
|
204
|
+
type: this.type
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
toString(this: Graph<any, any>) {
|
|
208
|
+
return format(this)
|
|
209
|
+
},
|
|
210
|
+
pipe() {
|
|
211
|
+
return pipeArguments(this, arguments)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// =============================================================================
|
|
216
|
+
// Errors
|
|
217
|
+
// =============================================================================
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Error thrown when a graph operation fails.
|
|
221
|
+
*
|
|
222
|
+
* @since 3.18.0
|
|
223
|
+
* @category errors
|
|
224
|
+
*/
|
|
225
|
+
export class GraphError extends Data.TaggedError("GraphError")<{
|
|
226
|
+
readonly message: string
|
|
227
|
+
}> {}
|
|
228
|
+
|
|
229
|
+
/** @internal */
|
|
230
|
+
const missingNode = (node: number) => new GraphError({ message: `Node ${node} does not exist` })
|
|
231
|
+
|
|
232
|
+
// =============================================================================
|
|
233
|
+
// Constructors
|
|
234
|
+
// =============================================================================
|
|
235
|
+
|
|
236
|
+
/** @internal */
|
|
237
|
+
export const isGraph = (u: unknown): u is Graph<unknown, unknown> => typeof u === "object" && u !== null && TypeId in u
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Creates a directed graph, optionally with initial mutations.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```ts
|
|
244
|
+
* import { Graph } from "effect"
|
|
245
|
+
*
|
|
246
|
+
* // Directed graph with initial nodes and edges
|
|
247
|
+
* const graph = Graph.directed<string, string>((mutable) => {
|
|
248
|
+
* const a = Graph.addNode(mutable, "A")
|
|
249
|
+
* const b = Graph.addNode(mutable, "B")
|
|
250
|
+
* const c = Graph.addNode(mutable, "C")
|
|
251
|
+
* Graph.addEdge(mutable, a, b, "A->B")
|
|
252
|
+
* Graph.addEdge(mutable, b, c, "B->C")
|
|
253
|
+
* })
|
|
254
|
+
* ```
|
|
255
|
+
*
|
|
256
|
+
* @since 3.18.0
|
|
257
|
+
* @category constructors
|
|
258
|
+
*/
|
|
259
|
+
export const directed = <N, E>(mutate?: (mutable: MutableDirectedGraph<N, E>) => void): DirectedGraph<N, E> => {
|
|
260
|
+
const graph: Mutable<DirectedGraph<N, E>> = Object.create(ProtoGraph)
|
|
261
|
+
graph.type = "directed"
|
|
262
|
+
graph.nodes = new Map()
|
|
263
|
+
graph.edges = new Map()
|
|
264
|
+
graph.adjacency = new Map()
|
|
265
|
+
graph.reverseAdjacency = new Map()
|
|
266
|
+
graph.nextNodeIndex = 0
|
|
267
|
+
graph.nextEdgeIndex = 0
|
|
268
|
+
graph.isAcyclic = Option.some(true)
|
|
269
|
+
graph.mutable = false
|
|
270
|
+
|
|
271
|
+
if (mutate) {
|
|
272
|
+
const mutable = beginMutation(graph as DirectedGraph<N, E>)
|
|
273
|
+
mutate(mutable as MutableDirectedGraph<N, E>)
|
|
274
|
+
return endMutation(mutable)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return graph
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Creates an undirected graph, optionally with initial mutations.
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```ts
|
|
285
|
+
* import { Graph } from "effect"
|
|
286
|
+
*
|
|
287
|
+
* // Undirected graph with initial nodes and edges
|
|
288
|
+
* const graph = Graph.undirected<string, string>((mutable) => {
|
|
289
|
+
* const a = Graph.addNode(mutable, "A")
|
|
290
|
+
* const b = Graph.addNode(mutable, "B")
|
|
291
|
+
* const c = Graph.addNode(mutable, "C")
|
|
292
|
+
* Graph.addEdge(mutable, a, b, "A-B")
|
|
293
|
+
* Graph.addEdge(mutable, b, c, "B-C")
|
|
294
|
+
* })
|
|
295
|
+
* ```
|
|
296
|
+
*
|
|
297
|
+
* @since 3.18.0
|
|
298
|
+
* @category constructors
|
|
299
|
+
*/
|
|
300
|
+
export const undirected = <N, E>(mutate?: (mutable: MutableUndirectedGraph<N, E>) => void): UndirectedGraph<N, E> => {
|
|
301
|
+
const graph: Mutable<UndirectedGraph<N, E>> = Object.create(ProtoGraph)
|
|
302
|
+
graph.type = "undirected"
|
|
303
|
+
graph.nodes = new Map()
|
|
304
|
+
graph.edges = new Map()
|
|
305
|
+
graph.adjacency = new Map()
|
|
306
|
+
graph.reverseAdjacency = new Map()
|
|
307
|
+
graph.nextNodeIndex = 0
|
|
308
|
+
graph.nextEdgeIndex = 0
|
|
309
|
+
graph.isAcyclic = Option.some(true)
|
|
310
|
+
graph.mutable = false
|
|
311
|
+
|
|
312
|
+
if (mutate) {
|
|
313
|
+
const mutable = beginMutation(graph)
|
|
314
|
+
mutate(mutable as MutableUndirectedGraph<N, E>)
|
|
315
|
+
return endMutation(mutable)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return graph
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// =============================================================================
|
|
322
|
+
// Scoped Mutable API
|
|
323
|
+
// =============================================================================
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Creates a mutable scope for safe graph mutations by copying the data structure.
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```ts
|
|
330
|
+
* import { Graph } from "effect"
|
|
331
|
+
*
|
|
332
|
+
* const graph = Graph.directed<string, number>()
|
|
333
|
+
* const mutable = Graph.beginMutation(graph)
|
|
334
|
+
* // Now mutable can be safely modified without affecting original graph
|
|
335
|
+
* ```
|
|
336
|
+
*
|
|
337
|
+
* @since 3.18.0
|
|
338
|
+
* @category mutations
|
|
339
|
+
*/
|
|
340
|
+
export const beginMutation = <N, E, T extends Kind = "directed">(
|
|
341
|
+
graph: Graph<N, E, T>
|
|
342
|
+
): MutableGraph<N, E, T> => {
|
|
343
|
+
// Copy adjacency maps with deep cloned arrays
|
|
344
|
+
const adjacency = new Map<NodeIndex, Array<EdgeIndex>>()
|
|
345
|
+
const reverseAdjacency = new Map<NodeIndex, Array<EdgeIndex>>()
|
|
346
|
+
|
|
347
|
+
for (const [nodeIndex, edges] of graph.adjacency) {
|
|
348
|
+
adjacency.set(nodeIndex, [...edges])
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
for (const [nodeIndex, edges] of graph.reverseAdjacency) {
|
|
352
|
+
reverseAdjacency.set(nodeIndex, [...edges])
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const mutable: Mutable<MutableGraph<N, E, T>> = Object.create(ProtoGraph)
|
|
356
|
+
mutable.type = graph.type
|
|
357
|
+
mutable.nodes = new Map(graph.nodes)
|
|
358
|
+
mutable.edges = new Map(graph.edges)
|
|
359
|
+
mutable.adjacency = adjacency
|
|
360
|
+
mutable.reverseAdjacency = reverseAdjacency
|
|
361
|
+
mutable.nextNodeIndex = graph.nextNodeIndex
|
|
362
|
+
mutable.nextEdgeIndex = graph.nextEdgeIndex
|
|
363
|
+
mutable.isAcyclic = graph.isAcyclic
|
|
364
|
+
mutable.mutable = true
|
|
365
|
+
|
|
366
|
+
return mutable
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Converts a mutable graph back to an immutable graph, ending the mutation scope.
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```ts
|
|
374
|
+
* import { Graph } from "effect"
|
|
375
|
+
*
|
|
376
|
+
* const graph = Graph.directed<string, number>()
|
|
377
|
+
* const mutable = Graph.beginMutation(graph)
|
|
378
|
+
* // ... perform mutations on mutable ...
|
|
379
|
+
* const newGraph = Graph.endMutation(mutable)
|
|
380
|
+
* ```
|
|
381
|
+
*
|
|
382
|
+
* @since 3.18.0
|
|
383
|
+
* @category mutations
|
|
384
|
+
*/
|
|
385
|
+
export const endMutation = <N, E, T extends Kind = "directed">(
|
|
386
|
+
mutable: MutableGraph<N, E, T>
|
|
387
|
+
): Graph<N, E, T> => {
|
|
388
|
+
const graph: Mutable<Graph<N, E, T>> = Object.create(ProtoGraph)
|
|
389
|
+
graph.type = mutable.type
|
|
390
|
+
graph.nodes = new Map(mutable.nodes)
|
|
391
|
+
graph.edges = new Map(mutable.edges)
|
|
392
|
+
graph.adjacency = mutable.adjacency
|
|
393
|
+
graph.reverseAdjacency = mutable.reverseAdjacency
|
|
394
|
+
graph.nextNodeIndex = mutable.nextNodeIndex
|
|
395
|
+
graph.nextEdgeIndex = mutable.nextEdgeIndex
|
|
396
|
+
graph.isAcyclic = mutable.isAcyclic
|
|
397
|
+
graph.mutable = false
|
|
398
|
+
|
|
399
|
+
return graph
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Performs scoped mutations on a graph, automatically managing the mutation lifecycle.
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* ```ts
|
|
407
|
+
* import { Graph } from "effect"
|
|
408
|
+
*
|
|
409
|
+
* const graph = Graph.directed<string, number>()
|
|
410
|
+
* const newGraph = Graph.mutate(graph, (mutable) => {
|
|
411
|
+
* // Safe mutations go here
|
|
412
|
+
* // mutable gets automatically converted back to immutable
|
|
413
|
+
* })
|
|
414
|
+
* ```
|
|
415
|
+
*
|
|
416
|
+
* @since 3.18.0
|
|
417
|
+
* @category mutations
|
|
418
|
+
*/
|
|
419
|
+
export const mutate: {
|
|
420
|
+
<N, E, T extends Kind = "directed">(
|
|
421
|
+
f: (mutable: MutableGraph<N, E, T>) => void
|
|
422
|
+
): (graph: Graph<N, E, T>) => Graph<N, E, T>
|
|
423
|
+
<N, E, T extends Kind = "directed">(
|
|
424
|
+
graph: Graph<N, E, T>,
|
|
425
|
+
f: (mutable: MutableGraph<N, E, T>) => void
|
|
426
|
+
): Graph<N, E, T>
|
|
427
|
+
} = dual(2, <N, E, T extends Kind = "directed">(
|
|
428
|
+
graph: Graph<N, E, T>,
|
|
429
|
+
f: (mutable: MutableGraph<N, E, T>) => void
|
|
430
|
+
): Graph<N, E, T> => {
|
|
431
|
+
const mutable = beginMutation(graph)
|
|
432
|
+
f(mutable)
|
|
433
|
+
return endMutation(mutable)
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
// =============================================================================
|
|
437
|
+
// Basic Node Operations
|
|
438
|
+
// =============================================================================
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Adds a new node to a mutable graph and returns its index.
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* ```ts
|
|
445
|
+
* import { Graph } from "effect"
|
|
446
|
+
*
|
|
447
|
+
* const result = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
|
|
448
|
+
* const nodeA = Graph.addNode(mutable, "Node A")
|
|
449
|
+
* const nodeB = Graph.addNode(mutable, "Node B")
|
|
450
|
+
* console.log(nodeA) // NodeIndex with value 0
|
|
451
|
+
* console.log(nodeB) // NodeIndex with value 1
|
|
452
|
+
* })
|
|
453
|
+
* ```
|
|
454
|
+
*
|
|
455
|
+
* @since 3.18.0
|
|
456
|
+
* @category mutations
|
|
457
|
+
*/
|
|
458
|
+
export const addNode = <N, E, T extends Kind = "directed">(
|
|
459
|
+
mutable: MutableGraph<N, E, T>,
|
|
460
|
+
data: N
|
|
461
|
+
): NodeIndex => {
|
|
462
|
+
const nodeIndex = mutable.nextNodeIndex
|
|
463
|
+
|
|
464
|
+
// Add node data
|
|
465
|
+
mutable.nodes.set(nodeIndex, data)
|
|
466
|
+
|
|
467
|
+
// Initialize empty adjacency lists
|
|
468
|
+
mutable.adjacency.set(nodeIndex, [])
|
|
469
|
+
mutable.reverseAdjacency.set(nodeIndex, [])
|
|
470
|
+
|
|
471
|
+
// Update graph allocators
|
|
472
|
+
mutable.nextNodeIndex = mutable.nextNodeIndex + 1
|
|
473
|
+
|
|
474
|
+
return nodeIndex
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Gets the data associated with a node index, if it exists.
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```ts
|
|
482
|
+
* import { Graph, Option } from "effect"
|
|
483
|
+
*
|
|
484
|
+
* const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
|
|
485
|
+
* Graph.addNode(mutable, "Node A")
|
|
486
|
+
* })
|
|
487
|
+
*
|
|
488
|
+
* const nodeIndex = 0
|
|
489
|
+
* const nodeData = Graph.getNode(graph, nodeIndex)
|
|
490
|
+
*
|
|
491
|
+
* if (Option.isSome(nodeData)) {
|
|
492
|
+
* console.log(nodeData.value) // "Node A"
|
|
493
|
+
* }
|
|
494
|
+
* ```
|
|
495
|
+
*
|
|
496
|
+
* @since 3.18.0
|
|
497
|
+
* @category getters
|
|
498
|
+
*/
|
|
499
|
+
export const getNode = <N, E, T extends Kind = "directed">(
|
|
500
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
501
|
+
nodeIndex: NodeIndex
|
|
502
|
+
): Option.Option<N> => graph.nodes.has(nodeIndex) ? Option.some(graph.nodes.get(nodeIndex)!) : Option.none()
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Checks if a node with the given index exists in the graph.
|
|
506
|
+
*
|
|
507
|
+
* @example
|
|
508
|
+
* ```ts
|
|
509
|
+
* import { Graph } from "effect"
|
|
510
|
+
*
|
|
511
|
+
* const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
|
|
512
|
+
* Graph.addNode(mutable, "Node A")
|
|
513
|
+
* })
|
|
514
|
+
*
|
|
515
|
+
* const nodeIndex = 0
|
|
516
|
+
* const exists = Graph.hasNode(graph, nodeIndex)
|
|
517
|
+
* console.log(exists) // true
|
|
518
|
+
*
|
|
519
|
+
* const nonExistentIndex = 999
|
|
520
|
+
* const notExists = Graph.hasNode(graph, nonExistentIndex)
|
|
521
|
+
* console.log(notExists) // false
|
|
522
|
+
* ```
|
|
523
|
+
*
|
|
524
|
+
* @since 3.18.0
|
|
525
|
+
* @category getters
|
|
526
|
+
*/
|
|
527
|
+
export const hasNode = <N, E, T extends Kind = "directed">(
|
|
528
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
529
|
+
nodeIndex: NodeIndex
|
|
530
|
+
): boolean => graph.nodes.has(nodeIndex)
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Returns the number of nodes in the graph.
|
|
534
|
+
*
|
|
535
|
+
* @example
|
|
536
|
+
* ```ts
|
|
537
|
+
* import { Graph } from "effect"
|
|
538
|
+
*
|
|
539
|
+
* const emptyGraph = Graph.directed<string, number>()
|
|
540
|
+
* console.log(Graph.nodeCount(emptyGraph)) // 0
|
|
541
|
+
*
|
|
542
|
+
* const graphWithNodes = Graph.mutate(emptyGraph, (mutable) => {
|
|
543
|
+
* Graph.addNode(mutable, "Node A")
|
|
544
|
+
* Graph.addNode(mutable, "Node B")
|
|
545
|
+
* Graph.addNode(mutable, "Node C")
|
|
546
|
+
* })
|
|
547
|
+
*
|
|
548
|
+
* console.log(Graph.nodeCount(graphWithNodes)) // 3
|
|
549
|
+
* ```
|
|
550
|
+
*
|
|
551
|
+
* @since 3.18.0
|
|
552
|
+
* @category getters
|
|
553
|
+
*/
|
|
554
|
+
export const nodeCount = <N, E, T extends Kind = "directed">(
|
|
555
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>
|
|
556
|
+
): number => graph.nodes.size
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Finds the first node that matches the given predicate.
|
|
560
|
+
*
|
|
561
|
+
* @example
|
|
562
|
+
* ```ts
|
|
563
|
+
* import { Graph, Option } from "effect"
|
|
564
|
+
*
|
|
565
|
+
* const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
|
|
566
|
+
* Graph.addNode(mutable, "Node A")
|
|
567
|
+
* Graph.addNode(mutable, "Node B")
|
|
568
|
+
* Graph.addNode(mutable, "Node C")
|
|
569
|
+
* })
|
|
570
|
+
*
|
|
571
|
+
* const result = Graph.findNode(graph, (data) => data.startsWith("Node B"))
|
|
572
|
+
* console.log(result) // Option.some(1)
|
|
573
|
+
*
|
|
574
|
+
* const notFound = Graph.findNode(graph, (data) => data === "Node D")
|
|
575
|
+
* console.log(notFound) // Option.none()
|
|
576
|
+
* ```
|
|
577
|
+
*
|
|
578
|
+
* @since 3.18.0
|
|
579
|
+
* @category getters
|
|
580
|
+
*/
|
|
581
|
+
export const findNode = <N, E, T extends Kind = "directed">(
|
|
582
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
583
|
+
predicate: (data: N) => boolean
|
|
584
|
+
): Option.Option<NodeIndex> => {
|
|
585
|
+
for (const [index, data] of graph.nodes) {
|
|
586
|
+
if (predicate(data)) {
|
|
587
|
+
return Option.some(index)
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return Option.none()
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Finds all nodes that match the given predicate.
|
|
595
|
+
*
|
|
596
|
+
* @example
|
|
597
|
+
* ```ts
|
|
598
|
+
* import { Graph } from "effect"
|
|
599
|
+
*
|
|
600
|
+
* const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
|
|
601
|
+
* Graph.addNode(mutable, "Start A")
|
|
602
|
+
* Graph.addNode(mutable, "Node B")
|
|
603
|
+
* Graph.addNode(mutable, "Start C")
|
|
604
|
+
* })
|
|
605
|
+
*
|
|
606
|
+
* const result = Graph.findNodes(graph, (data) => data.startsWith("Start"))
|
|
607
|
+
* console.log(result) // [0, 2]
|
|
608
|
+
*
|
|
609
|
+
* const empty = Graph.findNodes(graph, (data) => data === "Not Found")
|
|
610
|
+
* console.log(empty) // []
|
|
611
|
+
* ```
|
|
612
|
+
*
|
|
613
|
+
* @since 3.18.0
|
|
614
|
+
* @category getters
|
|
615
|
+
*/
|
|
616
|
+
export const findNodes = <N, E, T extends Kind = "directed">(
|
|
617
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
618
|
+
predicate: (data: N) => boolean
|
|
619
|
+
): Array<NodeIndex> => {
|
|
620
|
+
const results: Array<NodeIndex> = []
|
|
621
|
+
for (const [index, data] of graph.nodes) {
|
|
622
|
+
if (predicate(data)) {
|
|
623
|
+
results.push(index)
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return results
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Finds the first edge that matches the given predicate.
|
|
631
|
+
*
|
|
632
|
+
* @example
|
|
633
|
+
* ```ts
|
|
634
|
+
* import { Graph, Option } from "effect"
|
|
635
|
+
*
|
|
636
|
+
* const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
|
|
637
|
+
* const nodeA = Graph.addNode(mutable, "Node A")
|
|
638
|
+
* const nodeB = Graph.addNode(mutable, "Node B")
|
|
639
|
+
* const nodeC = Graph.addNode(mutable, "Node C")
|
|
640
|
+
* Graph.addEdge(mutable, nodeA, nodeB, 10)
|
|
641
|
+
* Graph.addEdge(mutable, nodeB, nodeC, 20)
|
|
642
|
+
* })
|
|
643
|
+
*
|
|
644
|
+
* const result = Graph.findEdge(graph, (data) => data > 15)
|
|
645
|
+
* console.log(result) // Option.some(1)
|
|
646
|
+
*
|
|
647
|
+
* const notFound = Graph.findEdge(graph, (data) => data > 100)
|
|
648
|
+
* console.log(notFound) // Option.none()
|
|
649
|
+
* ```
|
|
650
|
+
*
|
|
651
|
+
* @since 3.18.0
|
|
652
|
+
* @category getters
|
|
653
|
+
*/
|
|
654
|
+
export const findEdge = <N, E, T extends Kind = "directed">(
|
|
655
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
656
|
+
predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean
|
|
657
|
+
): Option.Option<EdgeIndex> => {
|
|
658
|
+
for (const [edgeIndex, edgeData] of graph.edges) {
|
|
659
|
+
if (predicate(edgeData.data, edgeData.source, edgeData.target)) {
|
|
660
|
+
return Option.some(edgeIndex)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return Option.none()
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Finds all edges that match the given predicate.
|
|
668
|
+
*
|
|
669
|
+
* @example
|
|
670
|
+
* ```ts
|
|
671
|
+
* import { Graph } from "effect"
|
|
672
|
+
*
|
|
673
|
+
* const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
|
|
674
|
+
* const nodeA = Graph.addNode(mutable, "Node A")
|
|
675
|
+
* const nodeB = Graph.addNode(mutable, "Node B")
|
|
676
|
+
* const nodeC = Graph.addNode(mutable, "Node C")
|
|
677
|
+
* Graph.addEdge(mutable, nodeA, nodeB, 10)
|
|
678
|
+
* Graph.addEdge(mutable, nodeB, nodeC, 20)
|
|
679
|
+
* Graph.addEdge(mutable, nodeC, nodeA, 30)
|
|
680
|
+
* })
|
|
681
|
+
*
|
|
682
|
+
* const result = Graph.findEdges(graph, (data) => data >= 20)
|
|
683
|
+
* console.log(result) // [1, 2]
|
|
684
|
+
*
|
|
685
|
+
* const empty = Graph.findEdges(graph, (data) => data > 100)
|
|
686
|
+
* console.log(empty) // []
|
|
687
|
+
* ```
|
|
688
|
+
*
|
|
689
|
+
* @since 3.18.0
|
|
690
|
+
* @category getters
|
|
691
|
+
*/
|
|
692
|
+
export const findEdges = <N, E, T extends Kind = "directed">(
|
|
693
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
694
|
+
predicate: (data: E, source: NodeIndex, target: NodeIndex) => boolean
|
|
695
|
+
): Array<EdgeIndex> => {
|
|
696
|
+
const results: Array<EdgeIndex> = []
|
|
697
|
+
for (const [edgeIndex, edgeData] of graph.edges) {
|
|
698
|
+
if (predicate(edgeData.data, edgeData.source, edgeData.target)) {
|
|
699
|
+
results.push(edgeIndex)
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return results
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Updates a single node's data by applying a transformation function.
|
|
707
|
+
*
|
|
708
|
+
* @example
|
|
709
|
+
* ```ts
|
|
710
|
+
* import { Graph } from "effect"
|
|
711
|
+
*
|
|
712
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
713
|
+
* Graph.addNode(mutable, "Node A")
|
|
714
|
+
* Graph.addNode(mutable, "Node B")
|
|
715
|
+
* Graph.updateNode(mutable, 0, (data) => data.toUpperCase())
|
|
716
|
+
* })
|
|
717
|
+
*
|
|
718
|
+
* const nodeData = Graph.getNode(graph, 0)
|
|
719
|
+
* console.log(nodeData) // Option.some("NODE A")
|
|
720
|
+
* ```
|
|
721
|
+
*
|
|
722
|
+
* @since 3.18.0
|
|
723
|
+
* @category transformations
|
|
724
|
+
*/
|
|
725
|
+
export const updateNode = <N, E, T extends Kind = "directed">(
|
|
726
|
+
mutable: MutableGraph<N, E, T>,
|
|
727
|
+
index: NodeIndex,
|
|
728
|
+
f: (data: N) => N
|
|
729
|
+
): void => {
|
|
730
|
+
if (!mutable.nodes.has(index)) {
|
|
731
|
+
return
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const currentData = mutable.nodes.get(index)!
|
|
735
|
+
const newData = f(currentData)
|
|
736
|
+
mutable.nodes.set(index, newData)
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Updates a single edge's data by applying a transformation function.
|
|
741
|
+
*
|
|
742
|
+
* @example
|
|
743
|
+
* ```ts
|
|
744
|
+
* import { Graph } from "effect"
|
|
745
|
+
*
|
|
746
|
+
* const result = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
|
|
747
|
+
* const nodeA = Graph.addNode(mutable, "Node A")
|
|
748
|
+
* const nodeB = Graph.addNode(mutable, "Node B")
|
|
749
|
+
* const edgeIndex = Graph.addEdge(mutable, nodeA, nodeB, 10)
|
|
750
|
+
* Graph.updateEdge(mutable, edgeIndex, (data) => data * 2)
|
|
751
|
+
* })
|
|
752
|
+
*
|
|
753
|
+
* const edgeData = Graph.getEdge(result, 0)
|
|
754
|
+
* console.log(edgeData) // Option.some({ source: 0, target: 1, data: 20 })
|
|
755
|
+
* ```
|
|
756
|
+
*
|
|
757
|
+
* @since 3.18.0
|
|
758
|
+
* @category mutations
|
|
759
|
+
*/
|
|
760
|
+
export const updateEdge = <N, E, T extends Kind = "directed">(
|
|
761
|
+
mutable: MutableGraph<N, E, T>,
|
|
762
|
+
edgeIndex: EdgeIndex,
|
|
763
|
+
f: (data: E) => E
|
|
764
|
+
): void => {
|
|
765
|
+
if (!mutable.edges.has(edgeIndex)) {
|
|
766
|
+
return
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const currentEdge = mutable.edges.get(edgeIndex)!
|
|
770
|
+
const newData = f(currentEdge.data)
|
|
771
|
+
mutable.edges.set(edgeIndex, {
|
|
772
|
+
...currentEdge,
|
|
773
|
+
data: newData
|
|
774
|
+
})
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Creates a new graph with transformed node data using the provided mapping function.
|
|
779
|
+
*
|
|
780
|
+
* @example
|
|
781
|
+
* ```ts
|
|
782
|
+
* import { Graph } from "effect"
|
|
783
|
+
*
|
|
784
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
785
|
+
* Graph.addNode(mutable, "node a")
|
|
786
|
+
* Graph.addNode(mutable, "node b")
|
|
787
|
+
* Graph.addNode(mutable, "node c")
|
|
788
|
+
* Graph.mapNodes(mutable, (data) => data.toUpperCase())
|
|
789
|
+
* })
|
|
790
|
+
*
|
|
791
|
+
* const nodeData = Graph.getNode(graph, 0)
|
|
792
|
+
* console.log(nodeData) // Option.some("NODE A")
|
|
793
|
+
* ```
|
|
794
|
+
*
|
|
795
|
+
* @since 3.18.0
|
|
796
|
+
* @category transformations
|
|
797
|
+
*/
|
|
798
|
+
export const mapNodes = <N, E, T extends Kind = "directed">(
|
|
799
|
+
mutable: MutableGraph<N, E, T>,
|
|
800
|
+
f: (data: N) => N
|
|
801
|
+
): void => {
|
|
802
|
+
// Transform existing node data in place
|
|
803
|
+
for (const [index, data] of mutable.nodes) {
|
|
804
|
+
const newData = f(data)
|
|
805
|
+
mutable.nodes.set(index, newData)
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Transforms all edge data in a mutable graph using the provided mapping function.
|
|
811
|
+
*
|
|
812
|
+
* @example
|
|
813
|
+
* ```ts
|
|
814
|
+
* import { Graph } from "effect"
|
|
815
|
+
*
|
|
816
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
817
|
+
* const a = Graph.addNode(mutable, "A")
|
|
818
|
+
* const b = Graph.addNode(mutable, "B")
|
|
819
|
+
* const c = Graph.addNode(mutable, "C")
|
|
820
|
+
* Graph.addEdge(mutable, a, b, 10)
|
|
821
|
+
* Graph.addEdge(mutable, b, c, 20)
|
|
822
|
+
* Graph.mapEdges(mutable, (data) => data * 2)
|
|
823
|
+
* })
|
|
824
|
+
*
|
|
825
|
+
* const edgeData = Graph.getEdge(graph, 0)
|
|
826
|
+
* console.log(edgeData) // Option.some({ source: 0, target: 1, data: 20 })
|
|
827
|
+
* ```
|
|
828
|
+
*
|
|
829
|
+
* @since 3.18.0
|
|
830
|
+
* @category transformations
|
|
831
|
+
*/
|
|
832
|
+
export const mapEdges = <N, E, T extends Kind = "directed">(
|
|
833
|
+
mutable: MutableGraph<N, E, T>,
|
|
834
|
+
f: (data: E) => E
|
|
835
|
+
): void => {
|
|
836
|
+
// Transform existing edge data in place
|
|
837
|
+
for (const [index, edgeData] of mutable.edges) {
|
|
838
|
+
const newData = f(edgeData.data)
|
|
839
|
+
mutable.edges.set(index, {
|
|
840
|
+
...edgeData,
|
|
841
|
+
data: newData
|
|
842
|
+
})
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Reverses all edge directions in a mutable graph by swapping source and target nodes.
|
|
848
|
+
*
|
|
849
|
+
* @example
|
|
850
|
+
* ```ts
|
|
851
|
+
* import { Graph } from "effect"
|
|
852
|
+
*
|
|
853
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
854
|
+
* const a = Graph.addNode(mutable, "A")
|
|
855
|
+
* const b = Graph.addNode(mutable, "B")
|
|
856
|
+
* const c = Graph.addNode(mutable, "C")
|
|
857
|
+
* Graph.addEdge(mutable, a, b, 1) // A -> B
|
|
858
|
+
* Graph.addEdge(mutable, b, c, 2) // B -> C
|
|
859
|
+
* Graph.reverse(mutable) // Now B -> A, C -> B
|
|
860
|
+
* })
|
|
861
|
+
*
|
|
862
|
+
* const edge0 = Graph.getEdge(graph, 0)
|
|
863
|
+
* console.log(edge0) // Option.some({ source: 1, target: 0, data: 1 }) - B -> A
|
|
864
|
+
* ```
|
|
865
|
+
*
|
|
866
|
+
* @since 3.18.0
|
|
867
|
+
* @category transformations
|
|
868
|
+
*/
|
|
869
|
+
export const reverse = <N, E, T extends Kind = "directed">(
|
|
870
|
+
mutable: MutableGraph<N, E, T>
|
|
871
|
+
): void => {
|
|
872
|
+
// Reverse all edges by swapping source and target
|
|
873
|
+
for (const [index, edgeData] of mutable.edges) {
|
|
874
|
+
mutable.edges.set(index, {
|
|
875
|
+
source: edgeData.target,
|
|
876
|
+
target: edgeData.source,
|
|
877
|
+
data: edgeData.data
|
|
878
|
+
})
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Clear and rebuild adjacency lists with reversed directions
|
|
882
|
+
mutable.adjacency.clear()
|
|
883
|
+
mutable.reverseAdjacency.clear()
|
|
884
|
+
|
|
885
|
+
// Rebuild adjacency lists with reversed directions
|
|
886
|
+
for (const [edgeIndex, edgeData] of mutable.edges) {
|
|
887
|
+
// Add to forward adjacency (source -> target)
|
|
888
|
+
const sourceEdges = mutable.adjacency.get(edgeData.source) || []
|
|
889
|
+
sourceEdges.push(edgeIndex)
|
|
890
|
+
mutable.adjacency.set(edgeData.source, sourceEdges)
|
|
891
|
+
|
|
892
|
+
// Add to reverse adjacency (target <- source)
|
|
893
|
+
const targetEdges = mutable.reverseAdjacency.get(edgeData.target) || []
|
|
894
|
+
targetEdges.push(edgeIndex)
|
|
895
|
+
mutable.reverseAdjacency.set(edgeData.target, targetEdges)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Invalidate cycle flag since edge directions changed
|
|
899
|
+
mutable.isAcyclic = Option.none()
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Filters and optionally transforms nodes in a mutable graph using a predicate function.
|
|
904
|
+
* Nodes that return Option.none are removed along with all their connected edges.
|
|
905
|
+
*
|
|
906
|
+
* @example
|
|
907
|
+
* ```ts
|
|
908
|
+
* import { Graph, Option } from "effect"
|
|
909
|
+
*
|
|
910
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
911
|
+
* const a = Graph.addNode(mutable, "active")
|
|
912
|
+
* const b = Graph.addNode(mutable, "inactive")
|
|
913
|
+
* const c = Graph.addNode(mutable, "active")
|
|
914
|
+
* Graph.addEdge(mutable, a, b, 1)
|
|
915
|
+
* Graph.addEdge(mutable, b, c, 2)
|
|
916
|
+
*
|
|
917
|
+
* // Keep only "active" nodes and transform to uppercase
|
|
918
|
+
* Graph.filterMapNodes(mutable, (data) =>
|
|
919
|
+
* data === "active" ? Option.some(data.toUpperCase()) : Option.none()
|
|
920
|
+
* )
|
|
921
|
+
* })
|
|
922
|
+
*
|
|
923
|
+
* console.log(Graph.nodeCount(graph)) // 2 (only "active" nodes remain)
|
|
924
|
+
* ```
|
|
925
|
+
*
|
|
926
|
+
* @since 3.18.0
|
|
927
|
+
* @category transformations
|
|
928
|
+
*/
|
|
929
|
+
export const filterMapNodes = <N, E, T extends Kind = "directed">(
|
|
930
|
+
mutable: MutableGraph<N, E, T>,
|
|
931
|
+
f: (data: N) => Option.Option<N>
|
|
932
|
+
): void => {
|
|
933
|
+
const nodesToRemove: Array<NodeIndex> = []
|
|
934
|
+
|
|
935
|
+
// First pass: identify nodes to remove and transform data for nodes to keep
|
|
936
|
+
for (const [index, data] of mutable.nodes) {
|
|
937
|
+
const result = f(data)
|
|
938
|
+
if (Option.isSome(result)) {
|
|
939
|
+
// Transform node data
|
|
940
|
+
mutable.nodes.set(index, result.value)
|
|
941
|
+
} else {
|
|
942
|
+
// Mark for removal
|
|
943
|
+
nodesToRemove.push(index)
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Second pass: remove filtered out nodes and their edges
|
|
948
|
+
for (const nodeIndex of nodesToRemove) {
|
|
949
|
+
removeNode(mutable, nodeIndex)
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Filters and optionally transforms edges in a mutable graph using a predicate function.
|
|
955
|
+
* Edges that return Option.none are removed from the graph.
|
|
956
|
+
*
|
|
957
|
+
* @example
|
|
958
|
+
* ```ts
|
|
959
|
+
* import { Graph, Option } from "effect"
|
|
960
|
+
*
|
|
961
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
962
|
+
* const a = Graph.addNode(mutable, "A")
|
|
963
|
+
* const b = Graph.addNode(mutable, "B")
|
|
964
|
+
* const c = Graph.addNode(mutable, "C")
|
|
965
|
+
* Graph.addEdge(mutable, a, b, 5)
|
|
966
|
+
* Graph.addEdge(mutable, b, c, 15)
|
|
967
|
+
* Graph.addEdge(mutable, c, a, 25)
|
|
968
|
+
*
|
|
969
|
+
* // Keep only edges with weight >= 10 and double their weight
|
|
970
|
+
* Graph.filterMapEdges(mutable, (data) =>
|
|
971
|
+
* data >= 10 ? Option.some(data * 2) : Option.none()
|
|
972
|
+
* )
|
|
973
|
+
* })
|
|
974
|
+
*
|
|
975
|
+
* console.log(Graph.edgeCount(graph)) // 2 (edges with weight 5 removed)
|
|
976
|
+
* ```
|
|
977
|
+
*
|
|
978
|
+
* @since 3.18.0
|
|
979
|
+
* @category transformations
|
|
980
|
+
*/
|
|
981
|
+
export const filterMapEdges = <N, E, T extends Kind = "directed">(
|
|
982
|
+
mutable: MutableGraph<N, E, T>,
|
|
983
|
+
f: (data: E) => Option.Option<E>
|
|
984
|
+
): void => {
|
|
985
|
+
const edgesToRemove: Array<EdgeIndex> = []
|
|
986
|
+
|
|
987
|
+
// First pass: identify edges to remove and transform data for edges to keep
|
|
988
|
+
for (const [index, edgeData] of mutable.edges) {
|
|
989
|
+
const result = f(edgeData.data)
|
|
990
|
+
if (Option.isSome(result)) {
|
|
991
|
+
// Transform edge data
|
|
992
|
+
mutable.edges.set(index, {
|
|
993
|
+
...edgeData,
|
|
994
|
+
data: result.value
|
|
995
|
+
})
|
|
996
|
+
} else {
|
|
997
|
+
// Mark for removal
|
|
998
|
+
edgesToRemove.push(index)
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Second pass: remove filtered out edges
|
|
1003
|
+
for (const edgeIndex of edgesToRemove) {
|
|
1004
|
+
removeEdge(mutable, edgeIndex)
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Filters nodes by removing those that don't match the predicate.
|
|
1010
|
+
* This function modifies the mutable graph in place.
|
|
1011
|
+
*
|
|
1012
|
+
* @example
|
|
1013
|
+
* ```ts
|
|
1014
|
+
* import { Graph } from "effect"
|
|
1015
|
+
*
|
|
1016
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
1017
|
+
* Graph.addNode(mutable, "active")
|
|
1018
|
+
* Graph.addNode(mutable, "inactive")
|
|
1019
|
+
* Graph.addNode(mutable, "pending")
|
|
1020
|
+
* Graph.addNode(mutable, "active")
|
|
1021
|
+
*
|
|
1022
|
+
* // Keep only "active" nodes
|
|
1023
|
+
* Graph.filterNodes(mutable, (data) => data === "active")
|
|
1024
|
+
* })
|
|
1025
|
+
*
|
|
1026
|
+
* console.log(Graph.nodeCount(graph)) // 2 (only "active" nodes remain)
|
|
1027
|
+
* ```
|
|
1028
|
+
*
|
|
1029
|
+
* @since 3.18.0
|
|
1030
|
+
* @category transformations
|
|
1031
|
+
*/
|
|
1032
|
+
export const filterNodes = <N, E, T extends Kind = "directed">(
|
|
1033
|
+
mutable: MutableGraph<N, E, T>,
|
|
1034
|
+
predicate: (data: N) => boolean
|
|
1035
|
+
): void => {
|
|
1036
|
+
const nodesToRemove: Array<NodeIndex> = []
|
|
1037
|
+
|
|
1038
|
+
// Identify nodes to remove
|
|
1039
|
+
for (const [index, data] of mutable.nodes) {
|
|
1040
|
+
if (!predicate(data)) {
|
|
1041
|
+
nodesToRemove.push(index)
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Remove filtered out nodes (this also removes connected edges)
|
|
1046
|
+
for (const nodeIndex of nodesToRemove) {
|
|
1047
|
+
removeNode(mutable, nodeIndex)
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Filters edges by removing those that don't match the predicate.
|
|
1053
|
+
* This function modifies the mutable graph in place.
|
|
1054
|
+
*
|
|
1055
|
+
* @example
|
|
1056
|
+
* ```ts
|
|
1057
|
+
* import { Graph } from "effect"
|
|
1058
|
+
*
|
|
1059
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
1060
|
+
* const a = Graph.addNode(mutable, "A")
|
|
1061
|
+
* const b = Graph.addNode(mutable, "B")
|
|
1062
|
+
* const c = Graph.addNode(mutable, "C")
|
|
1063
|
+
*
|
|
1064
|
+
* Graph.addEdge(mutable, a, b, 5)
|
|
1065
|
+
* Graph.addEdge(mutable, b, c, 15)
|
|
1066
|
+
* Graph.addEdge(mutable, c, a, 25)
|
|
1067
|
+
*
|
|
1068
|
+
* // Keep only edges with weight >= 10
|
|
1069
|
+
* Graph.filterEdges(mutable, (data) => data >= 10)
|
|
1070
|
+
* })
|
|
1071
|
+
*
|
|
1072
|
+
* console.log(Graph.edgeCount(graph)) // 2 (edge with weight 5 removed)
|
|
1073
|
+
* ```
|
|
1074
|
+
*
|
|
1075
|
+
* @since 3.18.0
|
|
1076
|
+
* @category transformations
|
|
1077
|
+
*/
|
|
1078
|
+
export const filterEdges = <N, E, T extends Kind = "directed">(
|
|
1079
|
+
mutable: MutableGraph<N, E, T>,
|
|
1080
|
+
predicate: (data: E) => boolean
|
|
1081
|
+
): void => {
|
|
1082
|
+
const edgesToRemove: Array<EdgeIndex> = []
|
|
1083
|
+
|
|
1084
|
+
// Identify edges to remove
|
|
1085
|
+
for (const [index, edgeData] of mutable.edges) {
|
|
1086
|
+
if (!predicate(edgeData.data)) {
|
|
1087
|
+
edgesToRemove.push(index)
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Remove filtered out edges
|
|
1092
|
+
for (const edgeIndex of edgesToRemove) {
|
|
1093
|
+
removeEdge(mutable, edgeIndex)
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// =============================================================================
|
|
1098
|
+
// Cycle Flag Management (Internal)
|
|
1099
|
+
// =============================================================================
|
|
1100
|
+
|
|
1101
|
+
/** @internal */
|
|
1102
|
+
const invalidateCycleFlagOnRemoval = <N, E, T extends Kind = "directed">(
|
|
1103
|
+
mutable: MutableGraph<N, E, T>
|
|
1104
|
+
): void => {
|
|
1105
|
+
// Only invalidate if the graph had cycles (removing edges/nodes cannot introduce cycles in acyclic graphs)
|
|
1106
|
+
// If already unknown (null) or acyclic (true), no need to change
|
|
1107
|
+
if (Option.isSome(mutable.isAcyclic) && mutable.isAcyclic.value === false) {
|
|
1108
|
+
mutable.isAcyclic = Option.none()
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/** @internal */
|
|
1113
|
+
const invalidateCycleFlagOnAddition = <N, E, T extends Kind = "directed">(
|
|
1114
|
+
mutable: MutableGraph<N, E, T>
|
|
1115
|
+
): void => {
|
|
1116
|
+
// Only invalidate if the graph was acyclic (adding edges cannot remove cycles from cyclic graphs)
|
|
1117
|
+
// If already unknown (null) or cyclic (false), no need to change
|
|
1118
|
+
if (Option.isSome(mutable.isAcyclic) && mutable.isAcyclic.value === true) {
|
|
1119
|
+
mutable.isAcyclic = Option.none()
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// =============================================================================
|
|
1124
|
+
// Edge Operations
|
|
1125
|
+
// =============================================================================
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Adds a new edge to a mutable graph and returns its index.
|
|
1129
|
+
*
|
|
1130
|
+
* @example
|
|
1131
|
+
* ```ts
|
|
1132
|
+
* import { Graph } from "effect"
|
|
1133
|
+
*
|
|
1134
|
+
* const result = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
|
|
1135
|
+
* const nodeA = Graph.addNode(mutable, "Node A")
|
|
1136
|
+
* const nodeB = Graph.addNode(mutable, "Node B")
|
|
1137
|
+
* const edge = Graph.addEdge(mutable, nodeA, nodeB, 42)
|
|
1138
|
+
* console.log(edge) // EdgeIndex with value 0
|
|
1139
|
+
* })
|
|
1140
|
+
* ```
|
|
1141
|
+
*
|
|
1142
|
+
* @since 3.18.0
|
|
1143
|
+
* @category mutations
|
|
1144
|
+
*/
|
|
1145
|
+
export const addEdge = <N, E, T extends Kind = "directed">(
|
|
1146
|
+
mutable: MutableGraph<N, E, T>,
|
|
1147
|
+
source: NodeIndex,
|
|
1148
|
+
target: NodeIndex,
|
|
1149
|
+
data: E
|
|
1150
|
+
): EdgeIndex => {
|
|
1151
|
+
// Validate that both nodes exist
|
|
1152
|
+
if (!mutable.nodes.has(source)) {
|
|
1153
|
+
throw missingNode(source)
|
|
1154
|
+
}
|
|
1155
|
+
if (!mutable.nodes.has(target)) {
|
|
1156
|
+
throw missingNode(target)
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const edgeIndex = mutable.nextEdgeIndex
|
|
1160
|
+
|
|
1161
|
+
// Create edge data
|
|
1162
|
+
const edgeData = new Edge({ source, target, data })
|
|
1163
|
+
mutable.edges.set(edgeIndex, edgeData)
|
|
1164
|
+
|
|
1165
|
+
// Update adjacency lists
|
|
1166
|
+
const sourceAdjacency = mutable.adjacency.get(source)
|
|
1167
|
+
if (sourceAdjacency !== undefined) {
|
|
1168
|
+
sourceAdjacency.push(edgeIndex)
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const targetReverseAdjacency = mutable.reverseAdjacency.get(target)
|
|
1172
|
+
if (targetReverseAdjacency !== undefined) {
|
|
1173
|
+
targetReverseAdjacency.push(edgeIndex)
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// For undirected graphs, add reverse connections
|
|
1177
|
+
if (mutable.type === "undirected") {
|
|
1178
|
+
const targetAdjacency = mutable.adjacency.get(target)
|
|
1179
|
+
if (targetAdjacency !== undefined) {
|
|
1180
|
+
targetAdjacency.push(edgeIndex)
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const sourceReverseAdjacency = mutable.reverseAdjacency.get(source)
|
|
1184
|
+
if (sourceReverseAdjacency !== undefined) {
|
|
1185
|
+
sourceReverseAdjacency.push(edgeIndex)
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// Update allocators
|
|
1190
|
+
mutable.nextEdgeIndex = mutable.nextEdgeIndex + 1
|
|
1191
|
+
|
|
1192
|
+
// Only invalidate cycle flag if the graph was acyclic
|
|
1193
|
+
// Adding edges cannot remove cycles from cyclic graphs
|
|
1194
|
+
invalidateCycleFlagOnAddition(mutable)
|
|
1195
|
+
|
|
1196
|
+
return edgeIndex
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* Removes a node and all its incident edges from a mutable graph.
|
|
1201
|
+
*
|
|
1202
|
+
* @example
|
|
1203
|
+
* ```ts
|
|
1204
|
+
* import { Graph } from "effect"
|
|
1205
|
+
*
|
|
1206
|
+
* const result = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
|
|
1207
|
+
* const nodeA = Graph.addNode(mutable, "Node A")
|
|
1208
|
+
* const nodeB = Graph.addNode(mutable, "Node B")
|
|
1209
|
+
* Graph.addEdge(mutable, nodeA, nodeB, 42)
|
|
1210
|
+
*
|
|
1211
|
+
* // Remove nodeA and all edges connected to it
|
|
1212
|
+
* Graph.removeNode(mutable, nodeA)
|
|
1213
|
+
* })
|
|
1214
|
+
* ```
|
|
1215
|
+
*
|
|
1216
|
+
* @since 3.18.0
|
|
1217
|
+
* @category mutations
|
|
1218
|
+
*/
|
|
1219
|
+
export const removeNode = <N, E, T extends Kind = "directed">(
|
|
1220
|
+
mutable: MutableGraph<N, E, T>,
|
|
1221
|
+
nodeIndex: NodeIndex
|
|
1222
|
+
): void => {
|
|
1223
|
+
// Check if node exists
|
|
1224
|
+
if (!mutable.nodes.has(nodeIndex)) {
|
|
1225
|
+
return // Node doesn't exist, nothing to remove
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Collect all incident edges for removal
|
|
1229
|
+
const edgesToRemove: Array<EdgeIndex> = []
|
|
1230
|
+
|
|
1231
|
+
// Get outgoing edges
|
|
1232
|
+
const outgoingEdges = mutable.adjacency.get(nodeIndex)
|
|
1233
|
+
if (outgoingEdges !== undefined) {
|
|
1234
|
+
for (const edge of outgoingEdges) {
|
|
1235
|
+
edgesToRemove.push(edge)
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Get incoming edges
|
|
1240
|
+
const incomingEdges = mutable.reverseAdjacency.get(nodeIndex)
|
|
1241
|
+
if (incomingEdges !== undefined) {
|
|
1242
|
+
for (const edge of incomingEdges) {
|
|
1243
|
+
edgesToRemove.push(edge)
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Remove all incident edges
|
|
1248
|
+
for (const edgeIndex of edgesToRemove) {
|
|
1249
|
+
removeEdgeInternal(mutable, edgeIndex)
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// Remove the node itself
|
|
1253
|
+
mutable.nodes.delete(nodeIndex)
|
|
1254
|
+
mutable.adjacency.delete(nodeIndex)
|
|
1255
|
+
mutable.reverseAdjacency.delete(nodeIndex)
|
|
1256
|
+
|
|
1257
|
+
// Only invalidate cycle flag if the graph wasn't already known to be acyclic
|
|
1258
|
+
// Removing nodes cannot introduce cycles in an acyclic graph
|
|
1259
|
+
invalidateCycleFlagOnRemoval(mutable)
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* Removes an edge from a mutable graph.
|
|
1264
|
+
*
|
|
1265
|
+
* @example
|
|
1266
|
+
* ```ts
|
|
1267
|
+
* import { Graph } from "effect"
|
|
1268
|
+
*
|
|
1269
|
+
* const result = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
|
|
1270
|
+
* const nodeA = Graph.addNode(mutable, "Node A")
|
|
1271
|
+
* const nodeB = Graph.addNode(mutable, "Node B")
|
|
1272
|
+
* const edge = Graph.addEdge(mutable, nodeA, nodeB, 42)
|
|
1273
|
+
*
|
|
1274
|
+
* // Remove the edge
|
|
1275
|
+
* Graph.removeEdge(mutable, edge)
|
|
1276
|
+
* })
|
|
1277
|
+
* ```
|
|
1278
|
+
*
|
|
1279
|
+
* @since 3.18.0
|
|
1280
|
+
* @category mutations
|
|
1281
|
+
*/
|
|
1282
|
+
export const removeEdge = <N, E, T extends Kind = "directed">(
|
|
1283
|
+
mutable: MutableGraph<N, E, T>,
|
|
1284
|
+
edgeIndex: EdgeIndex
|
|
1285
|
+
): void => {
|
|
1286
|
+
const wasRemoved = removeEdgeInternal(mutable, edgeIndex)
|
|
1287
|
+
|
|
1288
|
+
// Only invalidate cycle flag if an edge was actually removed
|
|
1289
|
+
// and only if the graph wasn't already known to be acyclic
|
|
1290
|
+
if (wasRemoved) {
|
|
1291
|
+
invalidateCycleFlagOnRemoval(mutable)
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/** @internal */
|
|
1296
|
+
const removeEdgeInternal = <N, E, T extends Kind = "directed">(
|
|
1297
|
+
mutable: MutableGraph<N, E, T>,
|
|
1298
|
+
edgeIndex: EdgeIndex
|
|
1299
|
+
): boolean => {
|
|
1300
|
+
// Get edge data
|
|
1301
|
+
const edge = mutable.edges.get(edgeIndex)
|
|
1302
|
+
if (edge === undefined) {
|
|
1303
|
+
return false // Edge doesn't exist, no mutation occurred
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
const { source, target } = edge
|
|
1307
|
+
|
|
1308
|
+
// Remove from adjacency lists
|
|
1309
|
+
const sourceAdjacency = mutable.adjacency.get(source)
|
|
1310
|
+
if (sourceAdjacency !== undefined) {
|
|
1311
|
+
const index = sourceAdjacency.indexOf(edgeIndex)
|
|
1312
|
+
if (index !== -1) {
|
|
1313
|
+
sourceAdjacency.splice(index, 1)
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const targetReverseAdjacency = mutable.reverseAdjacency.get(target)
|
|
1318
|
+
if (targetReverseAdjacency !== undefined) {
|
|
1319
|
+
const index = targetReverseAdjacency.indexOf(edgeIndex)
|
|
1320
|
+
if (index !== -1) {
|
|
1321
|
+
targetReverseAdjacency.splice(index, 1)
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// For undirected graphs, remove reverse connections
|
|
1326
|
+
if (mutable.type === "undirected") {
|
|
1327
|
+
const targetAdjacency = mutable.adjacency.get(target)
|
|
1328
|
+
if (targetAdjacency !== undefined) {
|
|
1329
|
+
const index = targetAdjacency.indexOf(edgeIndex)
|
|
1330
|
+
if (index !== -1) {
|
|
1331
|
+
targetAdjacency.splice(index, 1)
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const sourceReverseAdjacency = mutable.reverseAdjacency.get(source)
|
|
1336
|
+
if (sourceReverseAdjacency !== undefined) {
|
|
1337
|
+
const index = sourceReverseAdjacency.indexOf(edgeIndex)
|
|
1338
|
+
if (index !== -1) {
|
|
1339
|
+
sourceReverseAdjacency.splice(index, 1)
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// Remove edge data
|
|
1345
|
+
mutable.edges.delete(edgeIndex)
|
|
1346
|
+
|
|
1347
|
+
return true // Edge was successfully removed
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// =============================================================================
|
|
1351
|
+
// Edge Query Operations
|
|
1352
|
+
// =============================================================================
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Gets the edge data associated with an edge index, if it exists.
|
|
1356
|
+
*
|
|
1357
|
+
* @example
|
|
1358
|
+
* ```ts
|
|
1359
|
+
* import { Graph, Option } from "effect"
|
|
1360
|
+
*
|
|
1361
|
+
* const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
|
|
1362
|
+
* const nodeA = Graph.addNode(mutable, "Node A")
|
|
1363
|
+
* const nodeB = Graph.addNode(mutable, "Node B")
|
|
1364
|
+
* Graph.addEdge(mutable, nodeA, nodeB, 42)
|
|
1365
|
+
* })
|
|
1366
|
+
*
|
|
1367
|
+
* const edgeIndex = 0
|
|
1368
|
+
* const edgeData = Graph.getEdge(graph, edgeIndex)
|
|
1369
|
+
*
|
|
1370
|
+
* if (Option.isSome(edgeData)) {
|
|
1371
|
+
* console.log(edgeData.value.data) // 42
|
|
1372
|
+
* console.log(edgeData.value.source) // NodeIndex(0)
|
|
1373
|
+
* console.log(edgeData.value.target) // NodeIndex(1)
|
|
1374
|
+
* }
|
|
1375
|
+
* ```
|
|
1376
|
+
*
|
|
1377
|
+
* @since 3.18.0
|
|
1378
|
+
* @category getters
|
|
1379
|
+
*/
|
|
1380
|
+
export const getEdge = <N, E, T extends Kind = "directed">(
|
|
1381
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
1382
|
+
edgeIndex: EdgeIndex
|
|
1383
|
+
): Option.Option<Edge<E>> => graph.edges.has(edgeIndex) ? Option.some(graph.edges.get(edgeIndex)!) : Option.none()
|
|
1384
|
+
|
|
1385
|
+
/**
|
|
1386
|
+
* Checks if an edge exists between two nodes in the graph.
|
|
1387
|
+
*
|
|
1388
|
+
* @example
|
|
1389
|
+
* ```ts
|
|
1390
|
+
* import { Graph } from "effect"
|
|
1391
|
+
*
|
|
1392
|
+
* const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
|
|
1393
|
+
* const nodeA = Graph.addNode(mutable, "Node A")
|
|
1394
|
+
* const nodeB = Graph.addNode(mutable, "Node B")
|
|
1395
|
+
* const nodeC = Graph.addNode(mutable, "Node C")
|
|
1396
|
+
* Graph.addEdge(mutable, nodeA, nodeB, 42)
|
|
1397
|
+
* })
|
|
1398
|
+
*
|
|
1399
|
+
* const nodeA = 0
|
|
1400
|
+
* const nodeB = 1
|
|
1401
|
+
* const nodeC = 2
|
|
1402
|
+
*
|
|
1403
|
+
* const hasAB = Graph.hasEdge(graph, nodeA, nodeB)
|
|
1404
|
+
* console.log(hasAB) // true
|
|
1405
|
+
*
|
|
1406
|
+
* const hasAC = Graph.hasEdge(graph, nodeA, nodeC)
|
|
1407
|
+
* console.log(hasAC) // false
|
|
1408
|
+
* ```
|
|
1409
|
+
*
|
|
1410
|
+
* @since 3.18.0
|
|
1411
|
+
* @category getters
|
|
1412
|
+
*/
|
|
1413
|
+
export const hasEdge = <N, E, T extends Kind = "directed">(
|
|
1414
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
1415
|
+
source: NodeIndex,
|
|
1416
|
+
target: NodeIndex
|
|
1417
|
+
): boolean => {
|
|
1418
|
+
const adjacencyList = graph.adjacency.get(source)
|
|
1419
|
+
if (adjacencyList === undefined) {
|
|
1420
|
+
return false
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// Check if any edge in the adjacency list connects to the target
|
|
1424
|
+
for (const edgeIndex of adjacencyList) {
|
|
1425
|
+
const edge = graph.edges.get(edgeIndex)
|
|
1426
|
+
if (edge !== undefined && edge.target === target) {
|
|
1427
|
+
return true
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
return false
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Returns the number of edges in the graph.
|
|
1436
|
+
*
|
|
1437
|
+
* @example
|
|
1438
|
+
* ```ts
|
|
1439
|
+
* import { Graph } from "effect"
|
|
1440
|
+
*
|
|
1441
|
+
* const emptyGraph = Graph.directed<string, number>()
|
|
1442
|
+
* console.log(Graph.edgeCount(emptyGraph)) // 0
|
|
1443
|
+
*
|
|
1444
|
+
* const graphWithEdges = Graph.mutate(emptyGraph, (mutable) => {
|
|
1445
|
+
* const nodeA = Graph.addNode(mutable, "Node A")
|
|
1446
|
+
* const nodeB = Graph.addNode(mutable, "Node B")
|
|
1447
|
+
* const nodeC = Graph.addNode(mutable, "Node C")
|
|
1448
|
+
* Graph.addEdge(mutable, nodeA, nodeB, 1)
|
|
1449
|
+
* Graph.addEdge(mutable, nodeB, nodeC, 2)
|
|
1450
|
+
* Graph.addEdge(mutable, nodeC, nodeA, 3)
|
|
1451
|
+
* })
|
|
1452
|
+
*
|
|
1453
|
+
* console.log(Graph.edgeCount(graphWithEdges)) // 3
|
|
1454
|
+
* ```
|
|
1455
|
+
*
|
|
1456
|
+
* @since 3.18.0
|
|
1457
|
+
* @category getters
|
|
1458
|
+
*/
|
|
1459
|
+
export const edgeCount = <N, E, T extends Kind = "directed">(
|
|
1460
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>
|
|
1461
|
+
): number => graph.edges.size
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Returns the neighboring nodes (targets of outgoing edges) for a given node.
|
|
1465
|
+
*
|
|
1466
|
+
* @example
|
|
1467
|
+
* ```ts
|
|
1468
|
+
* import { Graph } from "effect"
|
|
1469
|
+
*
|
|
1470
|
+
* const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
|
|
1471
|
+
* const nodeA = Graph.addNode(mutable, "Node A")
|
|
1472
|
+
* const nodeB = Graph.addNode(mutable, "Node B")
|
|
1473
|
+
* const nodeC = Graph.addNode(mutable, "Node C")
|
|
1474
|
+
* Graph.addEdge(mutable, nodeA, nodeB, 1)
|
|
1475
|
+
* Graph.addEdge(mutable, nodeA, nodeC, 2)
|
|
1476
|
+
* })
|
|
1477
|
+
*
|
|
1478
|
+
* const nodeA = 0
|
|
1479
|
+
* const nodeB = 1
|
|
1480
|
+
* const nodeC = 2
|
|
1481
|
+
*
|
|
1482
|
+
* const neighborsA = Graph.neighbors(graph, nodeA)
|
|
1483
|
+
* console.log(neighborsA) // [NodeIndex(1), NodeIndex(2)]
|
|
1484
|
+
*
|
|
1485
|
+
* const neighborsB = Graph.neighbors(graph, nodeB)
|
|
1486
|
+
* console.log(neighborsB) // []
|
|
1487
|
+
* ```
|
|
1488
|
+
*
|
|
1489
|
+
* @since 3.18.0
|
|
1490
|
+
* @category getters
|
|
1491
|
+
*/
|
|
1492
|
+
export const neighbors = <N, E, T extends Kind = "directed">(
|
|
1493
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
1494
|
+
nodeIndex: NodeIndex
|
|
1495
|
+
): Array<NodeIndex> => {
|
|
1496
|
+
// For undirected graphs, use the specialized helper that returns the other endpoint
|
|
1497
|
+
if (graph.type === "undirected") {
|
|
1498
|
+
return getUndirectedNeighbors(graph as any, nodeIndex)
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
const adjacencyList = graph.adjacency.get(nodeIndex)
|
|
1502
|
+
if (adjacencyList === undefined) {
|
|
1503
|
+
return []
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
const result: Array<NodeIndex> = []
|
|
1507
|
+
for (const edgeIndex of adjacencyList) {
|
|
1508
|
+
const edge = graph.edges.get(edgeIndex)
|
|
1509
|
+
if (edge !== undefined) {
|
|
1510
|
+
result.push(edge.target)
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
return result
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
/**
|
|
1518
|
+
* Get neighbors of a node in a specific direction for bidirectional traversal.
|
|
1519
|
+
*
|
|
1520
|
+
* @example
|
|
1521
|
+
* ```ts
|
|
1522
|
+
* import { Graph } from "effect"
|
|
1523
|
+
*
|
|
1524
|
+
* const graph = Graph.directed<string, string>((mutable) => {
|
|
1525
|
+
* const a = Graph.addNode(mutable, "A")
|
|
1526
|
+
* const b = Graph.addNode(mutable, "B")
|
|
1527
|
+
* Graph.addEdge(mutable, a, b, "A->B")
|
|
1528
|
+
* })
|
|
1529
|
+
*
|
|
1530
|
+
* const nodeA = 0
|
|
1531
|
+
* const nodeB = 1
|
|
1532
|
+
*
|
|
1533
|
+
* // Get outgoing neighbors (nodes that nodeA points to)
|
|
1534
|
+
* const outgoing = Graph.neighborsDirected(graph, nodeA, "outgoing")
|
|
1535
|
+
*
|
|
1536
|
+
* // Get incoming neighbors (nodes that point to nodeB)
|
|
1537
|
+
* const incoming = Graph.neighborsDirected(graph, nodeB, "incoming")
|
|
1538
|
+
* ```
|
|
1539
|
+
*
|
|
1540
|
+
* @since 3.18.0
|
|
1541
|
+
* @category queries
|
|
1542
|
+
*/
|
|
1543
|
+
export const neighborsDirected = <N, E, T extends Kind = "directed">(
|
|
1544
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
1545
|
+
nodeIndex: NodeIndex,
|
|
1546
|
+
direction: Direction
|
|
1547
|
+
): Array<NodeIndex> => {
|
|
1548
|
+
const adjacencyMap = direction === "incoming"
|
|
1549
|
+
? graph.reverseAdjacency
|
|
1550
|
+
: graph.adjacency
|
|
1551
|
+
|
|
1552
|
+
const adjacencyList = adjacencyMap.get(nodeIndex)
|
|
1553
|
+
if (adjacencyList === undefined) {
|
|
1554
|
+
return []
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const result: Array<NodeIndex> = []
|
|
1558
|
+
for (const edgeIndex of adjacencyList) {
|
|
1559
|
+
const edge = graph.edges.get(edgeIndex)
|
|
1560
|
+
if (edge !== undefined) {
|
|
1561
|
+
// For incoming direction, we want the source node instead of target
|
|
1562
|
+
const neighborNode = direction === "incoming"
|
|
1563
|
+
? edge.source
|
|
1564
|
+
: edge.target
|
|
1565
|
+
result.push(neighborNode)
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
return result
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// =============================================================================
|
|
1573
|
+
// GraphViz Export
|
|
1574
|
+
// =============================================================================
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* Configuration options for GraphViz DOT format generation from graphs.
|
|
1578
|
+
*
|
|
1579
|
+
* @since 3.18.0
|
|
1580
|
+
* @category models
|
|
1581
|
+
*/
|
|
1582
|
+
export interface GraphVizOptions<N, E> {
|
|
1583
|
+
readonly nodeLabel?: (data: N) => string
|
|
1584
|
+
readonly edgeLabel?: (data: E) => string
|
|
1585
|
+
readonly graphName?: string
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
/**
|
|
1589
|
+
* Exports a graph to GraphViz DOT format for visualization.
|
|
1590
|
+
*
|
|
1591
|
+
* @example
|
|
1592
|
+
* ```ts
|
|
1593
|
+
* import { Graph } from "effect"
|
|
1594
|
+
*
|
|
1595
|
+
* const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
|
|
1596
|
+
* const nodeA = Graph.addNode(mutable, "Node A")
|
|
1597
|
+
* const nodeB = Graph.addNode(mutable, "Node B")
|
|
1598
|
+
* const nodeC = Graph.addNode(mutable, "Node C")
|
|
1599
|
+
* Graph.addEdge(mutable, nodeA, nodeB, 1)
|
|
1600
|
+
* Graph.addEdge(mutable, nodeB, nodeC, 2)
|
|
1601
|
+
* Graph.addEdge(mutable, nodeC, nodeA, 3)
|
|
1602
|
+
* })
|
|
1603
|
+
*
|
|
1604
|
+
* const dot = Graph.toGraphViz(graph)
|
|
1605
|
+
* console.log(dot)
|
|
1606
|
+
* // digraph G {
|
|
1607
|
+
* // "0" [label="Node A"];
|
|
1608
|
+
* // "1" [label="Node B"];
|
|
1609
|
+
* // "2" [label="Node C"];
|
|
1610
|
+
* // "0" -> "1" [label="1"];
|
|
1611
|
+
* // "1" -> "2" [label="2"];
|
|
1612
|
+
* // "2" -> "0" [label="3"];
|
|
1613
|
+
* // }
|
|
1614
|
+
* ```
|
|
1615
|
+
*
|
|
1616
|
+
* @since 3.18.0
|
|
1617
|
+
* @category utils
|
|
1618
|
+
*/
|
|
1619
|
+
export const toGraphViz = <N, E, T extends Kind = "directed">(
|
|
1620
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
1621
|
+
options?: GraphVizOptions<N, E>
|
|
1622
|
+
): string => {
|
|
1623
|
+
const {
|
|
1624
|
+
edgeLabel = (data: E) => String(data),
|
|
1625
|
+
graphName = "G",
|
|
1626
|
+
nodeLabel = (data: N) => String(data)
|
|
1627
|
+
} = options ?? {}
|
|
1628
|
+
|
|
1629
|
+
const isDirected = graph.type === "directed"
|
|
1630
|
+
const graphType = isDirected ? "digraph" : "graph"
|
|
1631
|
+
const edgeOperator = isDirected ? "->" : "--"
|
|
1632
|
+
|
|
1633
|
+
const lines: Array<string> = []
|
|
1634
|
+
lines.push(`${graphType} ${graphName} {`)
|
|
1635
|
+
|
|
1636
|
+
// Add nodes
|
|
1637
|
+
for (const [nodeIndex, nodeData] of graph.nodes) {
|
|
1638
|
+
const label = nodeLabel(nodeData).replace(/"/g, "\\\"")
|
|
1639
|
+
lines.push(` "${nodeIndex}" [label="${label}"];`)
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// Add edges
|
|
1643
|
+
for (const [, edgeData] of graph.edges) {
|
|
1644
|
+
const label = edgeLabel(edgeData.data).replace(/"/g, "\\\"")
|
|
1645
|
+
lines.push(` "${edgeData.source}" ${edgeOperator} "${edgeData.target}" [label="${label}"];`)
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
lines.push("}")
|
|
1649
|
+
return lines.join("\n")
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// =============================================================================
|
|
1653
|
+
// Mermaid Export
|
|
1654
|
+
// =============================================================================
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* Mermaid node shape types.
|
|
1658
|
+
*
|
|
1659
|
+
* @since 3.18.0
|
|
1660
|
+
* @category models
|
|
1661
|
+
*/
|
|
1662
|
+
export type MermaidNodeShape =
|
|
1663
|
+
| "rectangle"
|
|
1664
|
+
| "rounded"
|
|
1665
|
+
| "circle"
|
|
1666
|
+
| "diamond"
|
|
1667
|
+
| "hexagon"
|
|
1668
|
+
| "stadium"
|
|
1669
|
+
| "subroutine"
|
|
1670
|
+
| "cylindrical"
|
|
1671
|
+
|
|
1672
|
+
/**
|
|
1673
|
+
* Mermaid diagram direction types.
|
|
1674
|
+
*
|
|
1675
|
+
* @since 3.18.0
|
|
1676
|
+
* @category models
|
|
1677
|
+
*/
|
|
1678
|
+
export type MermaidDirection = "TB" | "TD" | "BT" | "LR" | "RL"
|
|
1679
|
+
|
|
1680
|
+
/**
|
|
1681
|
+
* Mermaid diagram type.
|
|
1682
|
+
*
|
|
1683
|
+
* @since 3.18.0
|
|
1684
|
+
* @category models
|
|
1685
|
+
*/
|
|
1686
|
+
export type MermaidDiagramType = "flowchart" | "graph"
|
|
1687
|
+
|
|
1688
|
+
/**
|
|
1689
|
+
* Configuration options for Mermaid diagram generation.
|
|
1690
|
+
*
|
|
1691
|
+
* @since 3.18.0
|
|
1692
|
+
* @category models
|
|
1693
|
+
*/
|
|
1694
|
+
export interface MermaidOptions<N, E> {
|
|
1695
|
+
readonly nodeLabel?: (data: N) => string
|
|
1696
|
+
readonly edgeLabel?: (data: E) => string
|
|
1697
|
+
readonly diagramType?: MermaidDiagramType
|
|
1698
|
+
readonly direction?: MermaidDirection
|
|
1699
|
+
readonly nodeShape?: (data: N) => MermaidNodeShape
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
/** @internal */
|
|
1703
|
+
const escapeMermaidLabel = (label: string): string => {
|
|
1704
|
+
// Escape special characters for Mermaid using HTML entity codes
|
|
1705
|
+
// According to: https://mermaid.js.org/syntax/flowchart.html#special-characters-that-break-syntax
|
|
1706
|
+
return label
|
|
1707
|
+
.replace(/#/g, "#35;")
|
|
1708
|
+
.replace(/"/g, "#quot;")
|
|
1709
|
+
.replace(/</g, "#lt;")
|
|
1710
|
+
.replace(/>/g, "#gt;")
|
|
1711
|
+
.replace(/&/g, "#amp;")
|
|
1712
|
+
.replace(/\[/g, "#91;")
|
|
1713
|
+
.replace(/\]/g, "#93;")
|
|
1714
|
+
.replace(/\{/g, "#123;")
|
|
1715
|
+
.replace(/\}/g, "#125;")
|
|
1716
|
+
.replace(/\(/g, "#40;")
|
|
1717
|
+
.replace(/\)/g, "#41;")
|
|
1718
|
+
.replace(/\|/g, "#124;")
|
|
1719
|
+
.replace(/\\/g, "#92;")
|
|
1720
|
+
.replace(/\n/g, "<br/>")
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/** @internal */
|
|
1724
|
+
const formatMermaidNode = (nodeId: string, label: string, shape: MermaidNodeShape): string => {
|
|
1725
|
+
switch (shape) {
|
|
1726
|
+
case "rectangle":
|
|
1727
|
+
return `${nodeId}["${label}"]`
|
|
1728
|
+
case "rounded":
|
|
1729
|
+
return `${nodeId}("${label}")`
|
|
1730
|
+
case "circle":
|
|
1731
|
+
return `${nodeId}(("${label}"))`
|
|
1732
|
+
case "diamond":
|
|
1733
|
+
return `${nodeId}{"${label}"}`
|
|
1734
|
+
case "hexagon":
|
|
1735
|
+
return `${nodeId}{{"${label}"}}`
|
|
1736
|
+
case "stadium":
|
|
1737
|
+
return `${nodeId}(["${label}"])`
|
|
1738
|
+
case "subroutine":
|
|
1739
|
+
return `${nodeId}[["${label}"]]`
|
|
1740
|
+
case "cylindrical":
|
|
1741
|
+
return `${nodeId}[("${label}")]`
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
/**
|
|
1746
|
+
* Exports a graph to Mermaid diagram format for visualization.
|
|
1747
|
+
*
|
|
1748
|
+
* @example
|
|
1749
|
+
* ```ts
|
|
1750
|
+
* import { Graph } from "effect"
|
|
1751
|
+
*
|
|
1752
|
+
* const graph = Graph.mutate(Graph.directed<string, number>(), (mutable) => {
|
|
1753
|
+
* const app = Graph.addNode(mutable, "App")
|
|
1754
|
+
* const db = Graph.addNode(mutable, "Database")
|
|
1755
|
+
* const cache = Graph.addNode(mutable, "Cache")
|
|
1756
|
+
* Graph.addEdge(mutable, app, db, 1)
|
|
1757
|
+
* Graph.addEdge(mutable, app, cache, 2)
|
|
1758
|
+
* })
|
|
1759
|
+
*
|
|
1760
|
+
* const mermaid = Graph.toMermaid(graph)
|
|
1761
|
+
* console.log(mermaid)
|
|
1762
|
+
* // flowchart TD
|
|
1763
|
+
* // 0["App"]
|
|
1764
|
+
* // 1["Database"]
|
|
1765
|
+
* // 2["Cache"]
|
|
1766
|
+
* // 0 -->|"1"| 1
|
|
1767
|
+
* // 0 -->|"2"| 2
|
|
1768
|
+
* ```
|
|
1769
|
+
*
|
|
1770
|
+
* @since 3.18.0
|
|
1771
|
+
* @category utils
|
|
1772
|
+
*/
|
|
1773
|
+
export const toMermaid = <N, E, T extends Kind = "directed">(
|
|
1774
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
1775
|
+
options?: MermaidOptions<N, E>
|
|
1776
|
+
): string => {
|
|
1777
|
+
// Extract and validate options with defaults
|
|
1778
|
+
const {
|
|
1779
|
+
diagramType,
|
|
1780
|
+
direction = "TD",
|
|
1781
|
+
edgeLabel = (data: E) => String(data),
|
|
1782
|
+
nodeLabel = (data: N) => String(data),
|
|
1783
|
+
nodeShape = () => "rectangle" as const
|
|
1784
|
+
} = options ?? {}
|
|
1785
|
+
|
|
1786
|
+
// Auto-detect diagram type if not specified
|
|
1787
|
+
const finalDiagramType = diagramType ??
|
|
1788
|
+
(graph.type === "directed" ? "flowchart" : "graph")
|
|
1789
|
+
|
|
1790
|
+
// Generate diagram header
|
|
1791
|
+
const lines: Array<string> = []
|
|
1792
|
+
lines.push(`${finalDiagramType} ${direction}`)
|
|
1793
|
+
|
|
1794
|
+
// Add nodes
|
|
1795
|
+
for (const [nodeIndex, nodeData] of graph.nodes) {
|
|
1796
|
+
const nodeId = String(nodeIndex)
|
|
1797
|
+
const label = escapeMermaidLabel(nodeLabel(nodeData))
|
|
1798
|
+
const shape = nodeShape(nodeData)
|
|
1799
|
+
const formattedNode = formatMermaidNode(nodeId, label, shape)
|
|
1800
|
+
lines.push(` ${formattedNode}`)
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// Add edges
|
|
1804
|
+
const edgeOperator = finalDiagramType === "flowchart" ? "-->" : "---"
|
|
1805
|
+
for (const [, edgeData] of graph.edges) {
|
|
1806
|
+
const sourceId = String(edgeData.source)
|
|
1807
|
+
const targetId = String(edgeData.target)
|
|
1808
|
+
const label = escapeMermaidLabel(edgeLabel(edgeData.data))
|
|
1809
|
+
|
|
1810
|
+
if (label) {
|
|
1811
|
+
lines.push(` ${sourceId} ${edgeOperator}|"${label}"| ${targetId}`)
|
|
1812
|
+
} else {
|
|
1813
|
+
lines.push(` ${sourceId} ${edgeOperator} ${targetId}`)
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
return lines.join("\n")
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// =============================================================================
|
|
1821
|
+
// Direction Types for Bidirectional Traversal
|
|
1822
|
+
// =============================================================================
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* Direction for graph traversal, indicating which edges to follow.
|
|
1826
|
+
*
|
|
1827
|
+
* @example
|
|
1828
|
+
* ```ts
|
|
1829
|
+
* import { Graph } from "effect"
|
|
1830
|
+
*
|
|
1831
|
+
* const graph = Graph.directed<string, string>((mutable) => {
|
|
1832
|
+
* const a = Graph.addNode(mutable, "A")
|
|
1833
|
+
* const b = Graph.addNode(mutable, "B")
|
|
1834
|
+
* Graph.addEdge(mutable, a, b, "A->B")
|
|
1835
|
+
* })
|
|
1836
|
+
*
|
|
1837
|
+
* // Follow outgoing edges (normal direction)
|
|
1838
|
+
* const outgoingNodes = Array.from(Graph.indices(Graph.dfs(graph, { start: [0], direction: "outgoing" })))
|
|
1839
|
+
*
|
|
1840
|
+
* // Follow incoming edges (reverse direction)
|
|
1841
|
+
* const incomingNodes = Array.from(Graph.indices(Graph.dfs(graph, { start: [1], direction: "incoming" })))
|
|
1842
|
+
* ```
|
|
1843
|
+
*
|
|
1844
|
+
* @since 3.18.0
|
|
1845
|
+
* @category models
|
|
1846
|
+
*/
|
|
1847
|
+
export type Direction = "outgoing" | "incoming"
|
|
1848
|
+
|
|
1849
|
+
// =============================================================================
|
|
1850
|
+
|
|
1851
|
+
// =============================================================================
|
|
1852
|
+
// Graph Structure Analysis Algorithms (Phase 5A)
|
|
1853
|
+
// =============================================================================
|
|
1854
|
+
|
|
1855
|
+
/**
|
|
1856
|
+
* Checks if the graph is acyclic (contains no cycles).
|
|
1857
|
+
*
|
|
1858
|
+
* Uses depth-first search to detect back edges, which indicate cycles.
|
|
1859
|
+
* For directed graphs, any back edge creates a cycle. For undirected graphs,
|
|
1860
|
+
* a back edge that doesn't go to the immediate parent creates a cycle.
|
|
1861
|
+
*
|
|
1862
|
+
* @example
|
|
1863
|
+
* ```ts
|
|
1864
|
+
* import { Graph } from "effect"
|
|
1865
|
+
*
|
|
1866
|
+
* // Acyclic directed graph (DAG)
|
|
1867
|
+
* const dag = Graph.directed<string, string>((mutable) => {
|
|
1868
|
+
* const a = Graph.addNode(mutable, "A")
|
|
1869
|
+
* const b = Graph.addNode(mutable, "B")
|
|
1870
|
+
* const c = Graph.addNode(mutable, "C")
|
|
1871
|
+
* Graph.addEdge(mutable, a, b, "A->B")
|
|
1872
|
+
* Graph.addEdge(mutable, b, c, "B->C")
|
|
1873
|
+
* })
|
|
1874
|
+
* console.log(Graph.isAcyclic(dag)) // true
|
|
1875
|
+
*
|
|
1876
|
+
* // Cyclic directed graph
|
|
1877
|
+
* const cyclic = Graph.directed<string, string>((mutable) => {
|
|
1878
|
+
* const a = Graph.addNode(mutable, "A")
|
|
1879
|
+
* const b = Graph.addNode(mutable, "B")
|
|
1880
|
+
* Graph.addEdge(mutable, a, b, "A->B")
|
|
1881
|
+
* Graph.addEdge(mutable, b, a, "B->A") // Creates cycle
|
|
1882
|
+
* })
|
|
1883
|
+
* console.log(Graph.isAcyclic(cyclic)) // false
|
|
1884
|
+
* ```
|
|
1885
|
+
*
|
|
1886
|
+
* @since 3.18.0
|
|
1887
|
+
* @category algorithms
|
|
1888
|
+
*/
|
|
1889
|
+
export const isAcyclic = <N, E, T extends Kind = "directed">(
|
|
1890
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>
|
|
1891
|
+
): boolean => {
|
|
1892
|
+
// Use existing cycle flag if available
|
|
1893
|
+
if (Option.isSome(graph.isAcyclic)) {
|
|
1894
|
+
return graph.isAcyclic.value
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
// Stack-safe DFS cycle detection using iterative approach
|
|
1898
|
+
const visited = new Set<NodeIndex>()
|
|
1899
|
+
const recursionStack = new Set<NodeIndex>()
|
|
1900
|
+
|
|
1901
|
+
// Stack entry: [node, neighbors, neighborIndex, isFirstVisit]
|
|
1902
|
+
type DfsStackEntry = [NodeIndex, Array<NodeIndex>, number, boolean]
|
|
1903
|
+
|
|
1904
|
+
// Get all nodes to handle disconnected components
|
|
1905
|
+
for (const startNode of graph.nodes.keys()) {
|
|
1906
|
+
if (visited.has(startNode)) {
|
|
1907
|
+
continue // Already processed this component
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// Iterative DFS with explicit stack
|
|
1911
|
+
const stack: Array<DfsStackEntry> = [[startNode, [], 0, true]]
|
|
1912
|
+
|
|
1913
|
+
while (stack.length > 0) {
|
|
1914
|
+
const [node, neighbors, neighborIndex, isFirstVisit] = stack[stack.length - 1]
|
|
1915
|
+
|
|
1916
|
+
// First visit to this node
|
|
1917
|
+
if (isFirstVisit) {
|
|
1918
|
+
if (recursionStack.has(node)) {
|
|
1919
|
+
// Back edge found - cycle detected
|
|
1920
|
+
graph.isAcyclic = Option.some(false)
|
|
1921
|
+
return false
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
if (visited.has(node)) {
|
|
1925
|
+
stack.pop()
|
|
1926
|
+
continue
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
visited.add(node)
|
|
1930
|
+
recursionStack.add(node)
|
|
1931
|
+
|
|
1932
|
+
// Get neighbors for this node
|
|
1933
|
+
const nodeNeighbors = Array.from(neighborsDirected(graph, node, "outgoing"))
|
|
1934
|
+
stack[stack.length - 1] = [node, nodeNeighbors, 0, false]
|
|
1935
|
+
continue
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// Process next neighbor
|
|
1939
|
+
if (neighborIndex < neighbors.length) {
|
|
1940
|
+
const neighbor = neighbors[neighborIndex]
|
|
1941
|
+
stack[stack.length - 1] = [node, neighbors, neighborIndex + 1, false]
|
|
1942
|
+
|
|
1943
|
+
if (recursionStack.has(neighbor)) {
|
|
1944
|
+
// Back edge found - cycle detected
|
|
1945
|
+
graph.isAcyclic = Option.some(false)
|
|
1946
|
+
return false
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
if (!visited.has(neighbor)) {
|
|
1950
|
+
stack.push([neighbor, [], 0, true])
|
|
1951
|
+
}
|
|
1952
|
+
} else {
|
|
1953
|
+
// Done with this node - backtrack
|
|
1954
|
+
recursionStack.delete(node)
|
|
1955
|
+
stack.pop()
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// Cache the result
|
|
1961
|
+
graph.isAcyclic = Option.some(true)
|
|
1962
|
+
return true
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
/**
|
|
1966
|
+
* Checks if an undirected graph is bipartite.
|
|
1967
|
+
*
|
|
1968
|
+
* A bipartite graph is one whose vertices can be divided into two disjoint sets
|
|
1969
|
+
* such that no two vertices within the same set are adjacent. Uses BFS coloring
|
|
1970
|
+
* to determine bipartiteness.
|
|
1971
|
+
*
|
|
1972
|
+
* @example
|
|
1973
|
+
* ```ts
|
|
1974
|
+
* import { Graph } from "effect"
|
|
1975
|
+
*
|
|
1976
|
+
* // Bipartite graph (alternating coloring possible)
|
|
1977
|
+
* const bipartite = Graph.undirected<string, string>((mutable) => {
|
|
1978
|
+
* const a = Graph.addNode(mutable, "A")
|
|
1979
|
+
* const b = Graph.addNode(mutable, "B")
|
|
1980
|
+
* const c = Graph.addNode(mutable, "C")
|
|
1981
|
+
* const d = Graph.addNode(mutable, "D")
|
|
1982
|
+
* Graph.addEdge(mutable, a, b, "edge") // Set 1: {A, C}, Set 2: {B, D}
|
|
1983
|
+
* Graph.addEdge(mutable, b, c, "edge")
|
|
1984
|
+
* Graph.addEdge(mutable, c, d, "edge")
|
|
1985
|
+
* })
|
|
1986
|
+
* console.log(Graph.isBipartite(bipartite)) // true
|
|
1987
|
+
*
|
|
1988
|
+
* // Non-bipartite graph (odd cycle)
|
|
1989
|
+
* const triangle = Graph.undirected<string, string>((mutable) => {
|
|
1990
|
+
* const a = Graph.addNode(mutable, "A")
|
|
1991
|
+
* const b = Graph.addNode(mutable, "B")
|
|
1992
|
+
* const c = Graph.addNode(mutable, "C")
|
|
1993
|
+
* Graph.addEdge(mutable, a, b, "edge")
|
|
1994
|
+
* Graph.addEdge(mutable, b, c, "edge")
|
|
1995
|
+
* Graph.addEdge(mutable, c, a, "edge") // Triangle (3-cycle)
|
|
1996
|
+
* })
|
|
1997
|
+
* console.log(Graph.isBipartite(triangle)) // false
|
|
1998
|
+
* ```
|
|
1999
|
+
*
|
|
2000
|
+
* @since 3.18.0
|
|
2001
|
+
* @category algorithms
|
|
2002
|
+
*/
|
|
2003
|
+
export const isBipartite = <N, E>(
|
|
2004
|
+
graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
|
|
2005
|
+
): boolean => {
|
|
2006
|
+
const coloring = new Map<NodeIndex, 0 | 1>()
|
|
2007
|
+
const discovered = new Set<NodeIndex>()
|
|
2008
|
+
let isBipartiteGraph = true
|
|
2009
|
+
|
|
2010
|
+
// Get all nodes to handle disconnected components
|
|
2011
|
+
for (const startNode of graph.nodes.keys()) {
|
|
2012
|
+
if (!discovered.has(startNode)) {
|
|
2013
|
+
// Start BFS coloring from this component
|
|
2014
|
+
const queue: Array<NodeIndex> = [startNode]
|
|
2015
|
+
coloring.set(startNode, 0) // Color start node with 0
|
|
2016
|
+
discovered.add(startNode)
|
|
2017
|
+
|
|
2018
|
+
while (queue.length > 0 && isBipartiteGraph) {
|
|
2019
|
+
const current = queue.shift()!
|
|
2020
|
+
const currentColor = coloring.get(current)!
|
|
2021
|
+
const neighborColor: 0 | 1 = currentColor === 0 ? 1 : 0
|
|
2022
|
+
|
|
2023
|
+
// Get all neighbors for undirected graph
|
|
2024
|
+
const nodeNeighbors = getUndirectedNeighbors(graph, current)
|
|
2025
|
+
for (const neighbor of nodeNeighbors) {
|
|
2026
|
+
if (!discovered.has(neighbor)) {
|
|
2027
|
+
// Color unvisited neighbor with opposite color
|
|
2028
|
+
coloring.set(neighbor, neighborColor)
|
|
2029
|
+
discovered.add(neighbor)
|
|
2030
|
+
queue.push(neighbor)
|
|
2031
|
+
} else {
|
|
2032
|
+
// Check if neighbor has the same color (conflict)
|
|
2033
|
+
if (coloring.get(neighbor) === currentColor) {
|
|
2034
|
+
isBipartiteGraph = false
|
|
2035
|
+
break
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// Early exit if not bipartite
|
|
2042
|
+
if (!isBipartiteGraph) {
|
|
2043
|
+
break
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
return isBipartiteGraph
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
/**
|
|
2052
|
+
* Get neighbors for undirected graphs by checking both adjacency and reverse adjacency.
|
|
2053
|
+
* For undirected graphs, we need to find the other endpoint of each edge incident to the node.
|
|
2054
|
+
*/
|
|
2055
|
+
const getUndirectedNeighbors = <N, E>(
|
|
2056
|
+
graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">,
|
|
2057
|
+
nodeIndex: NodeIndex
|
|
2058
|
+
): Array<NodeIndex> => {
|
|
2059
|
+
const neighbors = new Set<NodeIndex>()
|
|
2060
|
+
|
|
2061
|
+
// Check edges where this node is the source
|
|
2062
|
+
const adjacencyList = graph.adjacency.get(nodeIndex)
|
|
2063
|
+
if (adjacencyList !== undefined) {
|
|
2064
|
+
for (const edgeIndex of adjacencyList) {
|
|
2065
|
+
const edge = graph.edges.get(edgeIndex)
|
|
2066
|
+
if (edge !== undefined) {
|
|
2067
|
+
// For undirected graphs, the neighbor is the other endpoint
|
|
2068
|
+
const otherNode = edge.source === nodeIndex ? edge.target : edge.source
|
|
2069
|
+
neighbors.add(otherNode)
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
return Array.from(neighbors)
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
/**
|
|
2078
|
+
* Find connected components in an undirected graph.
|
|
2079
|
+
* Each component is represented as an array of node indices.
|
|
2080
|
+
*
|
|
2081
|
+
* @example
|
|
2082
|
+
* ```ts
|
|
2083
|
+
* import { Graph } from "effect"
|
|
2084
|
+
*
|
|
2085
|
+
* const graph = Graph.undirected<string, string>((mutable) => {
|
|
2086
|
+
* const a = Graph.addNode(mutable, "A")
|
|
2087
|
+
* const b = Graph.addNode(mutable, "B")
|
|
2088
|
+
* const c = Graph.addNode(mutable, "C")
|
|
2089
|
+
* const d = Graph.addNode(mutable, "D")
|
|
2090
|
+
* Graph.addEdge(mutable, a, b, "edge") // Component 1: A-B
|
|
2091
|
+
* Graph.addEdge(mutable, c, d, "edge") // Component 2: C-D
|
|
2092
|
+
* })
|
|
2093
|
+
*
|
|
2094
|
+
* const components = Graph.connectedComponents(graph)
|
|
2095
|
+
* console.log(components) // [[0, 1], [2, 3]]
|
|
2096
|
+
* ```
|
|
2097
|
+
*
|
|
2098
|
+
* @since 3.18.0
|
|
2099
|
+
* @category algorithms
|
|
2100
|
+
*/
|
|
2101
|
+
export const connectedComponents = <N, E>(
|
|
2102
|
+
graph: Graph<N, E, "undirected"> | MutableGraph<N, E, "undirected">
|
|
2103
|
+
): Array<Array<NodeIndex>> => {
|
|
2104
|
+
const visited = new Set<NodeIndex>()
|
|
2105
|
+
const components: Array<Array<NodeIndex>> = []
|
|
2106
|
+
for (const startNode of graph.nodes.keys()) {
|
|
2107
|
+
if (!visited.has(startNode)) {
|
|
2108
|
+
// DFS to find all nodes in this component
|
|
2109
|
+
const component: Array<NodeIndex> = []
|
|
2110
|
+
const stack: Array<NodeIndex> = [startNode]
|
|
2111
|
+
|
|
2112
|
+
while (stack.length > 0) {
|
|
2113
|
+
const current = stack.pop()!
|
|
2114
|
+
if (!visited.has(current)) {
|
|
2115
|
+
visited.add(current)
|
|
2116
|
+
component.push(current)
|
|
2117
|
+
|
|
2118
|
+
// Add all unvisited neighbors to stack
|
|
2119
|
+
const nodeNeighbors = getUndirectedNeighbors(graph, current)
|
|
2120
|
+
for (const neighbor of nodeNeighbors) {
|
|
2121
|
+
if (!visited.has(neighbor)) {
|
|
2122
|
+
stack.push(neighbor)
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
components.push(component)
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
return components
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
/**
|
|
2136
|
+
* Find strongly connected components in a directed graph using Kosaraju's algorithm.
|
|
2137
|
+
* Each SCC is represented as an array of node indices.
|
|
2138
|
+
*
|
|
2139
|
+
* @example
|
|
2140
|
+
* ```ts
|
|
2141
|
+
* import { Graph } from "effect"
|
|
2142
|
+
*
|
|
2143
|
+
* const graph = Graph.directed<string, string>((mutable) => {
|
|
2144
|
+
* const a = Graph.addNode(mutable, "A")
|
|
2145
|
+
* const b = Graph.addNode(mutable, "B")
|
|
2146
|
+
* const c = Graph.addNode(mutable, "C")
|
|
2147
|
+
* Graph.addEdge(mutable, a, b, "A->B")
|
|
2148
|
+
* Graph.addEdge(mutable, b, c, "B->C")
|
|
2149
|
+
* Graph.addEdge(mutable, c, a, "C->A") // Creates SCC: A-B-C
|
|
2150
|
+
* })
|
|
2151
|
+
*
|
|
2152
|
+
* const sccs = Graph.stronglyConnectedComponents(graph)
|
|
2153
|
+
* console.log(sccs) // [[0, 1, 2]]
|
|
2154
|
+
* ```
|
|
2155
|
+
*
|
|
2156
|
+
* @since 3.18.0
|
|
2157
|
+
* @category algorithms
|
|
2158
|
+
*/
|
|
2159
|
+
export const stronglyConnectedComponents = <N, E, T extends Kind = "directed">(
|
|
2160
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>
|
|
2161
|
+
): Array<Array<NodeIndex>> => {
|
|
2162
|
+
const visited = new Set<NodeIndex>()
|
|
2163
|
+
const finishOrder: Array<NodeIndex> = []
|
|
2164
|
+
// Iterate directly over node keys
|
|
2165
|
+
|
|
2166
|
+
// Step 1: Stack-safe DFS on original graph to get finish times
|
|
2167
|
+
// Stack entry: [node, neighbors, neighborIndex, isFirstVisit]
|
|
2168
|
+
type DfsStackEntry = [NodeIndex, Array<NodeIndex>, number, boolean]
|
|
2169
|
+
|
|
2170
|
+
for (const startNode of graph.nodes.keys()) {
|
|
2171
|
+
if (visited.has(startNode)) {
|
|
2172
|
+
continue
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
const stack: Array<DfsStackEntry> = [[startNode, [], 0, true]]
|
|
2176
|
+
|
|
2177
|
+
while (stack.length > 0) {
|
|
2178
|
+
const [node, nodeNeighbors, neighborIndex, isFirstVisit] = stack[stack.length - 1]
|
|
2179
|
+
|
|
2180
|
+
if (isFirstVisit) {
|
|
2181
|
+
if (visited.has(node)) {
|
|
2182
|
+
stack.pop()
|
|
2183
|
+
continue
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
visited.add(node)
|
|
2187
|
+
const nodeNeighborsList = neighbors(graph, node)
|
|
2188
|
+
stack[stack.length - 1] = [node, nodeNeighborsList, 0, false]
|
|
2189
|
+
continue
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
// Process next neighbor
|
|
2193
|
+
if (neighborIndex < nodeNeighbors.length) {
|
|
2194
|
+
const neighbor = nodeNeighbors[neighborIndex]
|
|
2195
|
+
stack[stack.length - 1] = [node, nodeNeighbors, neighborIndex + 1, false]
|
|
2196
|
+
|
|
2197
|
+
if (!visited.has(neighbor)) {
|
|
2198
|
+
stack.push([neighbor, [], 0, true])
|
|
2199
|
+
}
|
|
2200
|
+
} else {
|
|
2201
|
+
// Done with this node - add to finish order (post-order)
|
|
2202
|
+
finishOrder.push(node)
|
|
2203
|
+
stack.pop()
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// Step 2: Stack-safe DFS on transpose graph in reverse finish order
|
|
2209
|
+
visited.clear()
|
|
2210
|
+
const sccs: Array<Array<NodeIndex>> = []
|
|
2211
|
+
|
|
2212
|
+
for (let i = finishOrder.length - 1; i >= 0; i--) {
|
|
2213
|
+
const startNode = finishOrder[i]
|
|
2214
|
+
if (visited.has(startNode)) {
|
|
2215
|
+
continue
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
const scc: Array<NodeIndex> = []
|
|
2219
|
+
const stack: Array<NodeIndex> = [startNode]
|
|
2220
|
+
|
|
2221
|
+
while (stack.length > 0) {
|
|
2222
|
+
const node = stack.pop()!
|
|
2223
|
+
|
|
2224
|
+
if (visited.has(node)) {
|
|
2225
|
+
continue
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
visited.add(node)
|
|
2229
|
+
scc.push(node)
|
|
2230
|
+
|
|
2231
|
+
// Use reverse adjacency (transpose graph)
|
|
2232
|
+
const reverseAdjacency = graph.reverseAdjacency.get(node)
|
|
2233
|
+
if (reverseAdjacency !== undefined) {
|
|
2234
|
+
for (const edgeIndex of reverseAdjacency) {
|
|
2235
|
+
const edge = graph.edges.get(edgeIndex)
|
|
2236
|
+
if (edge !== undefined) {
|
|
2237
|
+
const predecessor = edge.source
|
|
2238
|
+
if (!visited.has(predecessor)) {
|
|
2239
|
+
stack.push(predecessor)
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
sccs.push(scc)
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
return sccs
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
// =============================================================================
|
|
2253
|
+
// Path Finding Algorithms (Phase 5B)
|
|
2254
|
+
// =============================================================================
|
|
2255
|
+
|
|
2256
|
+
/**
|
|
2257
|
+
* Result of a shortest path computation containing the path and total distance.
|
|
2258
|
+
*
|
|
2259
|
+
* @since 3.18.0
|
|
2260
|
+
* @category models
|
|
2261
|
+
*/
|
|
2262
|
+
export interface PathResult<E> {
|
|
2263
|
+
readonly path: Array<NodeIndex>
|
|
2264
|
+
readonly distance: number
|
|
2265
|
+
readonly costs: Array<E>
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
/**
|
|
2269
|
+
* Configuration for Dijkstra's algorithm.
|
|
2270
|
+
*
|
|
2271
|
+
* @since 3.18.0
|
|
2272
|
+
* @category models
|
|
2273
|
+
*/
|
|
2274
|
+
export interface DijkstraConfig<E> {
|
|
2275
|
+
source: NodeIndex
|
|
2276
|
+
target: NodeIndex
|
|
2277
|
+
cost: (edgeData: E) => number
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
/**
|
|
2281
|
+
* Configuration for A* algorithm.
|
|
2282
|
+
*
|
|
2283
|
+
* @since 3.18.0
|
|
2284
|
+
* @category models
|
|
2285
|
+
*/
|
|
2286
|
+
export interface AstarConfig<E, N> {
|
|
2287
|
+
source: NodeIndex
|
|
2288
|
+
target: NodeIndex
|
|
2289
|
+
cost: (edgeData: E) => number
|
|
2290
|
+
heuristic: (sourceNodeData: N, targetNodeData: N) => number
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
/**
|
|
2294
|
+
* Configuration for Bellman-Ford algorithm.
|
|
2295
|
+
*
|
|
2296
|
+
* @since 3.18.0
|
|
2297
|
+
* @category models
|
|
2298
|
+
*/
|
|
2299
|
+
export interface BellmanFordConfig<E> {
|
|
2300
|
+
source: NodeIndex
|
|
2301
|
+
target: NodeIndex
|
|
2302
|
+
cost: (edgeData: E) => number
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
/**
|
|
2306
|
+
* Find the shortest path between two nodes using Dijkstra's algorithm.
|
|
2307
|
+
*
|
|
2308
|
+
* Dijkstra's algorithm works with non-negative edge weights and finds the shortest
|
|
2309
|
+
* path from a source node to a target node in O((V + E) log V) time complexity.
|
|
2310
|
+
*
|
|
2311
|
+
* @example
|
|
2312
|
+
* ```ts
|
|
2313
|
+
* import { Graph, Option } from "effect"
|
|
2314
|
+
*
|
|
2315
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
2316
|
+
* const a = Graph.addNode(mutable, "A")
|
|
2317
|
+
* const b = Graph.addNode(mutable, "B")
|
|
2318
|
+
* const c = Graph.addNode(mutable, "C")
|
|
2319
|
+
* Graph.addEdge(mutable, a, b, 5)
|
|
2320
|
+
* Graph.addEdge(mutable, a, c, 10)
|
|
2321
|
+
* Graph.addEdge(mutable, b, c, 2)
|
|
2322
|
+
* })
|
|
2323
|
+
*
|
|
2324
|
+
* const result = Graph.dijkstra(graph, { source: 0, target: 2, cost: (edgeData) => edgeData })
|
|
2325
|
+
* if (Option.isSome(result)) {
|
|
2326
|
+
* console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C
|
|
2327
|
+
* console.log(result.value.distance) // 7 - total distance
|
|
2328
|
+
* }
|
|
2329
|
+
* ```
|
|
2330
|
+
*
|
|
2331
|
+
* @since 3.18.0
|
|
2332
|
+
* @category algorithms
|
|
2333
|
+
*/
|
|
2334
|
+
export const dijkstra = <N, E, T extends Kind = "directed">(
|
|
2335
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
2336
|
+
config: DijkstraConfig<E>
|
|
2337
|
+
): Option.Option<PathResult<E>> => {
|
|
2338
|
+
const { cost, source, target } = config
|
|
2339
|
+
// Validate that source and target nodes exist
|
|
2340
|
+
if (!graph.nodes.has(source)) {
|
|
2341
|
+
throw missingNode(source)
|
|
2342
|
+
}
|
|
2343
|
+
if (!graph.nodes.has(target)) {
|
|
2344
|
+
throw missingNode(target)
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
// Early return if source equals target
|
|
2348
|
+
if (source === target) {
|
|
2349
|
+
return Option.some({
|
|
2350
|
+
path: [source],
|
|
2351
|
+
distance: 0,
|
|
2352
|
+
costs: []
|
|
2353
|
+
})
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// Distance tracking and priority queue simulation
|
|
2357
|
+
const distances = new Map<NodeIndex, number>()
|
|
2358
|
+
const previous = new Map<NodeIndex, { node: NodeIndex; edgeData: E } | null>()
|
|
2359
|
+
const visited = new Set<NodeIndex>()
|
|
2360
|
+
|
|
2361
|
+
// Initialize distances
|
|
2362
|
+
// Iterate directly over node keys
|
|
2363
|
+
for (const node of graph.nodes.keys()) {
|
|
2364
|
+
distances.set(node, node === source ? 0 : Infinity)
|
|
2365
|
+
previous.set(node, null)
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
// Simple priority queue using array (can be optimized with proper heap)
|
|
2369
|
+
const priorityQueue: Array<{ node: NodeIndex; distance: number }> = [
|
|
2370
|
+
{ node: source, distance: 0 }
|
|
2371
|
+
]
|
|
2372
|
+
|
|
2373
|
+
while (priorityQueue.length > 0) {
|
|
2374
|
+
// Find minimum distance node (priority queue extract-min)
|
|
2375
|
+
let minIndex = 0
|
|
2376
|
+
for (let i = 1; i < priorityQueue.length; i++) {
|
|
2377
|
+
if (priorityQueue[i].distance < priorityQueue[minIndex].distance) {
|
|
2378
|
+
minIndex = i
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
const current = priorityQueue.splice(minIndex, 1)[0]
|
|
2383
|
+
const currentNode = current.node
|
|
2384
|
+
|
|
2385
|
+
// Skip if already visited (can happen with duplicate entries)
|
|
2386
|
+
if (visited.has(currentNode)) {
|
|
2387
|
+
continue
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
visited.add(currentNode)
|
|
2391
|
+
|
|
2392
|
+
// Early termination if we reached the target
|
|
2393
|
+
if (currentNode === target) {
|
|
2394
|
+
break
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// Get current distance
|
|
2398
|
+
const currentDistance = distances.get(currentNode)!
|
|
2399
|
+
|
|
2400
|
+
// Examine all outgoing edges
|
|
2401
|
+
const adjacencyList = graph.adjacency.get(currentNode)
|
|
2402
|
+
if (adjacencyList !== undefined) {
|
|
2403
|
+
for (const edgeIndex of adjacencyList) {
|
|
2404
|
+
const edge = graph.edges.get(edgeIndex)
|
|
2405
|
+
if (edge !== undefined) {
|
|
2406
|
+
const neighbor = edge.target
|
|
2407
|
+
const weight = cost(edge.data)
|
|
2408
|
+
|
|
2409
|
+
// Validate non-negative weights
|
|
2410
|
+
if (weight < 0) {
|
|
2411
|
+
throw new Error(`Dijkstra's algorithm requires non-negative edge weights, found ${weight}`)
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
const newDistance = currentDistance + weight
|
|
2415
|
+
const neighborDistance = distances.get(neighbor)!
|
|
2416
|
+
|
|
2417
|
+
// Relaxation step
|
|
2418
|
+
if (newDistance < neighborDistance) {
|
|
2419
|
+
distances.set(neighbor, newDistance)
|
|
2420
|
+
previous.set(neighbor, { node: currentNode, edgeData: edge.data })
|
|
2421
|
+
|
|
2422
|
+
// Add to priority queue if not visited
|
|
2423
|
+
if (!visited.has(neighbor)) {
|
|
2424
|
+
priorityQueue.push({ node: neighbor, distance: newDistance })
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
// Check if target is reachable
|
|
2433
|
+
const targetDistance = distances.get(target)!
|
|
2434
|
+
if (targetDistance === Infinity) {
|
|
2435
|
+
return Option.none() // No path exists
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
// Reconstruct path
|
|
2439
|
+
const path: Array<NodeIndex> = []
|
|
2440
|
+
const costs: Array<E> = []
|
|
2441
|
+
let currentNode: NodeIndex | null = target
|
|
2442
|
+
|
|
2443
|
+
while (currentNode !== null) {
|
|
2444
|
+
path.unshift(currentNode)
|
|
2445
|
+
const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)!
|
|
2446
|
+
if (prev !== null) {
|
|
2447
|
+
costs.unshift(prev.edgeData)
|
|
2448
|
+
currentNode = prev.node
|
|
2449
|
+
} else {
|
|
2450
|
+
currentNode = null
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
return Option.some({
|
|
2455
|
+
path,
|
|
2456
|
+
distance: targetDistance,
|
|
2457
|
+
costs
|
|
2458
|
+
})
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
/**
|
|
2462
|
+
* Result of all-pairs shortest path computation.
|
|
2463
|
+
*
|
|
2464
|
+
* @since 3.18.0
|
|
2465
|
+
* @category models
|
|
2466
|
+
*/
|
|
2467
|
+
export interface AllPairsResult<E> {
|
|
2468
|
+
readonly distances: Map<NodeIndex, Map<NodeIndex, number>>
|
|
2469
|
+
readonly paths: Map<NodeIndex, Map<NodeIndex, Array<NodeIndex> | null>>
|
|
2470
|
+
readonly costs: Map<NodeIndex, Map<NodeIndex, Array<E>>>
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
/**
|
|
2474
|
+
* Find shortest paths between all pairs of nodes using Floyd-Warshall algorithm.
|
|
2475
|
+
*
|
|
2476
|
+
* Floyd-Warshall algorithm computes shortest paths between all pairs of nodes in O(V³) time.
|
|
2477
|
+
* It can handle negative edge weights and detect negative cycles.
|
|
2478
|
+
*
|
|
2479
|
+
* @example
|
|
2480
|
+
* ```ts
|
|
2481
|
+
* import { Graph } from "effect"
|
|
2482
|
+
*
|
|
2483
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
2484
|
+
* const a = Graph.addNode(mutable, "A")
|
|
2485
|
+
* const b = Graph.addNode(mutable, "B")
|
|
2486
|
+
* const c = Graph.addNode(mutable, "C")
|
|
2487
|
+
* Graph.addEdge(mutable, a, b, 3)
|
|
2488
|
+
* Graph.addEdge(mutable, b, c, 2)
|
|
2489
|
+
* Graph.addEdge(mutable, a, c, 7)
|
|
2490
|
+
* })
|
|
2491
|
+
*
|
|
2492
|
+
* const result = Graph.floydWarshall(graph, (edgeData) => edgeData)
|
|
2493
|
+
* const distanceAToC = result.distances.get(0)?.get(2) // 5 (A->B->C)
|
|
2494
|
+
* const pathAToC = result.paths.get(0)?.get(2) // [0, 1, 2]
|
|
2495
|
+
* ```
|
|
2496
|
+
*
|
|
2497
|
+
* @since 3.18.0
|
|
2498
|
+
* @category algorithms
|
|
2499
|
+
*/
|
|
2500
|
+
export const floydWarshall = <N, E, T extends Kind = "directed">(
|
|
2501
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
2502
|
+
cost: (edgeData: E) => number
|
|
2503
|
+
): AllPairsResult<E> => {
|
|
2504
|
+
// Get all nodes for Floyd-Warshall algorithm (needs array for nested iteration)
|
|
2505
|
+
const allNodes = Array.from(graph.nodes.keys())
|
|
2506
|
+
|
|
2507
|
+
// Initialize distance matrix
|
|
2508
|
+
const dist = new Map<NodeIndex, Map<NodeIndex, number>>()
|
|
2509
|
+
const next = new Map<NodeIndex, Map<NodeIndex, NodeIndex | null>>()
|
|
2510
|
+
const edgeMatrix = new Map<NodeIndex, Map<NodeIndex, E | null>>()
|
|
2511
|
+
|
|
2512
|
+
// Initialize with infinity for all pairs
|
|
2513
|
+
for (const i of allNodes) {
|
|
2514
|
+
dist.set(i, new Map())
|
|
2515
|
+
next.set(i, new Map())
|
|
2516
|
+
edgeMatrix.set(i, new Map())
|
|
2517
|
+
|
|
2518
|
+
for (const j of allNodes) {
|
|
2519
|
+
dist.get(i)!.set(j, i === j ? 0 : Infinity)
|
|
2520
|
+
next.get(i)!.set(j, null)
|
|
2521
|
+
edgeMatrix.get(i)!.set(j, null)
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// Set edge weights
|
|
2526
|
+
for (const [, edgeData] of graph.edges) {
|
|
2527
|
+
const weight = cost(edgeData.data)
|
|
2528
|
+
const i = edgeData.source
|
|
2529
|
+
const j = edgeData.target
|
|
2530
|
+
|
|
2531
|
+
// Use minimum weight if multiple edges exist
|
|
2532
|
+
const currentWeight = dist.get(i)!.get(j)!
|
|
2533
|
+
if (weight < currentWeight) {
|
|
2534
|
+
dist.get(i)!.set(j, weight)
|
|
2535
|
+
next.get(i)!.set(j, j)
|
|
2536
|
+
edgeMatrix.get(i)!.set(j, edgeData.data)
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
// Floyd-Warshall main loop
|
|
2541
|
+
for (const k of allNodes) {
|
|
2542
|
+
for (const i of allNodes) {
|
|
2543
|
+
for (const j of allNodes) {
|
|
2544
|
+
const distIK = dist.get(i)!.get(k)!
|
|
2545
|
+
const distKJ = dist.get(k)!.get(j)!
|
|
2546
|
+
const distIJ = dist.get(i)!.get(j)!
|
|
2547
|
+
|
|
2548
|
+
if (distIK !== Infinity && distKJ !== Infinity && distIK + distKJ < distIJ) {
|
|
2549
|
+
dist.get(i)!.set(j, distIK + distKJ)
|
|
2550
|
+
next.get(i)!.set(j, next.get(i)!.get(k)!)
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
// Check for negative cycles
|
|
2557
|
+
for (const i of allNodes) {
|
|
2558
|
+
if (dist.get(i)!.get(i)! < 0) {
|
|
2559
|
+
throw new Error(`Negative cycle detected involving node ${i}`)
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
// Build result paths and edge weights
|
|
2564
|
+
const paths = new Map<NodeIndex, Map<NodeIndex, Array<NodeIndex> | null>>()
|
|
2565
|
+
const resultCosts = new Map<NodeIndex, Map<NodeIndex, Array<E>>>()
|
|
2566
|
+
|
|
2567
|
+
for (const i of allNodes) {
|
|
2568
|
+
paths.set(i, new Map())
|
|
2569
|
+
resultCosts.set(i, new Map())
|
|
2570
|
+
|
|
2571
|
+
for (const j of allNodes) {
|
|
2572
|
+
if (i === j) {
|
|
2573
|
+
paths.get(i)!.set(j, [i])
|
|
2574
|
+
resultCosts.get(i)!.set(j, [])
|
|
2575
|
+
} else if (dist.get(i)!.get(j)! === Infinity) {
|
|
2576
|
+
paths.get(i)!.set(j, null)
|
|
2577
|
+
resultCosts.get(i)!.set(j, [])
|
|
2578
|
+
} else {
|
|
2579
|
+
// Reconstruct path iteratively
|
|
2580
|
+
const path: Array<NodeIndex> = []
|
|
2581
|
+
const weights: Array<E> = []
|
|
2582
|
+
let current = i
|
|
2583
|
+
|
|
2584
|
+
path.push(current)
|
|
2585
|
+
while (current !== j) {
|
|
2586
|
+
const nextNode = next.get(current)!.get(j)!
|
|
2587
|
+
if (nextNode === null) break
|
|
2588
|
+
|
|
2589
|
+
const edgeData = edgeMatrix.get(current)!.get(nextNode)!
|
|
2590
|
+
if (edgeData !== null) {
|
|
2591
|
+
weights.push(edgeData)
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
current = nextNode
|
|
2595
|
+
path.push(current)
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
paths.get(i)!.set(j, path)
|
|
2599
|
+
resultCosts.get(i)!.set(j, weights)
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
return {
|
|
2605
|
+
distances: dist,
|
|
2606
|
+
paths,
|
|
2607
|
+
costs: resultCosts
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
/**
|
|
2612
|
+
* Find the shortest path between two nodes using A* pathfinding algorithm.
|
|
2613
|
+
*
|
|
2614
|
+
* A* is an extension of Dijkstra's algorithm that uses a heuristic function to guide
|
|
2615
|
+
* the search towards the target, potentially finding paths faster than Dijkstra's.
|
|
2616
|
+
* The heuristic must be admissible (never overestimate the actual cost).
|
|
2617
|
+
*
|
|
2618
|
+
* @example
|
|
2619
|
+
* ```ts
|
|
2620
|
+
* import { Graph, Option } from "effect"
|
|
2621
|
+
*
|
|
2622
|
+
* const graph = Graph.directed<{x: number, y: number}, number>((mutable) => {
|
|
2623
|
+
* const a = Graph.addNode(mutable, {x: 0, y: 0})
|
|
2624
|
+
* const b = Graph.addNode(mutable, {x: 1, y: 0})
|
|
2625
|
+
* const c = Graph.addNode(mutable, {x: 2, y: 0})
|
|
2626
|
+
* Graph.addEdge(mutable, a, b, 1)
|
|
2627
|
+
* Graph.addEdge(mutable, b, c, 1)
|
|
2628
|
+
* })
|
|
2629
|
+
*
|
|
2630
|
+
* // Manhattan distance heuristic
|
|
2631
|
+
* const heuristic = (nodeData: {x: number, y: number}, targetData: {x: number, y: number}) =>
|
|
2632
|
+
* Math.abs(nodeData.x - targetData.x) + Math.abs(nodeData.y - targetData.y)
|
|
2633
|
+
*
|
|
2634
|
+
* const result = Graph.astar(graph, { source: 0, target: 2, cost: (edgeData) => edgeData, heuristic })
|
|
2635
|
+
* if (Option.isSome(result)) {
|
|
2636
|
+
* console.log(result.value.path) // [0, 1, 2] - shortest path
|
|
2637
|
+
* console.log(result.value.distance) // 2 - total distance
|
|
2638
|
+
* }
|
|
2639
|
+
* ```
|
|
2640
|
+
*
|
|
2641
|
+
* @since 3.18.0
|
|
2642
|
+
* @category algorithms
|
|
2643
|
+
*/
|
|
2644
|
+
export const astar = <N, E, T extends Kind = "directed">(
|
|
2645
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
2646
|
+
config: AstarConfig<E, N>
|
|
2647
|
+
): Option.Option<PathResult<E>> => {
|
|
2648
|
+
const { cost, heuristic, source, target } = config
|
|
2649
|
+
// Validate that source and target nodes exist
|
|
2650
|
+
if (!graph.nodes.has(source)) {
|
|
2651
|
+
throw missingNode(source)
|
|
2652
|
+
}
|
|
2653
|
+
if (!graph.nodes.has(target)) {
|
|
2654
|
+
throw missingNode(target)
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// Early return if source equals target
|
|
2658
|
+
if (source === target) {
|
|
2659
|
+
return Option.some({
|
|
2660
|
+
path: [source],
|
|
2661
|
+
distance: 0,
|
|
2662
|
+
costs: []
|
|
2663
|
+
})
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
// Get target node data for heuristic calculations
|
|
2667
|
+
const targetNodeData = graph.nodes.get(target)
|
|
2668
|
+
if (targetNodeData === undefined) {
|
|
2669
|
+
throw new Error(`Target node ${target} data not found`)
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
// Distance tracking (g-score) and f-score (g + h)
|
|
2673
|
+
const gScore = new Map<NodeIndex, number>()
|
|
2674
|
+
const fScore = new Map<NodeIndex, number>()
|
|
2675
|
+
const previous = new Map<NodeIndex, { node: NodeIndex; edgeData: E } | null>()
|
|
2676
|
+
const visited = new Set<NodeIndex>()
|
|
2677
|
+
|
|
2678
|
+
// Initialize scores
|
|
2679
|
+
// Iterate directly over node keys
|
|
2680
|
+
for (const node of graph.nodes.keys()) {
|
|
2681
|
+
gScore.set(node, node === source ? 0 : Infinity)
|
|
2682
|
+
fScore.set(node, Infinity)
|
|
2683
|
+
previous.set(node, null)
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
// Calculate initial f-score for source
|
|
2687
|
+
const sourceNodeData = graph.nodes.get(source)
|
|
2688
|
+
if (sourceNodeData !== undefined) {
|
|
2689
|
+
const h = heuristic(sourceNodeData, targetNodeData)
|
|
2690
|
+
fScore.set(source, h)
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
// Priority queue using f-score (total estimated cost)
|
|
2694
|
+
const openSet: Array<{ node: NodeIndex; fScore: number }> = [
|
|
2695
|
+
{ node: source, fScore: fScore.get(source)! }
|
|
2696
|
+
]
|
|
2697
|
+
|
|
2698
|
+
while (openSet.length > 0) {
|
|
2699
|
+
// Find node with lowest f-score
|
|
2700
|
+
let minIndex = 0
|
|
2701
|
+
for (let i = 1; i < openSet.length; i++) {
|
|
2702
|
+
if (openSet[i].fScore < openSet[minIndex].fScore) {
|
|
2703
|
+
minIndex = i
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
const current = openSet.splice(minIndex, 1)[0]
|
|
2708
|
+
const currentNode = current.node
|
|
2709
|
+
|
|
2710
|
+
// Skip if already visited
|
|
2711
|
+
if (visited.has(currentNode)) {
|
|
2712
|
+
continue
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
visited.add(currentNode)
|
|
2716
|
+
|
|
2717
|
+
// Early termination if we reached the target
|
|
2718
|
+
if (currentNode === target) {
|
|
2719
|
+
break
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
// Get current g-score
|
|
2723
|
+
const currentGScore = gScore.get(currentNode)!
|
|
2724
|
+
|
|
2725
|
+
// Examine all outgoing edges
|
|
2726
|
+
const adjacencyList = graph.adjacency.get(currentNode)
|
|
2727
|
+
if (adjacencyList !== undefined) {
|
|
2728
|
+
for (const edgeIndex of adjacencyList) {
|
|
2729
|
+
const edge = graph.edges.get(edgeIndex)
|
|
2730
|
+
if (edge !== undefined) {
|
|
2731
|
+
const neighbor = edge.target
|
|
2732
|
+
const weight = cost(edge.data)
|
|
2733
|
+
|
|
2734
|
+
// Validate non-negative weights
|
|
2735
|
+
if (weight < 0) {
|
|
2736
|
+
throw new Error(`A* algorithm requires non-negative edge weights, found ${weight}`)
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
const tentativeGScore = currentGScore + weight
|
|
2740
|
+
const neighborGScore = gScore.get(neighbor)!
|
|
2741
|
+
|
|
2742
|
+
// If this path to neighbor is better than any previous one
|
|
2743
|
+
if (tentativeGScore < neighborGScore) {
|
|
2744
|
+
// Update g-score and previous
|
|
2745
|
+
gScore.set(neighbor, tentativeGScore)
|
|
2746
|
+
previous.set(neighbor, { node: currentNode, edgeData: edge.data })
|
|
2747
|
+
|
|
2748
|
+
// Calculate f-score using heuristic
|
|
2749
|
+
const neighborNodeData = graph.nodes.get(neighbor)
|
|
2750
|
+
if (neighborNodeData !== undefined) {
|
|
2751
|
+
const h = heuristic(neighborNodeData, targetNodeData)
|
|
2752
|
+
const f = tentativeGScore + h
|
|
2753
|
+
fScore.set(neighbor, f)
|
|
2754
|
+
|
|
2755
|
+
// Add to open set if not visited
|
|
2756
|
+
if (!visited.has(neighbor)) {
|
|
2757
|
+
openSet.push({ node: neighbor, fScore: f })
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
// Check if target is reachable
|
|
2767
|
+
const targetGScore = gScore.get(target)!
|
|
2768
|
+
if (targetGScore === Infinity) {
|
|
2769
|
+
return Option.none() // No path exists
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// Reconstruct path
|
|
2773
|
+
const path: Array<NodeIndex> = []
|
|
2774
|
+
const costs: Array<E> = []
|
|
2775
|
+
let currentNode: NodeIndex | null = target
|
|
2776
|
+
|
|
2777
|
+
while (currentNode !== null) {
|
|
2778
|
+
path.unshift(currentNode)
|
|
2779
|
+
const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)!
|
|
2780
|
+
if (prev !== null) {
|
|
2781
|
+
costs.unshift(prev.edgeData)
|
|
2782
|
+
currentNode = prev.node
|
|
2783
|
+
} else {
|
|
2784
|
+
currentNode = null
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
return Option.some({
|
|
2789
|
+
path,
|
|
2790
|
+
distance: targetGScore,
|
|
2791
|
+
costs
|
|
2792
|
+
})
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
/**
|
|
2796
|
+
* Find the shortest path between two nodes using Bellman-Ford algorithm.
|
|
2797
|
+
*
|
|
2798
|
+
* Bellman-Ford algorithm can handle negative edge weights and detects negative cycles.
|
|
2799
|
+
* It has O(VE) time complexity, slower than Dijkstra's but more versatile.
|
|
2800
|
+
* Returns Option.none() if a negative cycle is detected that affects the path.
|
|
2801
|
+
*
|
|
2802
|
+
* @example
|
|
2803
|
+
* ```ts
|
|
2804
|
+
* import { Graph, Option } from "effect"
|
|
2805
|
+
*
|
|
2806
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
2807
|
+
* const a = Graph.addNode(mutable, "A")
|
|
2808
|
+
* const b = Graph.addNode(mutable, "B")
|
|
2809
|
+
* const c = Graph.addNode(mutable, "C")
|
|
2810
|
+
* Graph.addEdge(mutable, a, b, -1) // Negative weight allowed
|
|
2811
|
+
* Graph.addEdge(mutable, b, c, 3)
|
|
2812
|
+
* Graph.addEdge(mutable, a, c, 5)
|
|
2813
|
+
* })
|
|
2814
|
+
*
|
|
2815
|
+
* const result = Graph.bellmanFord(graph, { source: 0, target: 2, cost: (edgeData) => edgeData })
|
|
2816
|
+
* if (Option.isSome(result)) {
|
|
2817
|
+
* console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C
|
|
2818
|
+
* console.log(result.value.distance) // 2 - total distance
|
|
2819
|
+
* }
|
|
2820
|
+
* ```
|
|
2821
|
+
*
|
|
2822
|
+
* @since 3.18.0
|
|
2823
|
+
* @category algorithms
|
|
2824
|
+
*/
|
|
2825
|
+
export const bellmanFord = <N, E, T extends Kind = "directed">(
|
|
2826
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
2827
|
+
config: BellmanFordConfig<E>
|
|
2828
|
+
): Option.Option<PathResult<E>> => {
|
|
2829
|
+
const { cost, source, target } = config
|
|
2830
|
+
// Validate that source and target nodes exist
|
|
2831
|
+
if (!graph.nodes.has(source)) {
|
|
2832
|
+
throw missingNode(source)
|
|
2833
|
+
}
|
|
2834
|
+
if (!graph.nodes.has(target)) {
|
|
2835
|
+
throw missingNode(target)
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
// Early return if source equals target
|
|
2839
|
+
if (source === target) {
|
|
2840
|
+
return Option.some({
|
|
2841
|
+
path: [source],
|
|
2842
|
+
distance: 0,
|
|
2843
|
+
costs: []
|
|
2844
|
+
})
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
// Initialize distances and predecessors
|
|
2848
|
+
const distances = new Map<NodeIndex, number>()
|
|
2849
|
+
const previous = new Map<NodeIndex, { node: NodeIndex; edgeData: E } | null>()
|
|
2850
|
+
// Iterate directly over node keys
|
|
2851
|
+
|
|
2852
|
+
for (const node of graph.nodes.keys()) {
|
|
2853
|
+
distances.set(node, node === source ? 0 : Infinity)
|
|
2854
|
+
previous.set(node, null)
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
// Collect all edges for relaxation
|
|
2858
|
+
const edges: Array<{ source: NodeIndex; target: NodeIndex; weight: number; edgeData: E }> = []
|
|
2859
|
+
for (const [, edgeData] of graph.edges) {
|
|
2860
|
+
const weight = cost(edgeData.data)
|
|
2861
|
+
edges.push({
|
|
2862
|
+
source: edgeData.source,
|
|
2863
|
+
target: edgeData.target,
|
|
2864
|
+
weight,
|
|
2865
|
+
edgeData: edgeData.data
|
|
2866
|
+
})
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
// Relax edges up to V-1 times
|
|
2870
|
+
const nodeCount = graph.nodes.size
|
|
2871
|
+
for (let i = 0; i < nodeCount - 1; i++) {
|
|
2872
|
+
let hasUpdate = false
|
|
2873
|
+
|
|
2874
|
+
for (const edge of edges) {
|
|
2875
|
+
const sourceDistance = distances.get(edge.source)!
|
|
2876
|
+
const targetDistance = distances.get(edge.target)!
|
|
2877
|
+
|
|
2878
|
+
// Relaxation step
|
|
2879
|
+
if (sourceDistance !== Infinity && sourceDistance + edge.weight < targetDistance) {
|
|
2880
|
+
distances.set(edge.target, sourceDistance + edge.weight)
|
|
2881
|
+
previous.set(edge.target, { node: edge.source, edgeData: edge.edgeData })
|
|
2882
|
+
hasUpdate = true
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
// Early termination if no updates
|
|
2887
|
+
if (!hasUpdate) {
|
|
2888
|
+
break
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
// Check for negative cycles
|
|
2893
|
+
for (const edge of edges) {
|
|
2894
|
+
const sourceDistance = distances.get(edge.source)!
|
|
2895
|
+
const targetDistance = distances.get(edge.target)!
|
|
2896
|
+
|
|
2897
|
+
if (sourceDistance !== Infinity && sourceDistance + edge.weight < targetDistance) {
|
|
2898
|
+
// Negative cycle detected - check if it affects the path to target
|
|
2899
|
+
const affectedNodes = new Set<NodeIndex>()
|
|
2900
|
+
const queue = [edge.target]
|
|
2901
|
+
|
|
2902
|
+
while (queue.length > 0) {
|
|
2903
|
+
const node = queue.shift()!
|
|
2904
|
+
if (affectedNodes.has(node)) continue
|
|
2905
|
+
affectedNodes.add(node)
|
|
2906
|
+
|
|
2907
|
+
// Add all nodes reachable from this node
|
|
2908
|
+
const adjacencyList = graph.adjacency.get(node)
|
|
2909
|
+
if (adjacencyList !== undefined) {
|
|
2910
|
+
for (const edgeIndex of adjacencyList) {
|
|
2911
|
+
const edge = graph.edges.get(edgeIndex)
|
|
2912
|
+
if (edge !== undefined) {
|
|
2913
|
+
queue.push(edge.target)
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
// If target is affected by negative cycle, return null
|
|
2920
|
+
if (affectedNodes.has(target)) {
|
|
2921
|
+
return Option.none()
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
// Check if target is reachable
|
|
2927
|
+
const targetDistance = distances.get(target)!
|
|
2928
|
+
if (targetDistance === Infinity) {
|
|
2929
|
+
return Option.none() // No path exists
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
// Reconstruct path
|
|
2933
|
+
const path: Array<NodeIndex> = []
|
|
2934
|
+
const costs: Array<E> = []
|
|
2935
|
+
let currentNode: NodeIndex | null = target
|
|
2936
|
+
|
|
2937
|
+
while (currentNode !== null) {
|
|
2938
|
+
path.unshift(currentNode)
|
|
2939
|
+
const prev: { node: NodeIndex; edgeData: E } | null = previous.get(currentNode)!
|
|
2940
|
+
if (prev !== null) {
|
|
2941
|
+
costs.unshift(prev.edgeData)
|
|
2942
|
+
currentNode = prev.node
|
|
2943
|
+
} else {
|
|
2944
|
+
currentNode = null
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
return Option.some({
|
|
2949
|
+
path,
|
|
2950
|
+
distance: targetDistance,
|
|
2951
|
+
costs
|
|
2952
|
+
})
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
/**
|
|
2956
|
+
* Concrete class for iterables that produce [NodeIndex, NodeData] tuples.
|
|
2957
|
+
*
|
|
2958
|
+
* This class provides a common abstraction for all iterables that return node data,
|
|
2959
|
+
* including traversal iterators (DFS, BFS, etc.) and element iterators (nodes, externals).
|
|
2960
|
+
* It uses a mapEntry function pattern for flexible iteration and transformation.
|
|
2961
|
+
*
|
|
2962
|
+
* @example
|
|
2963
|
+
* ```ts
|
|
2964
|
+
* import { Graph } from "effect"
|
|
2965
|
+
*
|
|
2966
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
2967
|
+
* const a = Graph.addNode(mutable, "A")
|
|
2968
|
+
* const b = Graph.addNode(mutable, "B")
|
|
2969
|
+
* Graph.addEdge(mutable, a, b, 1)
|
|
2970
|
+
* })
|
|
2971
|
+
*
|
|
2972
|
+
* // Both traversal and element iterators return NodeWalker
|
|
2973
|
+
* const dfsNodes: Graph.NodeWalker<string> = Graph.dfs(graph, { start: [0] })
|
|
2974
|
+
* const allNodes: Graph.NodeWalker<string> = Graph.nodes(graph)
|
|
2975
|
+
*
|
|
2976
|
+
* // Common interface for working with node iterables
|
|
2977
|
+
* function processNodes<N>(nodeIterable: Graph.NodeWalker<N>): Array<number> {
|
|
2978
|
+
* return Array.from(Graph.indices(nodeIterable))
|
|
2979
|
+
* }
|
|
2980
|
+
*
|
|
2981
|
+
* // Access node data using values() or entries()
|
|
2982
|
+
* const nodeData = Array.from(Graph.values(dfsNodes)) // ["A", "B"]
|
|
2983
|
+
* const nodeEntries = Array.from(Graph.entries(allNodes)) // [[0, "A"], [1, "B"]]
|
|
2984
|
+
* ```
|
|
2985
|
+
*
|
|
2986
|
+
* @since 3.18.0
|
|
2987
|
+
* @category models
|
|
2988
|
+
*/
|
|
2989
|
+
export class Walker<T, N> implements Iterable<[T, N]> {
|
|
2990
|
+
// @ts-ignore
|
|
2991
|
+
readonly [Symbol.iterator]: () => Iterator<[T, N]>
|
|
2992
|
+
|
|
2993
|
+
/**
|
|
2994
|
+
* Visits each element and maps it to a value using the provided function.
|
|
2995
|
+
*
|
|
2996
|
+
* Takes a function that receives the index and data,
|
|
2997
|
+
* and returns an iterable of the mapped values. Skips elements that
|
|
2998
|
+
* no longer exist in the graph.
|
|
2999
|
+
*
|
|
3000
|
+
* @example
|
|
3001
|
+
* ```ts
|
|
3002
|
+
* import { Graph } from "effect"
|
|
3003
|
+
*
|
|
3004
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
3005
|
+
* const a = Graph.addNode(mutable, "A")
|
|
3006
|
+
* const b = Graph.addNode(mutable, "B")
|
|
3007
|
+
* Graph.addEdge(mutable, a, b, 1)
|
|
3008
|
+
* })
|
|
3009
|
+
*
|
|
3010
|
+
* const dfs = Graph.dfs(graph, { start: [0] })
|
|
3011
|
+
*
|
|
3012
|
+
* // Map to just the node data
|
|
3013
|
+
* const values = Array.from(dfs.visit((index, data) => data))
|
|
3014
|
+
* console.log(values) // ["A", "B"]
|
|
3015
|
+
*
|
|
3016
|
+
* // Map to custom objects
|
|
3017
|
+
* const custom = Array.from(dfs.visit((index, data) => ({ id: index, name: data })))
|
|
3018
|
+
* console.log(custom) // [{ id: 0, name: "A" }, { id: 1, name: "B" }]
|
|
3019
|
+
* ```
|
|
3020
|
+
*
|
|
3021
|
+
* @since 3.18.0
|
|
3022
|
+
* @category iterators
|
|
3023
|
+
*/
|
|
3024
|
+
readonly visit: <U>(f: (index: T, data: N) => U) => Iterable<U>
|
|
3025
|
+
|
|
3026
|
+
constructor(
|
|
3027
|
+
/**
|
|
3028
|
+
* Visits each element and maps it to a value using the provided function.
|
|
3029
|
+
*
|
|
3030
|
+
* Takes a function that receives the index and data,
|
|
3031
|
+
* and returns an iterable of the mapped values. Skips elements that
|
|
3032
|
+
* no longer exist in the graph.
|
|
3033
|
+
*
|
|
3034
|
+
* @example
|
|
3035
|
+
* ```ts
|
|
3036
|
+
* import { Graph } from "effect"
|
|
3037
|
+
*
|
|
3038
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
3039
|
+
* const a = Graph.addNode(mutable, "A")
|
|
3040
|
+
* const b = Graph.addNode(mutable, "B")
|
|
3041
|
+
* Graph.addEdge(mutable, a, b, 1)
|
|
3042
|
+
* })
|
|
3043
|
+
*
|
|
3044
|
+
* const dfs = Graph.dfs(graph, { start: [0] })
|
|
3045
|
+
*
|
|
3046
|
+
* // Map to just the node data
|
|
3047
|
+
* const values = Array.from(dfs.visit((index, data) => data))
|
|
3048
|
+
* console.log(values) // ["A", "B"]
|
|
3049
|
+
*
|
|
3050
|
+
* // Map to custom objects
|
|
3051
|
+
* const custom = Array.from(dfs.visit((index, data) => ({ id: index, name: data })))
|
|
3052
|
+
* console.log(custom) // [{ id: 0, name: "A" }, { id: 1, name: "B" }]
|
|
3053
|
+
* ```
|
|
3054
|
+
*
|
|
3055
|
+
* @since 3.18.0
|
|
3056
|
+
* @category iterators
|
|
3057
|
+
*/
|
|
3058
|
+
visit: <U>(f: (index: T, data: N) => U) => Iterable<U>
|
|
3059
|
+
) {
|
|
3060
|
+
this.visit = visit
|
|
3061
|
+
this[Symbol.iterator] = visit((index, data) => [index, data] as [T, N])[Symbol.iterator]
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
/**
|
|
3066
|
+
* Type alias for node iteration using Walker.
|
|
3067
|
+
* NodeWalker is represented as Walker<NodeIndex, N>.
|
|
3068
|
+
*
|
|
3069
|
+
* @since 3.18.0
|
|
3070
|
+
* @category models
|
|
3071
|
+
*/
|
|
3072
|
+
export type NodeWalker<N> = Walker<NodeIndex, N>
|
|
3073
|
+
|
|
3074
|
+
/**
|
|
3075
|
+
* Type alias for edge iteration using Walker.
|
|
3076
|
+
* EdgeWalker is represented as Walker<EdgeIndex, Edge<E>>.
|
|
3077
|
+
*
|
|
3078
|
+
* @since 3.18.0
|
|
3079
|
+
* @category models
|
|
3080
|
+
*/
|
|
3081
|
+
export type EdgeWalker<E> = Walker<EdgeIndex, Edge<E>>
|
|
3082
|
+
|
|
3083
|
+
/**
|
|
3084
|
+
* Returns an iterator over the indices in the walker.
|
|
3085
|
+
*
|
|
3086
|
+
* @example
|
|
3087
|
+
* ```ts
|
|
3088
|
+
* import { Graph } from "effect"
|
|
3089
|
+
*
|
|
3090
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
3091
|
+
* const a = Graph.addNode(mutable, "A")
|
|
3092
|
+
* const b = Graph.addNode(mutable, "B")
|
|
3093
|
+
* Graph.addEdge(mutable, a, b, 1)
|
|
3094
|
+
* })
|
|
3095
|
+
*
|
|
3096
|
+
* const dfs = Graph.dfs(graph, { start: [0] })
|
|
3097
|
+
* const indices = Array.from(Graph.indices(dfs))
|
|
3098
|
+
* console.log(indices) // [0, 1]
|
|
3099
|
+
* ```
|
|
3100
|
+
*
|
|
3101
|
+
* @since 3.18.0
|
|
3102
|
+
* @category utilities
|
|
3103
|
+
*/
|
|
3104
|
+
export const indices = <T, N>(walker: Walker<T, N>): Iterable<T> => walker.visit((index, _) => index)
|
|
3105
|
+
|
|
3106
|
+
/**
|
|
3107
|
+
* Returns an iterator over the values (data) in the walker.
|
|
3108
|
+
*
|
|
3109
|
+
* @example
|
|
3110
|
+
* ```ts
|
|
3111
|
+
* import { Graph } from "effect"
|
|
3112
|
+
*
|
|
3113
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
3114
|
+
* const a = Graph.addNode(mutable, "A")
|
|
3115
|
+
* const b = Graph.addNode(mutable, "B")
|
|
3116
|
+
* Graph.addEdge(mutable, a, b, 1)
|
|
3117
|
+
* })
|
|
3118
|
+
*
|
|
3119
|
+
* const dfs = Graph.dfs(graph, { start: [0] })
|
|
3120
|
+
* const values = Array.from(Graph.values(dfs))
|
|
3121
|
+
* console.log(values) // ["A", "B"]
|
|
3122
|
+
* ```
|
|
3123
|
+
*
|
|
3124
|
+
* @since 3.18.0
|
|
3125
|
+
* @category utilities
|
|
3126
|
+
*/
|
|
3127
|
+
export const values = <T, N>(walker: Walker<T, N>): Iterable<N> => walker.visit((_, data) => data)
|
|
3128
|
+
|
|
3129
|
+
/**
|
|
3130
|
+
* Returns an iterator over [index, data] entries in the walker.
|
|
3131
|
+
*
|
|
3132
|
+
* @example
|
|
3133
|
+
* ```ts
|
|
3134
|
+
* import { Graph } from "effect"
|
|
3135
|
+
*
|
|
3136
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
3137
|
+
* const a = Graph.addNode(mutable, "A")
|
|
3138
|
+
* const b = Graph.addNode(mutable, "B")
|
|
3139
|
+
* Graph.addEdge(mutable, a, b, 1)
|
|
3140
|
+
* })
|
|
3141
|
+
*
|
|
3142
|
+
* const dfs = Graph.dfs(graph, { start: [0] })
|
|
3143
|
+
* const entries = Array.from(Graph.entries(dfs))
|
|
3144
|
+
* console.log(entries) // [[0, "A"], [1, "B"]]
|
|
3145
|
+
* ```
|
|
3146
|
+
*
|
|
3147
|
+
* @since 3.18.0
|
|
3148
|
+
* @category utilities
|
|
3149
|
+
*/
|
|
3150
|
+
export const entries = <T, N>(walker: Walker<T, N>): Iterable<[T, N]> =>
|
|
3151
|
+
walker.visit((index, data) => [index, data] as [T, N])
|
|
3152
|
+
|
|
3153
|
+
/**
|
|
3154
|
+
* Configuration for graph search iterators.
|
|
3155
|
+
*
|
|
3156
|
+
* @since 3.18.0
|
|
3157
|
+
* @category models
|
|
3158
|
+
*/
|
|
3159
|
+
export interface SearchConfig {
|
|
3160
|
+
readonly start?: Array<NodeIndex>
|
|
3161
|
+
readonly direction?: Direction
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
/**
|
|
3165
|
+
* Creates a new DFS iterator with optional configuration.
|
|
3166
|
+
*
|
|
3167
|
+
* The iterator maintains a stack of nodes to visit and tracks discovered nodes.
|
|
3168
|
+
* It provides lazy evaluation of the depth-first search.
|
|
3169
|
+
*
|
|
3170
|
+
* @example
|
|
3171
|
+
* ```ts
|
|
3172
|
+
* import { Graph } from "effect"
|
|
3173
|
+
*
|
|
3174
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
3175
|
+
* const a = Graph.addNode(mutable, "A")
|
|
3176
|
+
* const b = Graph.addNode(mutable, "B")
|
|
3177
|
+
* const c = Graph.addNode(mutable, "C")
|
|
3178
|
+
* Graph.addEdge(mutable, a, b, 1)
|
|
3179
|
+
* Graph.addEdge(mutable, b, c, 1)
|
|
3180
|
+
* })
|
|
3181
|
+
*
|
|
3182
|
+
* // Start from a specific node
|
|
3183
|
+
* const dfs1 = Graph.dfs(graph, { start: [0] })
|
|
3184
|
+
* for (const nodeIndex of Graph.indices(dfs1)) {
|
|
3185
|
+
* console.log(nodeIndex) // Traverses in DFS order: 0, 1, 2
|
|
3186
|
+
* }
|
|
3187
|
+
*
|
|
3188
|
+
* // Empty iterator (no starting nodes)
|
|
3189
|
+
* const dfs2 = Graph.dfs(graph)
|
|
3190
|
+
* // Can be used programmatically
|
|
3191
|
+
* ```
|
|
3192
|
+
*
|
|
3193
|
+
* @since 3.18.0
|
|
3194
|
+
* @category iterators
|
|
3195
|
+
*/
|
|
3196
|
+
export const dfs = <N, E, T extends Kind = "directed">(
|
|
3197
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
3198
|
+
config: SearchConfig = {}
|
|
3199
|
+
): NodeWalker<N> => {
|
|
3200
|
+
const start = config.start ?? []
|
|
3201
|
+
const direction = config.direction ?? "outgoing"
|
|
3202
|
+
|
|
3203
|
+
// Validate that all start nodes exist
|
|
3204
|
+
for (const nodeIndex of start) {
|
|
3205
|
+
if (!hasNode(graph, nodeIndex)) {
|
|
3206
|
+
throw missingNode(nodeIndex)
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
return new Walker((f) => ({
|
|
3211
|
+
[Symbol.iterator]: () => {
|
|
3212
|
+
const stack = [...start]
|
|
3213
|
+
const discovered = new Set<NodeIndex>()
|
|
3214
|
+
|
|
3215
|
+
const nextMapped = () => {
|
|
3216
|
+
while (stack.length > 0) {
|
|
3217
|
+
const current = stack.pop()!
|
|
3218
|
+
|
|
3219
|
+
if (discovered.has(current)) {
|
|
3220
|
+
continue
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
discovered.add(current)
|
|
3224
|
+
|
|
3225
|
+
const nodeDataOption = graph.nodes.get(current)
|
|
3226
|
+
if (nodeDataOption === undefined) {
|
|
3227
|
+
continue
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
const neighbors = neighborsDirected(graph, current, direction)
|
|
3231
|
+
for (let i = neighbors.length - 1; i >= 0; i--) {
|
|
3232
|
+
const neighbor = neighbors[i]
|
|
3233
|
+
if (!discovered.has(neighbor)) {
|
|
3234
|
+
stack.push(neighbor)
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
return { done: false, value: f(current, nodeDataOption) }
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
return { done: true, value: undefined } as const
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
return { next: nextMapped }
|
|
3245
|
+
}
|
|
3246
|
+
}))
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
/**
|
|
3250
|
+
* Creates a new BFS iterator with optional configuration.
|
|
3251
|
+
*
|
|
3252
|
+
* The iterator maintains a queue of nodes to visit and tracks discovered nodes.
|
|
3253
|
+
* It provides lazy evaluation of the breadth-first search.
|
|
3254
|
+
*
|
|
3255
|
+
* @example
|
|
3256
|
+
* ```ts
|
|
3257
|
+
* import { Graph } from "effect"
|
|
3258
|
+
*
|
|
3259
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
3260
|
+
* const a = Graph.addNode(mutable, "A")
|
|
3261
|
+
* const b = Graph.addNode(mutable, "B")
|
|
3262
|
+
* const c = Graph.addNode(mutable, "C")
|
|
3263
|
+
* Graph.addEdge(mutable, a, b, 1)
|
|
3264
|
+
* Graph.addEdge(mutable, b, c, 1)
|
|
3265
|
+
* })
|
|
3266
|
+
*
|
|
3267
|
+
* // Start from a specific node
|
|
3268
|
+
* const bfs1 = Graph.bfs(graph, { start: [0] })
|
|
3269
|
+
* for (const nodeIndex of Graph.indices(bfs1)) {
|
|
3270
|
+
* console.log(nodeIndex) // Traverses in BFS order: 0, 1, 2
|
|
3271
|
+
* }
|
|
3272
|
+
*
|
|
3273
|
+
* // Empty iterator (no starting nodes)
|
|
3274
|
+
* const bfs2 = Graph.bfs(graph)
|
|
3275
|
+
* // Can be used programmatically
|
|
3276
|
+
* ```
|
|
3277
|
+
*
|
|
3278
|
+
* @since 3.18.0
|
|
3279
|
+
* @category iterators
|
|
3280
|
+
*/
|
|
3281
|
+
export const bfs = <N, E, T extends Kind = "directed">(
|
|
3282
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
3283
|
+
config: SearchConfig = {}
|
|
3284
|
+
): NodeWalker<N> => {
|
|
3285
|
+
const start = config.start ?? []
|
|
3286
|
+
const direction = config.direction ?? "outgoing"
|
|
3287
|
+
|
|
3288
|
+
// Validate that all start nodes exist
|
|
3289
|
+
for (const nodeIndex of start) {
|
|
3290
|
+
if (!hasNode(graph, nodeIndex)) {
|
|
3291
|
+
throw missingNode(nodeIndex)
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
return new Walker((f) => ({
|
|
3296
|
+
[Symbol.iterator]: () => {
|
|
3297
|
+
const queue = [...start]
|
|
3298
|
+
const discovered = new Set<NodeIndex>()
|
|
3299
|
+
|
|
3300
|
+
const nextMapped = () => {
|
|
3301
|
+
while (queue.length > 0) {
|
|
3302
|
+
const current = queue.shift()!
|
|
3303
|
+
|
|
3304
|
+
if (!discovered.has(current)) {
|
|
3305
|
+
discovered.add(current)
|
|
3306
|
+
|
|
3307
|
+
const neighbors = neighborsDirected(graph, current, direction)
|
|
3308
|
+
for (const neighbor of neighbors) {
|
|
3309
|
+
if (!discovered.has(neighbor)) {
|
|
3310
|
+
queue.push(neighbor)
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
const nodeData = getNode(graph, current)
|
|
3315
|
+
if (Option.isSome(nodeData)) {
|
|
3316
|
+
return { done: false, value: f(current, nodeData.value) }
|
|
3317
|
+
}
|
|
3318
|
+
return nextMapped()
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
return { done: true, value: undefined } as const
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
return { next: nextMapped }
|
|
3326
|
+
}
|
|
3327
|
+
}))
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
/**
|
|
3331
|
+
* Configuration options for topological sort iterator.
|
|
3332
|
+
*
|
|
3333
|
+
* @since 3.18.0
|
|
3334
|
+
* @category models
|
|
3335
|
+
*/
|
|
3336
|
+
export interface TopoConfig {
|
|
3337
|
+
readonly initials?: Array<NodeIndex>
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
/**
|
|
3341
|
+
* Creates a new topological sort iterator with optional configuration.
|
|
3342
|
+
*
|
|
3343
|
+
* The iterator uses Kahn's algorithm to lazily produce nodes in topological order.
|
|
3344
|
+
* Throws an error if the graph contains cycles.
|
|
3345
|
+
*
|
|
3346
|
+
* @example
|
|
3347
|
+
* ```ts
|
|
3348
|
+
* import { Graph } from "effect"
|
|
3349
|
+
*
|
|
3350
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
3351
|
+
* const a = Graph.addNode(mutable, "A")
|
|
3352
|
+
* const b = Graph.addNode(mutable, "B")
|
|
3353
|
+
* const c = Graph.addNode(mutable, "C")
|
|
3354
|
+
* Graph.addEdge(mutable, a, b, 1)
|
|
3355
|
+
* Graph.addEdge(mutable, b, c, 1)
|
|
3356
|
+
* })
|
|
3357
|
+
*
|
|
3358
|
+
* // Standard topological sort
|
|
3359
|
+
* const topo1 = Graph.topo(graph)
|
|
3360
|
+
* for (const nodeIndex of Graph.indices(topo1)) {
|
|
3361
|
+
* console.log(nodeIndex) // 0, 1, 2 (topological order)
|
|
3362
|
+
* }
|
|
3363
|
+
*
|
|
3364
|
+
* // With initial nodes
|
|
3365
|
+
* const topo2 = Graph.topo(graph, { initials: [0] })
|
|
3366
|
+
*
|
|
3367
|
+
* // Throws error for cyclic graph
|
|
3368
|
+
* const cyclicGraph = Graph.directed<string, number>((mutable) => {
|
|
3369
|
+
* const a = Graph.addNode(mutable, "A")
|
|
3370
|
+
* const b = Graph.addNode(mutable, "B")
|
|
3371
|
+
* Graph.addEdge(mutable, a, b, 1)
|
|
3372
|
+
* Graph.addEdge(mutable, b, a, 2) // Creates cycle
|
|
3373
|
+
* })
|
|
3374
|
+
*
|
|
3375
|
+
* try {
|
|
3376
|
+
* Graph.topo(cyclicGraph) // Throws: "Cannot perform topological sort on cyclic graph"
|
|
3377
|
+
* } catch (error) {
|
|
3378
|
+
* console.log((error as Error).message)
|
|
3379
|
+
* }
|
|
3380
|
+
* ```
|
|
3381
|
+
*
|
|
3382
|
+
* @since 3.18.0
|
|
3383
|
+
* @category iterators
|
|
3384
|
+
*/
|
|
3385
|
+
export const topo = <N, E, T extends Kind = "directed">(
|
|
3386
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
3387
|
+
config: TopoConfig = {}
|
|
3388
|
+
): NodeWalker<N> => {
|
|
3389
|
+
// Check if graph is acyclic first
|
|
3390
|
+
if (!isAcyclic(graph)) {
|
|
3391
|
+
throw new Error("Cannot perform topological sort on cyclic graph")
|
|
3392
|
+
}
|
|
3393
|
+
|
|
3394
|
+
const initials = config.initials ?? []
|
|
3395
|
+
|
|
3396
|
+
// Validate that all initial nodes exist
|
|
3397
|
+
for (const nodeIndex of initials) {
|
|
3398
|
+
if (!hasNode(graph, nodeIndex)) {
|
|
3399
|
+
throw missingNode(nodeIndex)
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
return new Walker((f) => ({
|
|
3404
|
+
[Symbol.iterator]: () => {
|
|
3405
|
+
const inDegree = new Map<NodeIndex, number>()
|
|
3406
|
+
const remaining = new Set<NodeIndex>()
|
|
3407
|
+
const queue = [...initials]
|
|
3408
|
+
|
|
3409
|
+
// Initialize in-degree counts
|
|
3410
|
+
for (const [nodeIndex] of graph.nodes) {
|
|
3411
|
+
inDegree.set(nodeIndex, 0)
|
|
3412
|
+
remaining.add(nodeIndex)
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
// Calculate in-degrees
|
|
3416
|
+
for (const [, edgeData] of graph.edges) {
|
|
3417
|
+
const currentInDegree = inDegree.get(edgeData.target) || 0
|
|
3418
|
+
inDegree.set(edgeData.target, currentInDegree + 1)
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
// Add nodes with zero in-degree to queue if no initials provided
|
|
3422
|
+
if (initials.length === 0) {
|
|
3423
|
+
for (const [nodeIndex, degree] of inDegree) {
|
|
3424
|
+
if (degree === 0) {
|
|
3425
|
+
queue.push(nodeIndex)
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
const nextMapped = () => {
|
|
3431
|
+
while (queue.length > 0) {
|
|
3432
|
+
const current = queue.shift()!
|
|
3433
|
+
|
|
3434
|
+
if (remaining.has(current)) {
|
|
3435
|
+
remaining.delete(current)
|
|
3436
|
+
|
|
3437
|
+
// Process outgoing edges, reducing in-degree of targets
|
|
3438
|
+
const neighbors = neighborsDirected(graph, current, "outgoing")
|
|
3439
|
+
for (const neighbor of neighbors) {
|
|
3440
|
+
if (remaining.has(neighbor)) {
|
|
3441
|
+
const currentInDegree = inDegree.get(neighbor) || 0
|
|
3442
|
+
const newInDegree = currentInDegree - 1
|
|
3443
|
+
inDegree.set(neighbor, newInDegree)
|
|
3444
|
+
|
|
3445
|
+
// If in-degree becomes 0, add to queue
|
|
3446
|
+
if (newInDegree === 0) {
|
|
3447
|
+
queue.push(neighbor)
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
const nodeData = getNode(graph, current)
|
|
3453
|
+
if (Option.isSome(nodeData)) {
|
|
3454
|
+
return { done: false, value: f(current, nodeData.value) }
|
|
3455
|
+
}
|
|
3456
|
+
return nextMapped()
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
return { done: true, value: undefined } as const
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
return { next: nextMapped }
|
|
3464
|
+
}
|
|
3465
|
+
}))
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
/**
|
|
3469
|
+
* Creates a new DFS postorder iterator with optional configuration.
|
|
3470
|
+
*
|
|
3471
|
+
* The iterator maintains a stack with visit state tracking and emits nodes
|
|
3472
|
+
* in postorder (after all descendants have been processed). Essential for
|
|
3473
|
+
* dependency resolution and tree destruction algorithms.
|
|
3474
|
+
*
|
|
3475
|
+
* @example
|
|
3476
|
+
* ```ts
|
|
3477
|
+
* import { Graph } from "effect"
|
|
3478
|
+
*
|
|
3479
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
3480
|
+
* const root = Graph.addNode(mutable, "root")
|
|
3481
|
+
* const child1 = Graph.addNode(mutable, "child1")
|
|
3482
|
+
* const child2 = Graph.addNode(mutable, "child2")
|
|
3483
|
+
* Graph.addEdge(mutable, root, child1, 1)
|
|
3484
|
+
* Graph.addEdge(mutable, root, child2, 1)
|
|
3485
|
+
* })
|
|
3486
|
+
*
|
|
3487
|
+
* // Postorder: children before parents
|
|
3488
|
+
* const postOrder = Graph.dfsPostOrder(graph, { start: [0] })
|
|
3489
|
+
* for (const node of postOrder) {
|
|
3490
|
+
* console.log(node) // 1, 2, 0
|
|
3491
|
+
* }
|
|
3492
|
+
* ```
|
|
3493
|
+
*
|
|
3494
|
+
* @since 3.18.0
|
|
3495
|
+
* @category iterators
|
|
3496
|
+
*/
|
|
3497
|
+
export const dfsPostOrder = <N, E, T extends Kind = "directed">(
|
|
3498
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
3499
|
+
config: SearchConfig = {}
|
|
3500
|
+
): NodeWalker<N> => {
|
|
3501
|
+
const start = config.start ?? []
|
|
3502
|
+
const direction = config.direction ?? "outgoing"
|
|
3503
|
+
|
|
3504
|
+
// Validate that all start nodes exist
|
|
3505
|
+
for (const nodeIndex of start) {
|
|
3506
|
+
if (!hasNode(graph, nodeIndex)) {
|
|
3507
|
+
throw missingNode(nodeIndex)
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
return new Walker((f) => ({
|
|
3512
|
+
[Symbol.iterator]: () => {
|
|
3513
|
+
const stack: Array<{ node: NodeIndex; visitedChildren: boolean }> = []
|
|
3514
|
+
const discovered = new Set<NodeIndex>()
|
|
3515
|
+
const finished = new Set<NodeIndex>()
|
|
3516
|
+
|
|
3517
|
+
// Initialize stack with start nodes
|
|
3518
|
+
for (let i = start.length - 1; i >= 0; i--) {
|
|
3519
|
+
stack.push({ node: start[i], visitedChildren: false })
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3522
|
+
const nextMapped = () => {
|
|
3523
|
+
while (stack.length > 0) {
|
|
3524
|
+
const current = stack[stack.length - 1]
|
|
3525
|
+
|
|
3526
|
+
if (!discovered.has(current.node)) {
|
|
3527
|
+
discovered.add(current.node)
|
|
3528
|
+
current.visitedChildren = false
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
if (!current.visitedChildren) {
|
|
3532
|
+
current.visitedChildren = true
|
|
3533
|
+
const neighbors = neighborsDirected(graph, current.node, direction)
|
|
3534
|
+
|
|
3535
|
+
for (let i = neighbors.length - 1; i >= 0; i--) {
|
|
3536
|
+
const neighbor = neighbors[i]
|
|
3537
|
+
if (!discovered.has(neighbor) && !finished.has(neighbor)) {
|
|
3538
|
+
stack.push({ node: neighbor, visitedChildren: false })
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
} else {
|
|
3542
|
+
const nodeToEmit = stack.pop()!.node
|
|
3543
|
+
|
|
3544
|
+
if (!finished.has(nodeToEmit)) {
|
|
3545
|
+
finished.add(nodeToEmit)
|
|
3546
|
+
|
|
3547
|
+
const nodeData = getNode(graph, nodeToEmit)
|
|
3548
|
+
if (Option.isSome(nodeData)) {
|
|
3549
|
+
return { done: false, value: f(nodeToEmit, nodeData.value) }
|
|
3550
|
+
}
|
|
3551
|
+
return nextMapped()
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
return { done: true, value: undefined } as const
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
return { next: nextMapped }
|
|
3560
|
+
}
|
|
3561
|
+
}))
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
/**
|
|
3565
|
+
* Creates an iterator over all node indices in the graph.
|
|
3566
|
+
*
|
|
3567
|
+
* The iterator produces node indices in the order they were added to the graph.
|
|
3568
|
+
* This provides access to all nodes regardless of connectivity.
|
|
3569
|
+
*
|
|
3570
|
+
* @example
|
|
3571
|
+
* ```ts
|
|
3572
|
+
* import { Graph } from "effect"
|
|
3573
|
+
*
|
|
3574
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
3575
|
+
* const a = Graph.addNode(mutable, "A")
|
|
3576
|
+
* const b = Graph.addNode(mutable, "B")
|
|
3577
|
+
* const c = Graph.addNode(mutable, "C")
|
|
3578
|
+
* Graph.addEdge(mutable, a, b, 1)
|
|
3579
|
+
* })
|
|
3580
|
+
*
|
|
3581
|
+
* const indices = Array.from(Graph.indices(Graph.nodes(graph)))
|
|
3582
|
+
* console.log(indices) // [0, 1, 2]
|
|
3583
|
+
* ```
|
|
3584
|
+
*
|
|
3585
|
+
* @since 3.18.0
|
|
3586
|
+
* @category iterators
|
|
3587
|
+
*/
|
|
3588
|
+
export const nodes = <N, E, T extends Kind = "directed">(
|
|
3589
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>
|
|
3590
|
+
): NodeWalker<N> =>
|
|
3591
|
+
new Walker((f) => ({
|
|
3592
|
+
[Symbol.iterator]() {
|
|
3593
|
+
const nodeMap = graph.nodes
|
|
3594
|
+
const iterator = nodeMap.entries()
|
|
3595
|
+
|
|
3596
|
+
return {
|
|
3597
|
+
next() {
|
|
3598
|
+
const result = iterator.next()
|
|
3599
|
+
if (result.done) {
|
|
3600
|
+
return { done: true, value: undefined }
|
|
3601
|
+
}
|
|
3602
|
+
const [nodeIndex, nodeData] = result.value
|
|
3603
|
+
return { done: false, value: f(nodeIndex, nodeData) }
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
}))
|
|
3608
|
+
|
|
3609
|
+
/**
|
|
3610
|
+
* Creates an iterator over all edge indices in the graph.
|
|
3611
|
+
*
|
|
3612
|
+
* The iterator produces edge indices in the order they were added to the graph.
|
|
3613
|
+
* This provides access to all edges regardless of connectivity.
|
|
3614
|
+
*
|
|
3615
|
+
* @example
|
|
3616
|
+
* ```ts
|
|
3617
|
+
* import { Graph } from "effect"
|
|
3618
|
+
*
|
|
3619
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
3620
|
+
* const a = Graph.addNode(mutable, "A")
|
|
3621
|
+
* const b = Graph.addNode(mutable, "B")
|
|
3622
|
+
* const c = Graph.addNode(mutable, "C")
|
|
3623
|
+
* Graph.addEdge(mutable, a, b, 1)
|
|
3624
|
+
* Graph.addEdge(mutable, b, c, 2)
|
|
3625
|
+
* })
|
|
3626
|
+
*
|
|
3627
|
+
* const indices = Array.from(Graph.indices(Graph.edges(graph)))
|
|
3628
|
+
* console.log(indices) // [0, 1]
|
|
3629
|
+
* ```
|
|
3630
|
+
*
|
|
3631
|
+
* @since 3.18.0
|
|
3632
|
+
* @category iterators
|
|
3633
|
+
*/
|
|
3634
|
+
export const edges = <N, E, T extends Kind = "directed">(
|
|
3635
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>
|
|
3636
|
+
): EdgeWalker<E> =>
|
|
3637
|
+
new Walker((f) => ({
|
|
3638
|
+
[Symbol.iterator]() {
|
|
3639
|
+
const edgeMap = graph.edges
|
|
3640
|
+
const iterator = edgeMap.entries()
|
|
3641
|
+
|
|
3642
|
+
return {
|
|
3643
|
+
next() {
|
|
3644
|
+
const result = iterator.next()
|
|
3645
|
+
if (result.done) {
|
|
3646
|
+
return { done: true, value: undefined }
|
|
3647
|
+
}
|
|
3648
|
+
const [edgeIndex, edgeData] = result.value
|
|
3649
|
+
return { done: false, value: f(edgeIndex, edgeData) }
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
}))
|
|
3654
|
+
|
|
3655
|
+
/**
|
|
3656
|
+
* Configuration for externals iterator.
|
|
3657
|
+
*
|
|
3658
|
+
* @since 3.18.0
|
|
3659
|
+
* @category models
|
|
3660
|
+
*/
|
|
3661
|
+
export interface ExternalsConfig {
|
|
3662
|
+
readonly direction?: Direction
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
/**
|
|
3666
|
+
* Creates an iterator over external nodes (nodes without edges in specified direction).
|
|
3667
|
+
*
|
|
3668
|
+
* External nodes are nodes that have no outgoing edges (direction="outgoing") or
|
|
3669
|
+
* no incoming edges (direction="incoming"). These are useful for finding
|
|
3670
|
+
* sources, sinks, or isolated nodes.
|
|
3671
|
+
*
|
|
3672
|
+
* @example
|
|
3673
|
+
* ```ts
|
|
3674
|
+
* import { Graph } from "effect"
|
|
3675
|
+
*
|
|
3676
|
+
* const graph = Graph.directed<string, number>((mutable) => {
|
|
3677
|
+
* const source = Graph.addNode(mutable, "source") // 0 - no incoming
|
|
3678
|
+
* const middle = Graph.addNode(mutable, "middle") // 1 - has both
|
|
3679
|
+
* const sink = Graph.addNode(mutable, "sink") // 2 - no outgoing
|
|
3680
|
+
* const isolated = Graph.addNode(mutable, "isolated") // 3 - no edges
|
|
3681
|
+
*
|
|
3682
|
+
* Graph.addEdge(mutable, source, middle, 1)
|
|
3683
|
+
* Graph.addEdge(mutable, middle, sink, 2)
|
|
3684
|
+
* })
|
|
3685
|
+
*
|
|
3686
|
+
* // Nodes with no outgoing edges (sinks + isolated)
|
|
3687
|
+
* const sinks = Array.from(Graph.indices(Graph.externals(graph, { direction: "outgoing" })))
|
|
3688
|
+
* console.log(sinks) // [2, 3]
|
|
3689
|
+
*
|
|
3690
|
+
* // Nodes with no incoming edges (sources + isolated)
|
|
3691
|
+
* const sources = Array.from(Graph.indices(Graph.externals(graph, { direction: "incoming" })))
|
|
3692
|
+
* console.log(sources) // [0, 3]
|
|
3693
|
+
* ```
|
|
3694
|
+
*
|
|
3695
|
+
* @since 3.18.0
|
|
3696
|
+
* @category iterators
|
|
3697
|
+
*/
|
|
3698
|
+
export const externals = <N, E, T extends Kind = "directed">(
|
|
3699
|
+
graph: Graph<N, E, T> | MutableGraph<N, E, T>,
|
|
3700
|
+
config: ExternalsConfig = {}
|
|
3701
|
+
): NodeWalker<N> => {
|
|
3702
|
+
const direction = config.direction ?? "outgoing"
|
|
3703
|
+
|
|
3704
|
+
return new Walker((f) => ({
|
|
3705
|
+
[Symbol.iterator]: () => {
|
|
3706
|
+
const nodeMap = graph.nodes
|
|
3707
|
+
const adjacencyMap = direction === "incoming"
|
|
3708
|
+
? graph.reverseAdjacency
|
|
3709
|
+
: graph.adjacency
|
|
3710
|
+
|
|
3711
|
+
const nodeIterator = nodeMap.entries()
|
|
3712
|
+
|
|
3713
|
+
const nextMapped = () => {
|
|
3714
|
+
let current = nodeIterator.next()
|
|
3715
|
+
while (!current.done) {
|
|
3716
|
+
const [nodeIndex, nodeData] = current.value
|
|
3717
|
+
const adjacencyList = adjacencyMap.get(nodeIndex)
|
|
3718
|
+
|
|
3719
|
+
// Node is external if it has no edges in the specified direction
|
|
3720
|
+
if (adjacencyList === undefined || adjacencyList.length === 0) {
|
|
3721
|
+
return { done: false, value: f(nodeIndex, nodeData) }
|
|
3722
|
+
}
|
|
3723
|
+
current = nodeIterator.next()
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
return { done: true, value: undefined } as const
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
return { next: nextMapped }
|
|
3730
|
+
}
|
|
3731
|
+
}))
|
|
3732
|
+
}
|