@macula-io/ts 0.17.0 → 0.19.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 (49) hide show
  1. package/README.md +106 -272
  2. package/dist/binding.d.ts +50 -45
  3. package/dist/binding.js +8 -125
  4. package/dist/binding.js.map +1 -1
  5. package/dist/content.d.ts +32 -13
  6. package/dist/content.js +43 -41
  7. package/dist/content.js.map +1 -1
  8. package/dist/index.d.ts +5 -9
  9. package/dist/index.js +6 -8
  10. package/dist/index.js.map +1 -1
  11. package/dist/key.d.ts +31 -0
  12. package/dist/key.js +72 -0
  13. package/dist/key.js.map +1 -0
  14. package/dist/pool.d.ts +185 -131
  15. package/dist/pool.js +223 -576
  16. package/dist/pool.js.map +1 -1
  17. package/dist/stream.d.ts +63 -0
  18. package/dist/stream.js +93 -0
  19. package/dist/stream.js.map +1 -0
  20. package/dist/wire.d.ts +53 -0
  21. package/dist/wire.js +79 -0
  22. package/dist/wire.js.map +1 -0
  23. package/package.json +7 -5
  24. package/prebuilds/darwin-arm64/@macula-io+ts.node +0 -0
  25. package/prebuilds/darwin-x64/@macula-io+ts.node +0 -0
  26. package/prebuilds/linux-arm64/@macula-io+ts.node +0 -0
  27. package/prebuilds/linux-x64/@macula-io+ts.node +0 -0
  28. package/prebuilds/win32-x64/@macula-io+ts.node +0 -0
  29. package/dist/dht.d.ts +0 -55
  30. package/dist/dht.js +0 -25
  31. package/dist/dht.js.map +0 -1
  32. package/dist/directdial.d.ts +0 -79
  33. package/dist/directdial.js +0 -76
  34. package/dist/directdial.js.map +0 -1
  35. package/dist/identity.d.ts +0 -43
  36. package/dist/identity.js +0 -83
  37. package/dist/identity.js.map +0 -1
  38. package/dist/pubsub.d.ts +0 -60
  39. package/dist/pubsub.js +0 -8
  40. package/dist/pubsub.js.map +0 -1
  41. package/dist/rpc.d.ts +0 -86
  42. package/dist/rpc.js +0 -57
  43. package/dist/rpc.js.map +0 -1
  44. package/dist/session.d.ts +0 -481
  45. package/dist/session.js +0 -810
  46. package/dist/session.js.map +0 -1
  47. package/dist/ucan.d.ts +0 -110
  48. package/dist/ucan.js +0 -140
  49. package/dist/ucan.js.map +0 -1
package/dist/pool.d.ts CHANGED
@@ -1,146 +1,200 @@
1
- import { Identity } from "./identity.js";
2
- import type { PubsubEvent } from "./pubsub.js";
3
- import { type BytesOutput, type JsonValue } from "./rpc.js";
4
- /** One configured connection target. */
1
+ import { type Handle } from "./binding.js";
2
+ import { NodeKey } from "./key.js";
3
+ import { Stream, StreamMode, type StreamRequest } from "./stream.js";
4
+ import { type ContentOptions, type Mcid } from "./content.js";
5
+ import { type BytesOutput, type Id, type JsonValue } from "./wire.js";
6
+ /** A station to link to, pinned by the node_id it must prove. */
5
7
  export interface Seed {
6
- host: string;
7
- port: number;
8
+ readonly host: string;
9
+ readonly port: number;
10
+ readonly nodeId: Id;
8
11
  }
