graphwise 1.6.0 → 1.8.0

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.
Files changed (84) hide show
  1. package/README.md +81 -30
  2. package/dist/async/index.cjs +243 -0
  3. package/dist/async/index.cjs.map +1 -0
  4. package/dist/async/index.js +230 -0
  5. package/dist/async/index.js.map +1 -0
  6. package/dist/expansion/base-core.d.ts +24 -0
  7. package/dist/expansion/base-core.d.ts.map +1 -0
  8. package/dist/expansion/base-core.unit.test.d.ts +10 -0
  9. package/dist/expansion/base-core.unit.test.d.ts.map +1 -0
  10. package/dist/expansion/base-helpers.d.ts +57 -0
  11. package/dist/expansion/base-helpers.d.ts.map +1 -0
  12. package/dist/expansion/base.d.ts +32 -1
  13. package/dist/expansion/base.d.ts.map +1 -1
  14. package/dist/expansion/dfs-priority.d.ts +11 -0
  15. package/dist/expansion/dfs-priority.d.ts.map +1 -1
  16. package/dist/expansion/dome.d.ts +20 -0
  17. package/dist/expansion/dome.d.ts.map +1 -1
  18. package/dist/expansion/edge.d.ts +18 -0
  19. package/dist/expansion/edge.d.ts.map +1 -1
  20. package/dist/expansion/flux.d.ts +16 -0
  21. package/dist/expansion/flux.d.ts.map +1 -1
  22. package/dist/expansion/frontier-balanced.d.ts +11 -0
  23. package/dist/expansion/frontier-balanced.d.ts.map +1 -1
  24. package/dist/expansion/fuse.d.ts +16 -0
  25. package/dist/expansion/fuse.d.ts.map +1 -1
  26. package/dist/expansion/hae.d.ts +16 -0
  27. package/dist/expansion/hae.d.ts.map +1 -1
  28. package/dist/expansion/lace.d.ts +16 -0
  29. package/dist/expansion/lace.d.ts.map +1 -1
  30. package/dist/expansion/maze.d.ts +17 -0
  31. package/dist/expansion/maze.d.ts.map +1 -1
  32. package/dist/expansion/pipe.d.ts +16 -0
  33. package/dist/expansion/pipe.d.ts.map +1 -1
  34. package/dist/expansion/random-priority.d.ts +18 -0
  35. package/dist/expansion/random-priority.d.ts.map +1 -1
  36. package/dist/expansion/reach.d.ts +17 -0
  37. package/dist/expansion/reach.d.ts.map +1 -1
  38. package/dist/expansion/sage.d.ts +15 -0
  39. package/dist/expansion/sage.d.ts.map +1 -1
  40. package/dist/expansion/sift.d.ts +16 -0
  41. package/dist/expansion/sift.d.ts.map +1 -1
  42. package/dist/expansion/standard-bfs.d.ts +11 -0
  43. package/dist/expansion/standard-bfs.d.ts.map +1 -1
  44. package/dist/expansion/tide.d.ts +16 -0
  45. package/dist/expansion/tide.d.ts.map +1 -1
  46. package/dist/expansion/warp.d.ts +16 -0
  47. package/dist/expansion/warp.d.ts.map +1 -1
  48. package/dist/index/index.cjs +1060 -99
  49. package/dist/index/index.cjs.map +1 -1
  50. package/dist/index/index.js +1015 -100
  51. package/dist/index/index.js.map +1 -1
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/ranking/mi/adamic-adar.d.ts +8 -0
  55. package/dist/ranking/mi/adamic-adar.d.ts.map +1 -1
  56. package/dist/ranking/mi/adaptive.d.ts +8 -0
  57. package/dist/ranking/mi/adaptive.d.ts.map +1 -1
  58. package/dist/ranking/mi/cosine.d.ts +7 -0
  59. package/dist/ranking/mi/cosine.d.ts.map +1 -1
  60. package/dist/ranking/mi/etch.d.ts +8 -0
  61. package/dist/ranking/mi/etch.d.ts.map +1 -1
  62. package/dist/ranking/mi/hub-promoted.d.ts +7 -0
  63. package/dist/ranking/mi/hub-promoted.d.ts.map +1 -1
  64. package/dist/ranking/mi/jaccard.d.ts +7 -0
  65. package/dist/ranking/mi/jaccard.d.ts.map +1 -1
  66. package/dist/ranking/mi/notch.d.ts +8 -0
  67. package/dist/ranking/mi/notch.d.ts.map +1 -1
  68. package/dist/ranking/mi/overlap-coefficient.d.ts +7 -0
  69. package/dist/ranking/mi/overlap-coefficient.d.ts.map +1 -1
  70. package/dist/ranking/mi/resource-allocation.d.ts +8 -0
  71. package/dist/ranking/mi/resource-allocation.d.ts.map +1 -1
  72. package/dist/ranking/mi/scale.d.ts +7 -0
  73. package/dist/ranking/mi/scale.d.ts.map +1 -1
  74. package/dist/ranking/mi/skew.d.ts +7 -0
  75. package/dist/ranking/mi/skew.d.ts.map +1 -1
  76. package/dist/ranking/mi/sorensen.d.ts +7 -0
  77. package/dist/ranking/mi/sorensen.d.ts.map +1 -1
  78. package/dist/ranking/mi/span.d.ts +8 -0
  79. package/dist/ranking/mi/span.d.ts.map +1 -1
  80. package/dist/ranking/mi/types.d.ts +12 -0
  81. package/dist/ranking/mi/types.d.ts.map +1 -1
  82. package/dist/ranking/parse.d.ts +24 -1
  83. package/dist/ranking/parse.d.ts.map +1 -1
  84. package/package.json +6 -1
package/README.md CHANGED
@@ -14,6 +14,7 @@ Low-dependency TypeScript graph algorithms for citation network analysis: novel
14
14
  - **Seed selection**: GRASP, Stratified
15
15
  - **Subgraph extraction**: ego-network, k-core, k-truss, motif, induced, filter
16
16
  - **Optional WebGPU acceleration**
