Wormhole
1Connecting
All Wormhole interactions go through Stargate's SignalR hub. Clients never connect to Wormhole directly.
Production:
https://stargate.walterpmoore.com/whDevelopment:
https://localhost:7161/wh
Manual — JavaScript
Include the SignalR client and build a connection manually:
// CDN (no bundler)
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.min.js"></script>
const connection = new signalR.HubConnectionBuilder()
.withUrl("https://stargate.walterpmoore.com/wh")
.withAutomaticReconnect()
.build();
await connection.start();
JS
Manual — Python
pip install signalrcore before using the examples below.
from signalrcore.hub_connection_builder import HubConnectionBuilder
connection = (
HubConnectionBuilder()
.with_url("https://stargate.walterpmoore.com/wh")
.with_automatic_reconnect({
"type": "raw",
"keep_alive_interval": 10,
"reconnect_interval": 5,
"max_attempts": 5,
})
.build()
)
connection.start()
PY
C# Client
Microsoft.AspNetCore.SignalR.Client to your project before using the examples below.
using WalterPMoore.Cid.Central.Wormhole;
var client = new WormholeClient(); // dev: true for localhost
await client.ConnectAsync();
C#
2JavaScript Module
wormhole.js is a thin module that wraps the SignalR connection and handles
body-shape conventions automatically. It is the recommended approach for web app integrations.
https://wormhole.walterpmoore.com/wormhole.js
· ↓ download
import { WormholeClient } from 'https://wormhole.walterpmoore.com/wormhole.js';
const client = new WormholeClient({ dev: false });
await client.connect();
JS
Pass dev: true to point at localhost, or url to override the hub URL entirely.
Exposed API
| Method | Returns | Description |
|---|---|---|
| connect() | Promise | Connect to the hub. Must be called before anything else. |
| disconnect() | Promise | Gracefully close the connection. |
| openPortal(source, portalName, options?) | Promise<string> | Submit a single job. Returns the job ID. |
| openPortals(source, portals[]) | Promise<string[]> | Submit a sequence of jobs. Returns all job IDs immediately. |
| queryJobStatus(jobId) | Promise<JobStatus|null> | Fetch the current status of a job via Stargate. |
| subscribeToJob(jobId) | Promise | Subscribe to updates for a job created outside this connection (e.g. a post-push job). |
| subscribeToJobs(jobIds[]) | Promise | Subscribe to multiple jobs at once. |
| subscribeToJobAndQuery(jobId) | Promise<JobStatus|null> | Subscribe and return current status atomically. Preferred over separate subscribe + query. |
| subscribeToJobsAndQuery(jobIds[]) | Promise<(JobStatus|null)[]> | Subscribe to multiple jobs and return all statuses at once. |
| onJobStatus(handler) | this | Register a handler for status-change events. |
| onJobComplete(handler) | this | Register a handler for job completion events. |
| onJobFailed(handler) | this | Register a handler for job failure events. |
4C# Client
WormholeClient is a typed C# wrapper around the SignalR connection.
It handles body-shape conventions automatically and exposes simple async methods
and .NET events for job tracking.
Microsoft.AspNetCore.SignalR.Client
using WalterPMoore.Cid.Central.Wormhole;
// Production (default)
var client = new WormholeClient();
// Development
var client = new WormholeClient(dev: true);
// Custom URL
var client = new WormholeClient(url: "https://my-stargate-instance/wh");
await client.ConnectAsync();
C#
Exposed API
| Member | Type | Description |
|---|---|---|
| ConnectAsync() | Task | Connect to the hub. Must be called before anything else. |
| DisconnectAsync() | Task | Gracefully close the connection. |
| IsConnected | bool | Whether the client is currently connected. |
| OpenPortalAsync(source, portalName, options?) | Task<string> | Submit a single job. Returns the job ID. |
| OpenPortalsAsync(source, portals[]) | Task<IReadOnlyList<string>> | Submit a sequence of jobs. Returns all job IDs immediately. |
| QueryJobStatusAsync(jobId) | Task<JobStatus?> | Fetch the current status of a job via Stargate. |
| SubscribeToJobAsync(jobId) | Task | Subscribe to updates for a job created outside this connection. |
| SubscribeToJobsAsync(jobIds) | Task | Subscribe to multiple jobs at once. |
| SubscribeToJobAndQueryAsync(jobId) | Task<JobStatus?> | Subscribe and return current status atomically. |
| SubscribeToJobsAndQueryAsync(jobIds) | Task<JobStatus?[]> | Subscribe to multiple jobs and return all statuses at once. |
| OnJobStatus | event Action<string, JobStatus> | Fired when a job transitions to a new status. |
| OnJobComplete | event Action<string, object?> | Fired when a job completes. Result is job-type specific and may be null. |
| OnJobFailed | event Action<string, string> | Fired when a job fails. Second argument is the error message. |
4Opening Portals
A portal is a job request tied to a CID Central model.
The source is a workspace/branch/revision path.
Tool portals (those under Tool/) apply a CID Tools operation to the model.
Other portals have their own flat input shape.
When opening multiple portals against the same source, use OpenPortals.
The model is downloaded once and passed through each job in sequence — no redundant downloads.
All job IDs are returned immediately; sequencing is enforced server-side.
Input key.
The module handles this automatically. If calling the hub directly, wrap your options:
{ "Input": { ... } }
Available portals
| Portal | Type | Description |
|---|---|---|
| Tool/Audit | Tool | Audits the model for problems. Returns a problem count. |
| Tool/Supplement | Tool | Supplements and universalizes the model. |
| Tool/Merge | Tool | Merges another model into this one. |
| Tool/Transform | Tool | Applies a geometric transformation to the model. |
| Tool/Connectivity | Tool | Runs connectivity analysis on the model. |
| Tool/GuidReferences | Tool | Arms GUID references on the model. |
| Tool/Supplement/Materials | Tool | Supplements materials from the catalog. |
| Tool/Supplement/Sections | Tool | Supplements sections from the catalog. |
| Tool/Columns/Join | Tool | Joins columns in the model. |
| Tool/Columns/Split | Tool | Splits columns in the model. |
| Tool/UpdateGroups | Tool | Updates element groups. |
| Tool/RemoveRecords | Tool | Removes records from the model via merge. |
| Tool/FrameResults | Tool | Compiles frame results into the model. |
| Job/Diff | Job | Compares the model to a reference and stores the diff. See note below. |
| Job/FastGeo | Job | Generates a 3D geometry preview and stores it alongside the model. |
| Job/ExampleIdle | Job | Waits for a configurable duration. For testing purposes. |
By default,
Job/Diff compares the primary model against the previous revision on
the same branch (revision - 1). If the primary model is revision 0, or if there is
no previous revision, the diff is skipped gracefully.To compare against a specific revision instead, pass a
comparePortal in the options:
{
"comparePortal": { "source": "myproject/main/2" }
}
comparePortal.source accepts the same formats as the primary source —
a workspace/branch/revision path or a front desk GUID.
When specified alongside a batch sequence, the compare portal is always downloaded fresh
and is not subject to the continuation cache.
// Single job
const jobId = await client.openPortal(
'myproject/main/0',
'Tool/Supplement',
{
isUniversalizeDoubleSections: true,
isSupplementMaterials: true,
isSupplementSections: true,
isUniversalize: true,
}
);
// Sequence — model downloaded once, jobs run in order
const jobIds = await client.openPortals(
'myproject/main/0',
[
{
portalName: 'Tool/Supplement',
options: { isUniversalize: true },
},
{
portalName: 'Tool/Audit',
},
]
);
JS
// Single tool job — note the Input wrapper
const jobId = await connection.invoke(
'OpenPortal',
'myproject/main/0',
'Tool/Supplement',
{
Input: {
isUniversalizeDoubleSections: true,
isSupplementMaterials: true,
isSupplementSections: true,
isUniversalize: true,
}
}
);
// Sequence
const jobIds = await connection.invoke(
'OpenPortals',
'myproject/main/0',
[
{ PortalName: 'Tool/Supplement', Body: { Input: { isUniversalize: true } } },
{ PortalName: 'Tool/Audit', Body: { Input: {} } },
]
);
JS
// Single job
string jobId = await client.OpenPortalAsync(
"myproject/main/0",
"Tool/Supplement",
new {
isUniversalize = true,
isSupplementMaterials = true,
isSupplementSections = true,
});
// Sequence — model downloaded once, jobs run in order
IReadOnlyList<string> jobIds = await client.OpenPortalsAsync(
"myproject/main/0",
[
new("Tool/Supplement", new { isUniversalize = true }),
new("Tool/Audit"),
]);
C#
# Single job
job_id = connection.send(
"OpenPortal",
[
"myproject/main/0",
"Tool/Supplement",
{
"Input": {
"isUniversalize": True,
"isSupplementMaterials": True,
"isSupplementSections": True,
}
},
],
)
# Sequence — model downloaded once, jobs run in order
job_ids = connection.send(
"OpenPortals",
[
"myproject/main/0",
[
{"PortalName": "Tool/Supplement", "Body": {"Input": {"isUniversalize": True}}},
{"PortalName": "Tool/Audit", "Body": {"Input": {}}},
],
],
)
PY
5Querying Status
Query a job's current status immediately after submission to avoid a race condition
where a fast job completes before SignalR event handlers are registered.
Status values are the JobStatus enum serialized as strings: Created · Running · Completed · Failed · Canceled
const status = await client.queryJobStatus(jobId);
console.log(status); // e.g. "Running"
JS
const status = await connection.invoke('QueryJobStatus', jobId);
console.log(status); // e.g. "Running"
JS
JobStatus? status = await client.QueryJobStatusAsync(jobId);
Console.WriteLine(status); // e.g. Running
C#
status = connection.send("QueryJobStatus", [job_id])
print(status) # e.g. "Running"
PY
6Callbacks
Three events are pushed from Stargate to the client during a job's lifetime. Register handlers before submitting jobs.
| Event | Arguments | Description |
|---|---|---|
| ReceiveJobStatus | jobId, status | Fired when a job transitions to Running, then again to Completed or Failed. |
| ReceiveJobComplete | jobId, result | Fired when a job completes. result is job-type specific and may be null. |
| ReceiveJobFailed | jobId, error | Fired when a job fails. error is a string description. |
client
.onJobStatus((jobId, status) => {
console.log(`Job ${jobId}: ${status}`);
})
.onJobComplete((jobId, result) => {
console.log(`Job ${jobId} complete`, result);
})
.onJobFailed((jobId, error) => {
console.error(`Job ${jobId} failed: ${error}`);
});
// Then submit
const jobId = await client.openPortal('myproject/main/0', 'Tool/Supplement');
await client.queryJobStatus(jobId); // seed initial status
JS
connection.on('ReceiveJobStatus', (jobId, status) => {
console.log(`Job ${jobId}: ${status}`);
});
connection.on('ReceiveJobComplete', (jobId, result) => {
console.log(`Job ${jobId} complete`, result);
});
connection.on('ReceiveJobFailed', (jobId, error) => {
console.error(`Job ${jobId} failed: ${error}`);
});
// Then submit
const jobId = await connection.invoke('OpenPortal', 'myproject/main/0', 'Tool/Supplement', { Input: {} });
const status = await connection.invoke('QueryJobStatus', jobId); // seed initial status
JS
client.OnJobStatus += (jobId, status) => Console.WriteLine($"{jobId}: {status}");
client.OnJobComplete += (jobId, result) => Console.WriteLine($"{jobId} complete");
client.OnJobFailed += (jobId, error) => Console.WriteLine($"{jobId} failed: {error}");
// Register handlers, then submit
string jobId = await client.OpenPortalAsync("myproject/main/0", "Tool/Supplement");
await client.QueryJobStatusAsync(jobId); // seed initial status
C#
connection.on("ReceiveJobStatus", lambda args:
print(f"Job {args[0]}: {args[1]}"))
connection.on("ReceiveJobComplete", lambda args:
print(f"Job {args[0]} complete", args[1]))
connection.on("ReceiveJobFailed", lambda args:
print(f"Job {args[0]} failed: {args[1]}"))
# Register handlers before starting the connection, then submit
connection.start()
job_id = connection.send("OpenPortal", ["myproject/main/0", "Tool/Supplement", {"Input": {}}])
connection.send("QueryJobStatus", [job_id]) # seed initial status
PY
7Subscribing to External Jobs
Jobs created outside your SignalR connection — such as post-push jobs launched
server-side by PushModel — are not automatically routed to any client.
To receive updates for them, subscribe using the job IDs returned in the
PostJobIds field of the push response.
Call queryJobStatus immediately after subscribing to seed the current
state, in case the job is already running or finished by the time you subscribe.
// After a PushModel call that returns PostJobIds:
const { postJobIds } = pushResponse;
// Subscribe and get current statuses atomically — single call per job.
// Use the returned statuses to seed your UI immediately.
const statuses = await client.subscribeToJobsAndQuery(postJobIds);
postJobIds.forEach((id, i) => {
console.log(id, statuses[i]); // e.g. "Running", "Completed"
});
// From here, onJobStatus / onJobComplete / onJobFailed fire normally.
JS
// After a PushModel call that returns PostJobIds:
const { postJobIds } = pushResponse;
// SubscribeToJobAndQuery registers the connection and returns current
// status atomically, closing the race window.
const statuses = await Promise.all(
postJobIds.map(id => connection.invoke('SubscribeToJobAndQuery', id))
);
// From here, ReceiveJobStatus / ReceiveJobComplete / ReceiveJobFailed fire normally.
JS
// After a PushModel call that returns PostJobIds:
JobStatus?[] statuses = await client.SubscribeToJobsAndQueryAsync(postJobIds);
foreach (var (id, status) in postJobIds.Zip(statuses))
{
Console.WriteLine($"{id}: {status}");
}
// From here, OnJobStatus / OnJobComplete / OnJobFailed fire normally.
C#
# After a PushModel call that returns post_job_ids:
import concurrent.futures
def subscribe_and_query(job_id):
status = connection.send("SubscribeToJobAndQuery", [job_id])
print(f"{job_id}: {status}")
with concurrent.futures.ThreadPoolExecutor() as executor:
executor.map(subscribe_and_query, post_job_ids)
# From here, ReceiveJobStatus / ReceiveJobComplete / ReceiveJobFailed fire normally.
PY
8PushModel with Auto-Tasks
When pushing a model to CID Central, you can request that one or more Wormhole
jobs run against the uploaded revision immediately after it is stored.
The jobs are returned in the response as PostJobIds so you can
subscribe to and track their progress.
Jobs specified in PostJobs run after any server-side default jobs
(see Default Jobs), all as a single sequenced batch —
the model is only downloaded once.
POST /api/cid/central/Model/{workspace}/{branch}/{revision?}
PushArguments fields
| Field | Type | Description |
|---|---|---|
| PushMessage | string? | A message describing the purpose of the push. |
| IsHardOverwrite | bool? | Completely replace the existing revision rather than merging. |
| NoMergeJustUseIncoming | bool? | Skip the existing model entirely and push to the next revision. |
| IsCheckForMerge | bool? | If false, create a new revision even when the merge is a no-op. Default true. |
| MergeOptions | object? | Fine-grained merge configuration. |
| SkipDefaultPostJobs | bool? | Suppress server-configured default jobs. Caller-specified PostJobs still run. |
| PostJobs | array? | Ordered list of jobs to run after the push. See shape below. |
Request
// POST /api/cid/central/Model/myproject/main
{
"Model": { /* ... CID model ... */ },
"PushArguments": {
"PushMessage": "Weekly model update",
"PostJobs": [
{
"PortalName": "Tool/Supplement",
"Options": {
"isUniversalize": true,
"isSupplementMaterials": true,
"isSupplementSections": true
}
},
{
"PortalName": "Tool/Audit"
}
]
}
}
JSON
Response
{
"pushAction": "Merge",
"model": { /* uploaded CidCentralModel */ },
"direction": "Upload",
"postJobIds": [
"a1b2c3d4-...", // Job/Diff (default)
"e5f6a7b8-...", // Tool/Supplement (caller)
"c9d0e1f2-..." // Tool/Audit (caller)
]
}
JSON
Tracking the jobs
Pass PostJobIds to subscribeToJobs after connecting to the hub.
See Subscribing to External Jobs for the full pattern.
9Default Jobs
Default jobs run automatically after every successful push, before any
caller-specified PostJobs. They are defined server-side in
WormholeDispatcher.DefaultJobs and require no action from the caller.
Pass SkipDefaultPostJobs: true in PushArguments to suppress them.
Current default jobs
| Portal | Options | Description |
|---|---|---|
| Job/Diff | — | Compares the pushed revision to the previous revision on the same branch and stores the diff. Skipped gracefully if the revision is the first on its branch. |
WormholeDispatcher.DefaultJobs
in Stargate. No schema changes or client updates are required.
Execution order
For a push with default jobs A and B and caller jobs C and D, the full sequence is
A → B → C → D, all sharing a single model download via continuation.
If SkipDefaultPostJobs is true, only C → D run.