9
12
  export interface PoolOptions {
10
- /** How many currently-live control links a publish() fans out to
11
- * before resolving -- partial success counts as success, matching
12
- * macula_client:publish/5's own documented semantics. Default 1.
13
- * NOT YET FULLY AT PARITY above 1: the Erlang reference stamps ONE
14
- * seq across every replica specifically so a receiver's own
15
- * (publisher, seq, topic) dedup collapses the redundant copies into
16
- * one event. This SDK's Session.publish() has no caller-supplied-seq
17
- * parameter to do the same (each call mints its own fresh seq), so
18
- * setting this above 1 would deliver N distinct, non-deduplicable
19
- * copies to every subscriber -- clamped to 1 until that's fixed. */
20
- replicationFactor?: number;
21
- /** How long a recorded (realm, publisher, seq, topic) key is treated
22
- * as a duplicate before it ages out. Default 60_000, matching the
23
- * Erlang reference's own `dedup_window_ms` default. */
24
- dedupWindowMs?: number;
25
- /** How often the dedup table is swept for expired entries. Default
26
- * 30_000, matching the Erlang reference's own `dedup_sweep_ms`. */
27
- dedupSweepMs?: number;
28
- /** How often a live control link is health-checked (see
29
- * #armHealthCheck's own doc for why publish() alone can't be trusted
30
- * to ever notice a dead connection). Default 10_000. */
31
- healthCheckIntervalMs?: number;
13
+ /** Each realm's key as carried (hex or bytes), by realm id: an
14
+ * advertisement in a realm is trusted only when its authorization verifies
15
+ * against it, and a procedure is served only in a realm it names. */
16
+ readonly realmTrust?: ReadonlyArray<{
17
+ readonly realm: Id;
18
+ readonly key: string | Uint8Array;
19
+ }>;
20
+ readonly replicationFactor?: number;
21
+ readonly maxDirectLinks?: number;
22
+ readonly respawnDelayMs?: number;
23
+ /** How long connect waits for a first link, 30 s by default. */
24
+ readonly timeoutMs?: number;
32
25
  }