17
+ - **Async support**: Generator coroutine protocol, sync/async runners, all algorithms available as `*Async` variants
17
18
 
18
19
  ## Installation
19
20
 
@@ -37,7 +38,7 @@ const ranked = parse(graph, result.paths, { mi: jaccard });
37
38
 
38
39
  ### Expansion: BASE Framework
39
40
 
40
- **Boundary-free Adaptive Seeded Expansion** (BASE) is a parameter-free graph expansion algorithm. Given a graph $G = (V, E)$ and seed nodes $S \subseteq V$, BASE produces the subgraph induced by all vertices visited during priority-ordered expansion until frontier exhaustion:
41
+ **Boundary-free Adaptive Seeded Expansion** (BASE) discovers the neighbourhood around seed nodes without any configuration. You provide seeds and a priority function; BASE expands outward, visiting the most interesting nodes first and recording paths when search frontiers from different seeds collide. It stops naturally when there is nothing left to explore — no depth limits, no size thresholds, no parameters to tune.
41
42
 
42
43
  $$G_S = (V_S, E_S) \quad \text{where} \quad V_S = \bigcup_{v \in S} \text{Expand}(v, \pi)$$
43
44
 
@@ -51,7 +52,7 @@ Three key properties:
51
52
 
52
53
  #### DOME: Degree-Ordered Multi-seed Expansion
53
54
 
54
- The default priority function uses degree-based hub deferral:
55
+ Explores low-connectivity nodes before hubs. In a social network, DOME visits niche specialists before reaching the well-connected influencers, discovering the quiet corners of the graph before the busy crossroads.
55
56
 
56
57
  $$\pi(v) = \frac{\deg^{+}(v) + \deg^{-}(v)}{w_V(v) + \varepsilon}$$
57
58
 
@@ -81,99 +82,117 @@ where $\deg^{+}(v)$ is weighted out-degree, $\deg^{-}(v)$ is weighted in-degree,
81
82
 
82
83
  #### EDGE: Entropy-Driven Graph Expansion
83
84
 
85
+ Finds nodes that sit at the boundary between different kinds of things. If a person's friends include scientists, artists, and engineers (high type diversity), EDGE visits them early — they are likely bridges between communities.
86
+
84
87
  $$\pi_{\text{EDGE}}(v) = \frac{1}{H_{\text{local}}(v) + \varepsilon} \times \log(\deg(v) + 1)$$
85
88
 
86
- where $H_{\text{local}}(v) = -\sum_{\tau} p(\tau) \log p(\tau)$ is the Shannon entropy of the neighbour type distribution. Nodes bridging heterogeneous structural regimes (high entropy) are explored first.
89
+ where $H_{\text{local}}(v) = -\sum_{\tau} p(\tau) \log p(\tau)$ is the Shannon entropy of the neighbour type distribution.
87
90
 
88
91
  ---
89
92
 
90
93
  #### PIPE: Path-potential Informed Priority Expansion
91
94
 
95
+ Rushes towards nodes that are about to connect two search frontiers. When expanding from multiple seeds, PIPE detects that a node's neighbours have already been reached by another seed's frontier — meaning a connecting path is one step away.
96
+
92
97
  $$\pi_{\text{PIPE}}(v) = \frac{\deg(v)}{1 + \mathrm{pathPotential}(v)}$$
93
98
 
94
- where $\mathrm{pathPotential}(v) = \lvert N(v) \cap \bigcup_{j \neq i} V_j \rvert$ counts neighbours already visited by other seed frontiers. High path potential indicates imminent path completion.
99
+ where $\mathrm{pathPotential}(v) = \lvert N(v) \cap \bigcup_{j \neq i} V_j \rvert$ counts neighbours already visited by other seed frontiers.
95
100
 
96
101
  ---
97
102
 
98
103
  #### SAGE: Salience-Accumulation Guided Expansion
99
104
 
105
+ Learns from its own discoveries. Phase 1 explores by degree (like DOME). Once the first path is found, SAGE switches to Phase 2: nodes that appear in many discovered paths get top priority, guiding expansion towards structurally rich regions.
106
+
100
107
  $$
101
108
  \pi_{\text{SAGE}}(v) = \begin{cases} \log(\deg(v) + 1) & \text{Phase 1 (before first path)} \\ -(\text{salience}(v) \times 1000 - \deg(v)) & \text{Phase 2 (after first path)} \end{cases}
102
109
  $$
103
110
 
104
- where $\text{salience}(v)$ counts discovered paths containing $v$. Salience dominates in Phase 2; degree serves as tiebreaker.
111
+ where $\text{salience}(v)$ counts discovered paths containing $v$.
105
112
 
106
113
  ---
107
114
 
108
115
  #### REACH: Retrospective Expansion with Adaptive Convergence
109
116
 
117
+ Uses the quality of already-discovered paths to steer future exploration. Phase 1 explores by degree. Once paths are found, REACH asks "which unexplored nodes look structurally similar to the endpoints of my best paths?" and prioritises those — seeking more of what already worked.
118
+
110
119
  $$
111
120
  \pi_{\text{REACH}}(v) = \begin{cases} \log(\deg(v) + 1) & \text{Phase 1} \\ \log(\deg(v) + 1) \times (1 - \widehat{\text{MI}}(v)) & \text{Phase 2} \end{cases}
112
121
  $$
113
122
 
114
- where $\widehat{\text{MI}}(v)$ estimates MI via Jaccard similarity to discovered path endpoints:
115
-
116
- $$
117
- \widehat{\text{MI}}(v) = \frac{1}{\lvert \mathcal{P}\_{\text{top}} \rvert} \sum\_{p} J(N(v), N(p\_{\text{endpoint}}))
118
- $$
123
+ where $\widehat{\text{MI}}(v) = \frac{1}{\lvert \mathcal{P}\_{\text{top}} \rvert} \sum\_{p} J(N(v), N(p\_{\text{endpoint}}))$ estimates MI via Jaccard similarity to discovered path endpoints.
119
124
 
120
125
  ---
121
126
 
122
127
  #### MAZE: Multi-frontier Adaptive Zone Expansion
123
128
 
129
+ Combines the best of PIPE and SAGE across three phases. First, it races to find initial paths using path potential (like PIPE). Then it refines exploration using salience feedback (like SAGE). Finally, it decides when to stop based on whether it's still discovering diverse, high-quality paths.
130
+
124
131
  $$
125
132
  \pi^{(1)}(v) = \frac{\deg(v)}{1 + \mathrm{pathPotential}(v)} \qquad \pi^{(2)}(v) = \pi^{(1)}(v) \times \frac{1}{1 + \lambda \cdot \text{salience}(v)}
126
133
  $$
127
134
 
128
- Phase 1 uses PIPE's path potential until $M$ paths found. Phase 2 incorporates SAGE's salience feedback. Phase 3 evaluates diversity, path count, and salience plateau for termination.
135
+ Phase 1 uses path potential until $M$ paths found. Phase 2 adds salience feedback. Phase 3 evaluates diversity, path count, and salience plateau for termination.
129
136
 
130
137
  ---
131
138
 
132
139
  #### TIDE: Total Interconnected Degree Expansion
133
140
 
141
+ Avoids dense clusters by looking at total neighbourhood connectivity. A node surrounded by other well-connected nodes gets deferred; a node in a quiet corner of the graph gets explored first.
142
+
134
143
  $$\pi_{\text{TIDE}}(v) = \deg(v) + \sum_{w \in N(v)} \deg(w)$$
135
144
 
136
- Nodes in sparse regions (low aggregate neighbourhood degree) are explored first. Related to EDGE but uses raw degree sums rather than entropy.
145
+ Related to EDGE but uses raw degree sums rather than entropy.
137
146
 
138
147
  ---
139
148
 
140
149
  #### LACE: Local Affinity-Computed Expansion
141
150
 
151
+ Explores towards nodes that are most similar to what the frontier has already seen. If a candidate node shares many neighbours with the explored region, it gets priority — building outward from a coherent core.
152
+
142
153
  $$\pi_{\text{LACE}}(v) = 1 - \overline{\text{MI}}(v, \text{frontier})$$
143
154
 
144
- Prioritises nodes by average MI to already-visited frontier nodes. Related to HAE but uses MI to visited nodes rather than type entropy.
155
+ Related to HAE but uses MI to visited nodes rather than type entropy.
145
156
 
146
157
  ---
147
158
 
148
159
  #### WARP: Weighted Adjacent Reachability Priority
149
160
 
161
+ Aggressively prioritises nodes that look like they will connect two search frontiers, regardless of their degree. If a node's neighbours have been visited by another seed's search, it gets top priority.
162
+
150
163
  $$\pi_{\text{WARP}}(v) = \frac{1}{1 + \text{bridge}(v)}$$
151
164
 
152
- Pure cross-frontier bridge score without degree normalisation. Related to PIPE but omits the degree numerator.
165
+ Related to PIPE but omits the degree numerator, making it more aggressive at prioritising bridge nodes.
153
166
 
154
167
  ---
155
168
 
156
169
  #### FUSE: Fused Utility-Salience Expansion
157
170
 
171
+ Balances two signals simultaneously: how connected a node is (degree) and how strongly it relates to the explored region (MI). The weight $w$ controls the trade-off — at $w=0$ it behaves like DOME, at $w=1$ it behaves like LACE.
172
+
158
173
  $$\pi_{\text{FUSE}}(v) = (1 - w) \cdot \deg(v) + w \cdot (1 - \overline{\text{MI}})$$
159
174
 
160
- Single-phase weighted blend of degree and MI. Related to SAGE but uses continuous blending rather than two-phase transition.
175
+ Related to SAGE but uses continuous blending rather than two-phase transition.
161
176
 
162
177
  ---
163
178
 
164
179
  #### SIFT: Salience-Informed Frontier Threshold
165
180
 
181
+ Acts as a gate: nodes with MI above a threshold get MI-based priority (explore the promising ones); nodes below the threshold get deferred with a large degree-based penalty (ignore the unpromising ones). A binary version of REACH's continuous approach.
182
+
166
183
  $$
167
184
  \pi_{\text{SIFT}}(v) = \begin{cases} 1 - \overline{\text{MI}} & \text{if } \overline{\text{MI}} \geq \tau \\ \deg(v) + 100 & \text{otherwise} \end{cases}
168
185
  $$
169
186
 
170
- MI-threshold-based priority with degree fallback. Related to REACH but uses a hard threshold instead of continuous MI-weighted priority.
187
+ Related to REACH but uses a hard threshold instead of continuous MI-weighted priority.
171
188
 
172
189
  ---
173
190
 
174
191
  #### FLUX: Flexible Local Utility Crossover
175
192
 
176
- Density-adaptive strategy switching. Selects between DOME, EDGE, and PIPE modes per-node based on local graph density and cross-frontier bridge score. Related to MAZE but adapts spatially (per-node) rather than temporally (per-phase).
193
+ Adapts its strategy to the local topology of each node. In dense regions it uses low-degree-first exploration (like EDGE); near frontier boundaries it uses bridge detection (like PIPE); in sparse regions it falls back to degree ordering (like DOME). Different parts of the graph are explored with different strategies simultaneously.
194
+
195
+ Related to MAZE but adapts spatially (per-node) rather than temporally (per-phase).
177
196
 
178
197
  ---
179
198
 
@@ -192,73 +211,83 @@ Density-adaptive strategy switching. Selects between DOME, EDGE, and PIPE modes
192
211
 
193
212
  ### Path Ranking: PARSE
194
213
 
195
- **Path Aggregation Ranked by Salience Estimation** (PARSE) scores paths by the geometric mean of per-edge mutual information, eliminating length bias:
214
+ **Path Aggregation Ranked by Salience Estimation** (PARSE) ranks discovered paths by asking "how consistently strong is every edge along this path?" It uses the geometric mean of per-edge MI scores, which means one weak link drags down the entire path — unlike arithmetic mean where a strong edge can compensate for a weak one. A 10-hop path with consistently good edges scores the same as a 2-hop path with equally good edges.
196
215
 