33
- export interface PoolStatus {
34
- /** Seeds whose control link is currently connected. */
35
- healthyLinks: number;
36
- /** Seeds whose control link is not currently connected (connecting
37
- * or backing off). Every configured seed is exactly one or the
38
- * other -- a link backs off and retries forever, it is never given
39
- * up on and dropped from the pool, same as the Erlang reference. */
40
- failedLinks: number;
26
+ /** One of the pool's links. */
27
+ export interface LinkStatus {
28
+ readonly station: string;
29
+ readonly host: string;
30
+ readonly port: number;
31
+ readonly direct: boolean;
32
+ readonly up: boolean;
41
33
  }
42
- /** Thrown by publish()/call() when the pool has zero live control links
43
- * to try -- a distinct, identifiable condition from any one link's own
44
- * transient error, matching macula_client:publish/5's own `{error,
45
- * {transient, no_healthy_station}}`. */
46
- export declare class NoHealthyStationError extends Error {
47
- constructor();
34
+ /** A trusted provider of a procedure and the station it serves from. */
35
+ export interface Provider {
36
+ readonly node: string;
37
+ readonly station: string;
38
+ }
39
+ /** An event a subscription heard, verified. */
40
+ export interface Event {
41
+ readonly publisher: string;
42
+ readonly realm: string;
43
+ readonly topic: string;
44
+ readonly seq: number;
45
+ readonly publishedAt: number;
46
+ readonly payload: JsonValue;
47
+ readonly deliveredVia: string;
48
+ }
49
+ /** A served call's request. */
50
+ export interface Request {
51
+ readonly caller: string;
52
+ readonly realm: string;
53
+ readonly procedure: string;
54
+ readonly payload: JsonValue;
55
+ readonly deadlineMs: number;
56
+ }
57
+ /** A verified DHT record: its type, signer's key id, times, payload, and wire
58
+ * bytes (tagged). */
59
+ export interface DhtRecord {
60
+ readonly type: number;
61
+ readonly keyId: string;
62
+ readonly createdAt: number;
63
+ readonly expiresAt: number;
64
+ readonly payload: JsonValue;
65
+ readonly wire: JsonValue;
66
+ }
67
+ /** macula 12's record types. */
68
+ export declare enum RecordType {
69
+ NodeRecord = 1,
70
+ ProcedureAdvertisement = 6,
71
+ Tombstone = 12,
72
+ ContentAnnouncement = 17,
73
+ StationEndpoint = 18,
74
+ OrgDirectory = 21,
75
+ ProcedureDelegation = 22
76
+ }
77
+ /** A subscription, until stop() or the pool closes. */
78
+ export declare class Subscription {
79
+ private readonly handle;
80
+ readonly closed: Promise<string | null>;
81
+ /** @internal */
82
+ constructor(handle: Handle, closed: Promise<string | null>);
83
+ /** Ends the subscription on every link. */
84
+ stop(): Promise<void>;
85
+ }
86
+ /** A served procedure, until stop(). */
87
+ export declare class Served {
88
+ private readonly handle;
89
+ private stopped;
90
+ /** @internal */
91
+ constructor(handle: Handle);
92
+ /** Withdraws the procedure on every link. */
93
+ stop(): Promise<void>;
48
94
  }
49
- /**
50
- * A resilient multi-station client: live connections to every configured
51
- * seed held concurrently, each independently monitored and respawned
52
- * with backoff on disconnect, every tracked subscription re-established
53
- * automatically when its own link reconnects. See this module's own
54
- * header doc for the full design, why a "link" is a small role-scoped
55
- * session set rather than one Session, and its one deliberate deviation
56
- * from the Erlang reference (topic-scoped dedup).
57
- */
58
95
  export declare class Pool {
59
- #private;
96
+ private readonly handle;
97
+ private closed;
60
98
  private constructor();
61
- /** Dials the control role against every seed concurrently under
62
- * `controlIdentity` (the caller's own identity -- publish()/call()
63
- * are attributed to it on the wire) and resolves once every seed's
64
- * FIRST connect attempt has settled (success or a logged failure
65
- * headed into backoff) -- matches tapRoom()'s own "await the first
66
- * attempt, not the eventual outcome" reasoning (macula-mcp's
67
- * lobby_observer.ts): a caller that publishes/calls immediately
68
- * after connect() must not race links still mid-handshake. */
69
- static connect(seeds: Seed[], controlIdentity: Identity, opts?: PoolOptions): Promise<Pool>;
70
- /** Publishes to `replicationFactor` currently-live control links
71
- * (default 1); partial success counts as success. A link whose
72
- * publish attempt fails is marked for respawn immediately -- this is
73
- * this v1's only liveness signal for the control role, since it
74
- * cannot also carry a liveness-only subscribe (see this module's own
75
- * header doc). Throws NoHealthyStationError if zero links are live.
76
- *
77
- * `realm`/`payload` are validated before any link is touched. Found
78
- * live 2026-09-05: a malformed realm or an unserializable payload
79
- * throws inside session.publish() itself, well before any wire I/O --
80
- * treating that throw as evidence of a dead connection (the pre-fix
81
- * behavior) tore down a perfectly healthy link over a caller-side
82
- * argument bug. */
83
- publish(realm: string | undefined, topic: string, payload: JsonValue, opts?: {
99
+ /** Links the key's node to every seed, and resolves once one link is up. */
100
+ static connect(key: NodeKey, seeds: readonly Seed[], options?: PoolOptions): Promise<Pool>;
101
+ /** The node_id the pool links as. */
102
+ nodeId(): string;
103
+ /** name in this node's own namespace, `~<node_id>/<name>`: a procedure it
104
+ * serves with no org and no realm key, authorized by its advertisement's
105
+ * signature alone, and that any node calls with no realm key pinned. */
106
+ ownProcedure(name: string): string;
107
+ /** Every link the pool holds. */
108
+ status(): LinkStatus[];
109
+ /** Calls procedure in realm at a provider (any trusted one unless
110
+ * `provider` names one) by direct dial. A provider's ERROR is thrown as a
111
+ * ProviderError, a station's relay error as a RelayError. */
112
+ call(realm: Id, procedure: string, payload?: JsonValue, options?: {
113
+ provider?: Id;
114
+ timeoutMs?: number;
115
+ bytes?: BytesOutput;
116
+ }): Promise<JsonValue>;
117
+ /** The procedure's trusted providers, freshest first. */
118
+ providers(realm: Id, procedure: string, options?: {
119
+ timeoutMs?: number;
120
+ }): Promise<Provider[]>;
121
+ /** Publishes payload on topic in realm. Topics name a kind of fact; ids go
122
+ * in the payload. */
123
+ publish(realm: Id, topic: string, payload: JsonValue, options?: {
84
124
  ttlMs?: number;
85
125
  }): Promise<void>;
86
- /** Calls `procedure` against the pool's live control links in order
87
- * until one succeeds or all have been tried. Throws
88
- * NoHealthyStationError if zero links are live.
89
- *
90
- * `realm`/`payload`/`opts.bytes` are validated before any link is
91
- * touched, for the
92
- * same reason as publish() -- a malformed realm is a caller bug, not
93
- * evidence of a dead connection, and must never be attributed to one.
94
- *
95
- * A `MaculaCallError` (a real BOLT#4 response -- e.g.
96
- * unknown_next_peer, unauthorized, a procedure nobody serves, a gated
97
- * call this identity isn't authorized for) does NOT mark its link for
98
- * respawn: the connection plainly worked, it answered. call() still
99
- * falls through to the next link either way, matching the Erlang
100
- * reference's own keep_or_next (macula_client.erl) -- a non-idempotent
101
- * provider handler genuinely can be re-invoked on each live link this
102
- * reaches; that is parity with the reference, not a bug this fixes.
103
- *
104
- * Any OTHER thrown error (never a wire-level answer at all) is
105
- * ambiguous, not automatically a dead link: session.call()'s own
106
- * deadlineMs elapsing looks identical to a genuinely severed
107
- * connection, but means the far end is merely slow, not gone. Found
108
- * live 2026-09-05: treating every such error as a dead link meant one
109
- * slow provider response tore down and reconnected EVERY live control
110
- * link in turn as call() moved through them re-trying the same call.
111
- * #probeLiveness's own dedicated liveness call is the tiebreaker --
112
- * only a link that ALSO fails to get a wire-level answer on that
113
- * fresh probe is scheduled for reconnect. Each link's own `session`
114
- * reference is re-checked both before probing and before scheduling a
115
- * reconnect, in case a concurrent operation already superseded it. */
116
- call(realm: string | undefined, procedure: string, payload: JsonValue, opts?: {
126
+ /** Subscribes to topic in realm: onEvent hears each verified event once,
127
+ * however many links deliver it. `closed` resolves when the subscription
128
+ * ends, with why or null. */
129
+ subscribe(realm: Id, topic: string, onEvent: (event: Event) => void, options?: {
130
+ bytes?: BytesOutput;
131
+ }): Promise<Subscription>;
132
+ /** Serves procedure in realm: handler answers each call, and its thrown
133
+ * error goes back as a handler_error with its message. An org procedure
134
+ * needs the realm's key pinned and the org's delegation to this node in the
135
+ * DHT; a procedure in this node's own namespace (ownProcedure) needs
136
+ * neither, and another node's namespace is refused. */
137
+ serve(realm: Id, procedure: string, handler: (request: Request) => JsonValue | Promise<JsonValue>, options?: {
138
+ bytes?: BytesOutput;
139
+ }): Promise<Served>;
140
+ /** Serves procedure in realm as a stream of mode: handler drives each
141
+ * session. The stream is closed when the handler returns without ending
142
+ * it, aborted with code error when it throws, and released either way. */
143
+ serveStream(realm: Id, procedure: string, mode: StreamMode, handler: (stream: Stream, request: StreamRequest) => void | Promise<void>, options?: {
144
+ bytes?: BytesOutput;
145
+ }): Promise<Served>;
146
+ /** Opens a stream of mode on procedure in realm at a provider, by direct
147
+ * dial. A refusal arrives on its first recv(). */
148
+ openStream(realm: Id, procedure: string, mode: StreamMode, payload?: JsonValue, options?: {
149
+ provider?: Id;
117
150
  deadlineMs?: number;
151
+ timeoutMs?: number;
118
152
  bytes?: BytesOutput;
119
- }): Promise<JsonValue>;
120
- /** Subscribes `handler` to `(realm, topic)`: opens one subscribe-only
121
- * session against every configured seed, replayed automatically on
122
- * every future respawn. Mints a dedicated identity for this topic by
123
- * default (disposed on unsubscribe); pass `identity` to supply the
124
- * pool's own instead (e.g. for a stable, caller-controlled identity
125
- * across restarts, matching macula-mcp's own observeRoomIdentityPath
126
- * pattern) -- the pool never disposes an identity it didn't mint.
127
- * `opts.bytes` picks how bytes in each event's payload reach `handler`
128
- * (rpc.ts's BytesOutput), on every seed and after every respawn.
129
- * Returns an unsubscribe function. */
130
- subscribe(realm: string | undefined, topic: string, handler: (evt: PubsubEvent) => void, identity?: Identity, opts?: {
153
+ }): Promise<Stream>;
154
+ /** The verified record under key, or null when there is none. */
155
+ findRecord(key: Id, options?: {
156
+ timeoutMs?: number;
131
157
  bytes?: BytesOutput;
132
- }): Promise<() => Promise<void>>;
133
- /** Live/backing-off CONTROL link counts (publish/call reachability).
134
- * Every configured seed is exactly one or the other. Per-topic
135
- * subscription link health is not reflected here -- inspect a
136
- * specific subscription's own behavior (events arriving or not)
137
- * instead; exposing N independent per-topic health vectors here
138
- * would not simplify what a caller actually needs to know. */
139
- status(): PoolStatus;
140
- /** Closes every control and subscription link and disposes every
141
- * identity this pool owns (the caller-supplied control identity
142
- * included). Awaits each link's in-flight connect/reconnect first,
143
- * so a still-connecting link never has its identity yanked out from
144
- * under it. */
158
+ }): Promise<DhtRecord | null>;
159
+ /** Every verified record under key, and how many did not verify. */
160
+ findRecords(key: Id, options?: {
161
+ timeoutMs?: number;
162
+ bytes?: BytesOutput;
163
+ }): Promise<{
164
+ records: DhtRecord[];
165
+ dropped: number;
166
+ }>;
167
+ /** Every verified record of type the station holds, and how many did not
168
+ * verify. */
169
+ findRecordsByType(type: RecordType | number, options?: {
170
+ timeoutMs?: number;
171
+ bytes?: BytesOutput;
172
+ }): Promise<{
173
+ records: DhtRecord[];
174
+ dropped: number;
175
+ }>;
176
+ /** Shares data in realm: this node keeps it, serves it on its own
177
+ * `~<node_id>/content_v1` and announces it, renewing the announcement until
178
+ * unshareContent or close. Data of at most 256 KiB is one raw block; larger
179
+ * data a manifest over 256 KiB chunks, named name. Resolves to the content
180
+ * id as hex. Serving needs stations that admit a node's own namespace. */
181
+ shareContent(realm: Id, data: Uint8Array, name?: string, options?: {
182
+ timeoutMs?: number;
183
+ }): Promise<string>;
184
+ /** Stops sharing mcid in realm and withdraws its announcement. */
185
+ unshareContent(realm: Id, mcid: Mcid, options?: {
186
+ timeoutMs?: number;
187
+ }): Promise<void>;
188
+ /** Fetches the content mcid names in realm from a node that shares it,
189
+ * checked against mcid; no realm key is needed. Content nobody announces is
190
+ * a NotSharedError, content every sharer failed to give a
191
+ * ContentUnavailableError. */
192
+ getContent(realm: Id, mcid: Mcid, options?: ContentOptions): Promise<Uint8Array>;
193
+ /** Puts a signed record's wire bytes in the DHT. */
194
+ putRecord(wire: Uint8Array, options?: {
195
+ timeoutMs?: number;
196
+ }): Promise<void>;
197
+ /** Closes every link and subscription. */
145
198
  close(): Promise<void>;
199
+ private live;
146
200
  }