197
216
  $$M(P) = \exp\left( \frac{1}{k} \sum_{i=1}^{k} \log I(u_i, v_i) \right)$$
198
217
 
199
- where $k$ is path length (number of edges) and $I(u_i, v_i)$ is the per-edge MI score from any variant below. The geometric mean ensures a 10-hop path with consistently high-MI edges scores equally to a 2-hop path with the same average MI.
218
+ where $k$ is path length (number of edges) and $I(u_i, v_i)$ is the per-edge MI score from any variant below.
200
219
 
201
220
  ---
202
221
 
203
222
  ### MI Variants
204
223
 
205
- Seven MI variants serve as per-edge estimators within PARSE. All build on Jaccard neighbourhood overlap, then weight by domain-specific structural properties.
224
+ MI variants answer the question "how strongly are two connected nodes related?" Each measures the overlap between their neighbourhoods, then optionally weights by structural properties like density, degree rarity, clustering, or entity type. PARSE uses these as per-edge scores in its geometric mean.
206
225
 
207
226
  ---
208
227
 
209
228
  #### Jaccard (baseline)
210
229
 
211
- $$I_{\text{Jac}}(u, v) = \frac{|N(u) \cap N(v)|}{|N(u) \cup N(v)|}$$
230
+ What fraction of combined neighbours do two nodes share? If Alice and Bob know 3 of the same people out of 10 total acquaintances between them, their Jaccard score is 0.3.
212
231
 
213
- Standard neighbourhood overlap. Default MI estimator.
232
+ $$I_{\text{Jac}}(u, v) = \frac{|N(u) \cap N(v)|}{|N(u) \cup N(v)|}$$
214
233
 
215
234
  ---
216
235
 
217
236
  #### Adamic-Adar
218
237
 
219
- $$I_{\text{AA}}(u, v) = \sum_{w \in N(u) \cap N(v)} \frac{1}{\log(\deg(w) + 1)}$$
238
+ Counts shared neighbours, but recognises that sharing a rare connection is more meaningful than sharing a popular one. If two researchers both cite a niche paper, that says more about their relationship than both citing a famous textbook.
220
239
 
221
- Downweights common neighbours with high degree. Shared hub neighbours are less informative than shared rare neighbours.
240
+ $$I_{\text{AA}}(u, v) = \sum_{w \in N(u) \cap N(v)} \frac{1}{\log(\deg(w) + 1)}$$
222
241
 
223
242
  ---
224
243
 
225
244
  #### SCALE: Structural Correction via Adjusted Local Estimation
226
245
 
246
+ Adjusts for graph density. In a dense network where everyone knows everyone, sharing neighbours is expected and less meaningful. In a sparse network, the same overlap is rare and significant. SCALE divides Jaccard by density to make scores comparable across differently-dense regions.
247
+
227
248
  $$I_{\text{SCALE}}(u, v) = \frac{J(N(u), N(v))}{\rho(G)}$$
228
249
 
229
- where $\rho(G) = \frac{2|E|}{|V|(|V|-1)}$ is graph density. Normalises Jaccard by density so that overlap in dense subgraphs is not artificially inflated.
250
+ where $\rho(G) = \frac{2|E|}{|V|(|V|-1)}$ is graph density.
230
251
 
231
252
  ---
232
253
 
233
254
  #### SKEW: Sparse-weighted Knowledge Emphasis Weighting
234
255
 
256
+ Rewards edges between rare (low-degree) nodes and penalises edges involving hubs. Like TF-IDF in search engines: a connection between two niche nodes is more informative than a connection between two mega-hubs that connect to everything.
257
+
235
258
  $$I_{\text{SKEW}}(u, v) = J(N(u), N(v)) \cdot \log\!\left(\frac{N}{\deg(u) + 1}\right) \cdot \log\!\left(\frac{N}{\deg(v) + 1}\right)$$
236
259
 
237
- where $N = |V|$. IDF-style rarity weighting on both endpoints. Paths through low-degree (rare) nodes score higher; paths through hubs score lower.
260
+ where $N = |V|$.
238
261
 
239
262
  ---
240
263
 
241
264
  #### SPAN: Spanning-community Penalty for Adjacent Nodes
242
265
 
266
+ Rewards edges that bridge separate communities and penalises edges within tight-knit groups. If both endpoints sit in dense clusters where everyone knows everyone (high clustering coefficient), the edge is probably redundant. If at least one endpoint is a bridge between groups, the edge is structurally interesting.
267
+
243
268
  $$I_{\text{SPAN}}(u, v) = J(N(u), N(v)) \cdot \bigl(1 - \max(C(u), C(v))\bigr)$$
244
269
 
245
- where $C(v)$ is the local clustering coefficient. Penalises edges within tight clusters; rewards edges bridging communities (structural holes).
270
+ where $C(v)$ is the local clustering coefficient.
246
271
 
247
272
  ---
248
273
 
249
274
  #### ETCH: Edge Type Contrast Heuristic
250
275
 
276
+ Boosts edges of rare types. If a graph has 1000 "knows" edges but only 5 "mentors" edges, a mentoring relationship is worth more than an acquaintanceship. ETCH multiplies Jaccard by the log-rarity of the edge type.
277
+
251
278
  $$I_{\text{ETCH}}(u, v) = J(N(u), N(v)) \cdot \log\!\left(\frac{|E|}{\text{count}(\text{edges with type}(u,v))}\right)$$
252
279
 
253
- Weights Jaccard by edge-type rarity. Paths traversing rare edge types receive higher scores. Requires edge-type annotations; falls back to Jaccard when unavailable.
280
+ Requires edge-type annotations; falls back to Jaccard when unavailable.
254
281
 
255
282
  ---
256
283
 
257
284
  #### NOTCH: Node Type Contrast Heuristic
258
285
 
286
+ Boosts edges connecting rare node types. In a graph with 500 people but only 10 organisations, an edge involving an organisation is more distinctive. NOTCH multiplies Jaccard by the log-rarity of both endpoint types.
287
+
259
288
  $$I_{\text{NOTCH}}(u, v) = J(N(u), N(v)) \cdot \log\!\left(\frac{|V|}{c(\tau_u)}\right) \cdot \log\!\left(\frac{|V|}{c(\tau_v)}\right)$$
260
289
 
261
- where $c(\tau_u)$ is the count of nodes with the same type as $u$. Weights Jaccard by node-type rarity for both endpoints.
290
+ where $c(\tau_u)$ is the count of nodes with the same type as $u$.
262
291
 
263
292
  ---
264
293
 
@@ -296,7 +325,7 @@ where $c(\tau_u)$ is the count of nodes with the same type as $u$. Weights Jacca
296
325
 
297
326
  ### Seed Selection: GRASP
298
327
 
299
- **Graph-agnostic Representative seed pAir Sampling**: selects structurally representative seed pairs from an unknown graph using reservoir sampling and structural feature clustering. Operates blind: no full graph loading, no ground-truth labels, no human-defined strata.
328
+ **Graph-agnostic Representative seed pAir Sampling** picks starting points for expansion algorithms. Given a graph you have never seen before, GRASP streams through its edges, samples a representative set of nodes, clusters them by structural role (hubs, bridges, peripherals), and returns seed pairs that cover the full range of structural diversity — without loading the entire graph into memory.
300
329
 
301
330
  Three phases:
302
331
 
@@ -323,6 +352,28 @@ import { ... } from 'graphwise/extraction'; // Subgraph extraction
323
352
  import { ... } from 'graphwise/utils'; // Utilities
324
353
  import { ... } from 'graphwise/gpu'; // WebGPU acceleration
325
354
  import { ... } from 'graphwise/schemas'; // Zod schemas
355
+ import { ... } from 'graphwise/async'; // Async runners & protocol
356
+ ```
357
+
358
+ ### Async Usage
359
+
360
+ All algorithms are available as `*Async` variants for use with remote or lazy graph data sources:
361
+
362
+ ```typescript
363
+ import { domeAsync, parseAsync, jaccardAsync } from "graphwise";
364
+ import type { AsyncReadableGraph } from "graphwise/graph";
365
+
366
+ // Your async graph implementation
367
+ const remoteGraph: AsyncReadableGraph = createRemoteGraph();
368
+
369
+ const result = await domeAsync(remoteGraph, seeds, {
370
+ signal: controller.signal,
371
+ onProgress: (stats) => console.log(stats),
372
+ });
373
+
374
+ const ranked = await parseAsync(remoteGraph, result.paths, {
375
+ mi: jaccardAsync,
376
+ });
326
377
  ```
327
378
 
328
379
  ## Commands
@@ -0,0 +1,243 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/async/utils.ts
3
+ /**
4
+ * Async utility functions.
5
+ *
6
+ * @module async/utils
7
+ */
8
+ /** Collect an AsyncIterable into a readonly array. */
9
+ async function collectAsyncIterable(iter) {
10
+ const result = [];
11
+ for await (const item of iter) result.push(item);
12
+ return result;
13
+ }
14
+ /** Default yield strategy: setTimeout(0) to yield to the event loop. */
15
+ function defaultYieldStrategy() {
16
+ return new Promise((r) => {
17
+ setTimeout(r, 0);
18
+ });
19
+ }
20
+ //#endregion
21
+ //#region src/async/runners.ts
22
+ /**
23
+ * Resolve a single GraphOp against a synchronous ReadableGraph.
24
+ *
25
+ * Returns a tagged GraphOpResponse so the receiving generator can narrow
26
+ * the result type without type assertions.
27
+ *
28
+ * @param graph - The synchronous graph to query
29
+ * @param op - The operation to resolve
30
+ * @returns The tagged response
31
+ */
32
+ function resolveSyncOp(graph, op) {
33
+ switch (op.tag) {
34
+ case "neighbours": return {
35
+ tag: "neighbours",
36
+ value: Array.from(graph.neighbours(op.id, op.direction))
37
+ };
38
+ case "degree": return {
39
+ tag: "degree",
40
+ value: graph.degree(op.id, op.direction)
41
+ };
42
+ case "getNode": return {
43
+ tag: "getNode",
44
+ value: graph.getNode(op.id)
45
+ };
46
+ case "getEdge": return {
47
+ tag: "getEdge",
48
+ value: graph.getEdge(op.source, op.target)
49
+ };
50
+ case "hasNode": return {
51
+ tag: "hasNode",
52
+ value: graph.hasNode(op.id)
53
+ };
54
+ case "yield": return { tag: "yield" };
55
+ case "progress": return { tag: "progress" };
56
+ }
57
+ }
58
+ /**
59
+ * Drive a generator to completion using a synchronous graph.
60
+ *
61
+ * The generator yields GraphOp requests; each is resolved immediately
62
+ * against the graph and the tagged response is fed back via gen.next().
63
+ *
64
+ * @param gen - The generator to drive
65
+ * @param graph - The graph to resolve ops against
66
+ * @returns The generator's return value
67
+ */
68
+ function runSync(gen, graph) {
69
+ let step = gen.next();
70
+ while (step.done !== true) {
71
+ const response = resolveSyncOp(graph, step.value);
72
+ step = gen.next(response);
73
+ }
74
+ return step.value;
75
+ }
76
+ /**
77
+ * Resolve a single GraphOp against an async ReadableGraph.
78
+ *
79
+ * AsyncIterables (neighbours) are collected into readonly arrays so the
80
+ * generator receives the same value type as in sync mode. Returns a tagged
81
+ * GraphOpResponse for type-safe narrowing without assertions.
82
+ *
83
+ * @param graph - The async graph to query
84
+ * @param op - The operation to resolve
85
+ * @returns A promise resolving to the tagged response
86
+ */
87
+ async function resolveAsyncOp(graph, op) {
88
+ switch (op.tag) {
89
+ case "neighbours": return {
90
+ tag: "neighbours",
91
+ value: await collectAsyncIterable(graph.neighbours(op.id, op.direction))
92
+ };
93
+ case "degree": return {
94
+ tag: "degree",
95
+ value: await graph.degree(op.id, op.direction)
96
+ };
97
+ case "getNode": return {
98
+ tag: "getNode",
99
+ value: await graph.getNode(op.id)
100
+ };
101
+ case "getEdge": return {
102
+ tag: "getEdge",
103
+ value: await graph.getEdge(op.source, op.target)
104
+ };
105
+ case "hasNode": return {
106
+ tag: "hasNode",
107
+ value: await graph.hasNode(op.id)
108
+ };
109
+ case "yield": return { tag: "yield" };
110
+ case "progress": return { tag: "progress" };
111
+ }
112
+ }
113
+ /**
114
+ * Drive a generator to completion using an async graph.
115
+ *
116
+ * Extends sync semantics with:
117
+ * - Cancellation via AbortSignal (throws DOMException "AbortError")
118
+ * - Cooperative yielding at `yield` ops (calls yieldStrategy)
119
+ * - Progress callbacks at `progress` ops (may be async for backpressure)
120
+ * - Error propagation: graph errors are forwarded via gen.throw(); if the
121
+ * generator does not handle them, they propagate to the caller
122
+ *
123
+ * @param gen - The generator to drive
124
+ * @param graph - The async graph to resolve ops against
125
+ * @param options - Runner configuration
126
+ * @returns A promise resolving to the generator's return value
127
+ */
128
+ async function runAsync(gen, graph, options) {
129
+ const signal = options?.signal;
130
+ const onProgress = options?.onProgress;
131
+ const yieldStrategy = options?.yieldStrategy ?? defaultYieldStrategy;
132
+ let step = gen.next();
133
+ while (step.done !== true) {
134
+ if (signal?.aborted === true) {
135
+ const abortError = new DOMException("Aborted", "AbortError");
136
+ try {
137
+ gen.throw(abortError);
138
+ } catch {
139
+ throw abortError;
140
+ }
141
+ throw abortError;
142
+ }
143
+ const op = step.value;
144
+ if (op.tag === "yield") {
145
+ await yieldStrategy();
146
+ step = gen.next({ tag: "yield" });
147
+ continue;
148
+ }
149
+ if (op.tag === "progress") {
150
+ if (onProgress !== void 0) {
151
+ const maybePromise = onProgress(op.stats);
152
+ if (maybePromise instanceof Promise) await maybePromise;
153
+ }
154
+ step = gen.next({ tag: "progress" });
155
+ continue;
156
+ }
157
+ let response;
158
+ try {
159
+ response = await resolveAsyncOp(graph, op);
160
+ } catch (error) {
161
+ step = gen.throw(error);
162
+ continue;
163
+ }
164
+ step = gen.next(response);
165
+ }
166
+ return step.value;
167
+ }
168
+ //#endregion
169
+ //#region src/async/ops.ts
170
+ function* opNeighbours(id, direction) {
171
+ const response = yield direction !== void 0 ? {
172
+ tag: "neighbours",
173
+ id,
174
+ direction
175
+ } : {
176
+ tag: "neighbours",
177
+ id
178
+ };
179
+ if (response.tag !== "neighbours") throw new TypeError(`Expected neighbours response, got ${response.tag}`);
180
+ return response.value;
181
+ }
182
+ function* opDegree(id, direction) {
183
+ const response = yield direction !== void 0 ? {
184
+ tag: "degree",
185
+ id,
186
+ direction
187
+ } : {
188
+ tag: "degree",
189
+ id
190
+ };
191
+ if (response.tag !== "degree") throw new TypeError(`Expected degree response, got ${response.tag}`);
192
+ return response.value;
193
+ }
194
+ function* opGetNode(id) {
195
+ const response = yield {
196
+ tag: "getNode",
197
+ id
198
+ };
199
+ if (response.tag !== "getNode") throw new TypeError(`Expected getNode response, got ${response.tag}`);
200
+ return response.value;
201
+ }
202
+ function* opGetEdge(source, target) {
203
+ const response = yield {
204
+ tag: "getEdge",
205
+ source,
206
+ target
207
+ };
208
+ if (response.tag !== "getEdge") throw new TypeError(`Expected getEdge response, got ${response.tag}`);
209
+ return response.value;
210
+ }
211
+ function* opHasNode(id) {
212
+ const response = yield {
213
+ tag: "hasNode",
214
+ id
215
+ };
216
+ if (response.tag !== "hasNode") throw new TypeError(`Expected hasNode response, got ${response.tag}`);
217
+ return response.value;
218
+ }
219
+ function* opYield() {
220
+ yield { tag: "yield" };
221
+ }
222
+ function* opProgress(stats) {
223
+ yield {
224
+ tag: "progress",
225
+ stats
226
+ };
227
+ }
228
+ //#endregion
229
+ exports.collectAsyncIterable = collectAsyncIterable;
230
+ exports.defaultYieldStrategy = defaultYieldStrategy;
231
+ exports.opDegree = opDegree;
232
+ exports.opGetEdge = opGetEdge;
233
+ exports.opGetNode = opGetNode;
234
+ exports.opHasNode = opHasNode;
235
+ exports.opNeighbours = opNeighbours;
236
+ exports.opProgress = opProgress;
237
+ exports.opYield = opYield;
238
+ exports.resolveAsyncOp = resolveAsyncOp;
239
+ exports.resolveSyncOp = resolveSyncOp;
240
+ exports.runAsync = runAsync;
241
+ exports.runSync = runSync;
242
+
243
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/async/utils.ts","../../src/async/runners.ts","../../src/async/ops.ts"],"sourcesContent":["/**\n * Async utility functions.\n *\n * @module async/utils\n */\n\n/** Collect an AsyncIterable into a readonly array. */\nexport async function collectAsyncIterable<T>(\n\titer: AsyncIterable<T>,\n): Promise<readonly T[]> {\n\tconst result: T[] = [];\n\tfor await (const item of iter) result.push(item);\n\treturn result;\n}\n\n/** Default yield strategy: setTimeout(0) to yield to the event loop. */\nexport function defaultYieldStrategy(): Promise<void> {\n\treturn new Promise((r) => {\n\t\tsetTimeout(r, 0);\n\t});\n}\n","/**\n * Sync and async runners for generator-based graph algorithms.\n *\n * The runner drives a generator that yields GraphOp objects, resolves each op\n * against the graph, and feeds the result back via gen.next(response). This\n * allows algorithm logic to be written once as a generator and executed\n * synchronously or asynchronously depending on the graph backing.\n *\n * @module async/runners\n */\n\nimport type { NodeData, EdgeData, ReadableGraph } from \"../graph\";\nimport type { AsyncReadableGraph } from \"../graph/async-interfaces\";\nimport type { GraphOp, GraphOpResponse } from \"./protocol\";\nimport type { AsyncRunnerOptions } from \"./types\";\nimport { collectAsyncIterable, defaultYieldStrategy } from \"./utils\";\n\n// ---------------------------------------------------------------------------\n// Sync runner\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve a single GraphOp against a synchronous ReadableGraph.\n *\n * Returns a tagged GraphOpResponse so the receiving generator can narrow\n * the result type without type assertions.\n *\n * @param graph - The synchronous graph to query\n * @param op - The operation to resolve\n * @returns The tagged response\n */\nexport function resolveSyncOp<N extends NodeData, E extends EdgeData>(\n\tgraph: ReadableGraph<N, E>,\n\top: GraphOp,\n): GraphOpResponse<N, E> {\n\tswitch (op.tag) {\n\t\tcase \"neighbours\":\n\t\t\treturn {\n\t\t\t\ttag: \"neighbours\",\n\t\t\t\tvalue: Array.from(graph.neighbours(op.id, op.direction)),\n\t\t\t};\n\t\tcase \"degree\":\n\t\t\treturn { tag: \"degree\", value: graph.degree(op.id, op.direction) };\n\t\tcase \"getNode\":\n\t\t\treturn { tag: \"getNode\", value: graph.getNode(op.id) };\n\t\tcase \"getEdge\":\n\t\t\treturn { tag: \"getEdge\", value: graph.getEdge(op.source, op.target) };\n\t\tcase \"hasNode\":\n\t\t\treturn { tag: \"hasNode\", value: graph.hasNode(op.id) };\n\t\tcase \"yield\":\n\t\t\treturn { tag: \"yield\" };\n\t\tcase \"progress\":\n\t\t\treturn { tag: \"progress\" };\n\t}\n}\n\n/**\n * Drive a generator to completion using a synchronous graph.\n *\n * The generator yields GraphOp requests; each is resolved immediately\n * against the graph and the tagged response is fed back via gen.next().\n *\n * @param gen - The generator to drive\n * @param graph - The graph to resolve ops against\n * @returns The generator's return value\n */\nexport function runSync<N extends NodeData, E extends EdgeData, R>(\n\tgen: Generator<GraphOp, R, GraphOpResponse<N, E>>,\n\tgraph: ReadableGraph<N, E>,\n): R {\n\tlet step = gen.next();\n\twhile (step.done !== true) {\n\t\tconst response = resolveSyncOp(graph, step.value);\n\t\tstep = gen.next(response);\n\t}\n\treturn step.value;\n}\n\n// ---------------------------------------------------------------------------\n// Async runner\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve a single GraphOp against an async ReadableGraph.\n *\n * AsyncIterables (neighbours) are collected into readonly arrays so the\n * generator receives the same value type as in sync mode. Returns a tagged\n * GraphOpResponse for type-safe narrowing without assertions.\n *\n * @param graph - The async graph to query\n * @param op - The operation to resolve\n * @returns A promise resolving to the tagged response\n */\nexport async function resolveAsyncOp<N extends NodeData, E extends EdgeData>(\n\tgraph: AsyncReadableGraph<N, E>,\n\top: GraphOp,\n): Promise<GraphOpResponse<N, E>> {\n\tswitch (op.tag) {\n\t\tcase \"neighbours\":\n\t\t\treturn {\n\t\t\t\ttag: \"neighbours\",\n\t\t\t\tvalue: await collectAsyncIterable(\n\t\t\t\t\tgraph.neighbours(op.id, op.direction),\n\t\t\t\t),\n\t\t\t};\n\t\tcase \"degree\":\n\t\t\treturn { tag: \"degree\", value: await graph.degree(op.id, op.direction) };\n\t\tcase \"getNode\":\n\t\t\treturn { tag: \"getNode\", value: await graph.getNode(op.id) };\n\t\tcase \"getEdge\":\n\t\t\treturn {\n\t\t\t\ttag: \"getEdge\",\n\t\t\t\tvalue: await graph.getEdge(op.source, op.target),\n\t\t\t};\n\t\tcase \"hasNode\":\n\t\t\treturn { tag: \"hasNode\", value: await graph.hasNode(op.id) };\n\t\tcase \"yield\":\n\t\t\treturn { tag: \"yield\" };\n\t\tcase \"progress\":\n\t\t\treturn { tag: \"progress\" };\n\t}\n}\n\n/**\n * Drive a generator to completion using an async graph.\n *\n * Extends sync semantics with:\n * - Cancellation via AbortSignal (throws DOMException \"AbortError\")\n * - Cooperative yielding at `yield` ops (calls yieldStrategy)\n * - Progress callbacks at `progress` ops (may be async for backpressure)\n * - Error propagation: graph errors are forwarded via gen.throw(); if the\n * generator does not handle them, they propagate to the caller\n *\n * @param gen - The generator to drive\n * @param graph - The async graph to resolve ops against\n * @param options - Runner configuration\n * @returns A promise resolving to the generator's return value\n */\nexport async function runAsync<N extends NodeData, E extends EdgeData, R>(\n\tgen: Generator<GraphOp, R, GraphOpResponse<N, E>>,\n\tgraph: AsyncReadableGraph<N, E>,\n\toptions?: AsyncRunnerOptions,\n): Promise<R> {\n\tconst signal = options?.signal;\n\tconst onProgress = options?.onProgress;\n\tconst yieldStrategy = options?.yieldStrategy ?? defaultYieldStrategy;\n\n\tlet step = gen.next();\n\n\twhile (step.done !== true) {\n\t\t// Check for cancellation before processing each op. Throw the error\n\t\t// into the generator so that any finally blocks in the algorithm run\n\t\t// before the error propagates to the caller.\n\t\tif (signal?.aborted === true) {\n\t\t\tconst abortError = new DOMException(\"Aborted\", \"AbortError\");\n\t\t\ttry {\n\t\t\t\tgen.throw(abortError);\n\t\t\t} catch {\n\t\t\t\t// Generator did not handle the error — propagate it\n\t\t\t\tthrow abortError;\n\t\t\t}\n\t\t\t// Generator handled the error but we still honour cancellation\n\t\t\tthrow abortError;\n\t\t}\n\n\t\tconst op = step.value;\n\n\t\t// Handle cooperative yield ops without hitting the graph\n\t\tif (op.tag === \"yield\") {\n\t\t\tawait yieldStrategy();\n\t\t\tstep = gen.next({ tag: \"yield\" });\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Handle progress ops: call the callback (awaiting if async)\n\t\tif (op.tag === \"progress\") {\n\t\t\tif (onProgress !== undefined) {\n\t\t\t\tconst maybePromise = onProgress(op.stats);\n\t\t\t\tif (maybePromise instanceof Promise) {\n\t\t\t\t\tawait maybePromise;\n\t\t\t\t}\n\t\t\t}\n\t\t\tstep = gen.next({ tag: \"progress\" });\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Resolve graph ops, forwarding any errors into the generator\n\t\tlet response: GraphOpResponse<N, E>;\n\t\ttry {\n\t\t\tresponse = await resolveAsyncOp(graph, op);\n\t\t} catch (error) {\n\t\t\t// Forward the error into the generator; if unhandled, it propagates\n\t\t\tstep = gen.throw(error);\n\t\t\tcontinue;\n\t\t}\n\n\t\tstep = gen.next(response);\n\t}\n\n\treturn step.value;\n}\n","/**\n * Type-safe yield helpers for graph operations.\n *\n * Each function is a sub-generator that yields one GraphOp and returns\n * the correctly-typed result. Narrowing is done via the tagged discriminated\n * union in GraphOpResponse — no type assertions needed.\n *\n * Use with `yield*` inside algorithm generators.\n *\n * @module async/ops\n */\n\nimport type { NodeId, NodeData, EdgeData, Direction } from \"../graph\";\nimport type { GraphOp, GraphOpResponse, ProgressStats } from \"./protocol\";\n\nexport function* opNeighbours<\n\tN extends NodeData = NodeData,\n\tE extends EdgeData = EdgeData,\n>(\n\tid: NodeId,\n\tdirection?: Direction,\n): Generator<GraphOp, readonly NodeId[], GraphOpResponse<N, E>> {\n\tconst op: GraphOp =\n\t\tdirection !== undefined\n\t\t\t? { tag: \"neighbours\", id, direction }\n\t\t\t: { tag: \"neighbours\", id };\n\tconst response: GraphOpResponse<N, E> = yield op;\n\tif (response.tag !== \"neighbours\") {\n\t\tthrow new TypeError(`Expected neighbours response, got ${response.tag}`);\n\t}\n\treturn response.value;\n}\n\nexport function* opDegree<\n\tN extends NodeData = NodeData,\n\tE extends EdgeData = EdgeData,\n>(\n\tid: NodeId,\n\tdirection?: Direction,\n): Generator<GraphOp, number, GraphOpResponse<N, E>> {\n\tconst op: GraphOp =\n\t\tdirection !== undefined\n\t\t\t? { tag: \"degree\", id, direction }\n\t\t\t: { tag: \"degree\", id };\n\tconst response: GraphOpResponse<N, E> = yield op;\n\tif (response.tag !== \"degree\") {\n\t\tthrow new TypeError(`Expected degree response, got ${response.tag}`);\n\t}\n\treturn response.value;\n}\n\nexport function* opGetNode<\n\tN extends NodeData = NodeData,\n\tE extends EdgeData = EdgeData,\n>(id: NodeId): Generator<GraphOp, N | undefined, GraphOpResponse<N, E>> {\n\tconst response: GraphOpResponse<N, E> = yield { tag: \"getNode\", id };\n\tif (response.tag !== \"getNode\") {\n\t\tthrow new TypeError(`Expected getNode response, got ${response.tag}`);\n\t}\n\treturn response.value;\n}\n\nexport function* opGetEdge<\n\tN extends NodeData = NodeData,\n\tE extends EdgeData = EdgeData,\n>(\n\tsource: NodeId,\n\ttarget: NodeId,\n): Generator<GraphOp, E | undefined, GraphOpResponse<N, E>> {\n\tconst response: GraphOpResponse<N, E> = yield {\n\t\ttag: \"getEdge\",\n\t\tsource,\n\t\ttarget,\n\t};\n\tif (response.tag !== \"getEdge\") {\n\t\tthrow new TypeError(`Expected getEdge response, got ${response.tag}`);\n\t}\n\treturn response.value;\n}\n\nexport function* opHasNode<\n\tN extends NodeData = NodeData,\n\tE extends EdgeData = EdgeData,\n>(id: NodeId): Generator<GraphOp, boolean, GraphOpResponse<N, E>> {\n\tconst response: GraphOpResponse<N, E> = yield { tag: \"hasNode\", id };\n\tif (response.tag !== \"hasNode\") {\n\t\tthrow new TypeError(`Expected hasNode response, got ${response.tag}`);\n\t}\n\treturn response.value;\n}\n\nexport function* opYield<\n\tN extends NodeData = NodeData,\n\tE extends EdgeData = EdgeData,\n>(): Generator<GraphOp, void, GraphOpResponse<N, E>> {\n\tyield { tag: \"yield\" };\n}\n\nexport function* opProgress<\n\tN extends NodeData = NodeData,\n\tE extends EdgeData = EdgeData,\n>(stats: ProgressStats): Generator<GraphOp, void, GraphOpResponse<N, E>> {\n\tyield { tag: \"progress\", stats };\n}\n"],"mappings":"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"}