@holochain/client 0.19.0-dev.0 → 0.19.0-dev.2
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/README.md +3 -2
- package/lib/api/admin/types.d.ts +1 -0
- package/lib/api/app/types.d.ts +206 -1
- package/lib/api/app/types.js +47 -0
- package/lib/api/app/websocket.d.ts +86 -6
- package/lib/api/app/websocket.js +95 -32
- package/lib/api/client.d.ts +8 -5
- package/lib/api/client.js +129 -84
- package/lib/api/common.js +1 -0
- package/lib/hdk/action.d.ts +7 -0
- package/lib/tsdoc-metadata.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -179,8 +179,9 @@ You need `holochain` and `hc` on your path, best to get them from nix with `nix-
|
|
|
179
179
|
|
|
180
180
|
To perform the pre-requisite DNA compilation steps, and run the Nodejs test, run:
|
|
181
181
|
```bash
|
|
182
|
-
nix
|
|
183
|
-
./
|
|
182
|
+
nix develop
|
|
183
|
+
./build-fixture.sh
|
|
184
|
+
npm run test
|
|
184
185
|
```
|
|
185
186
|
|
|
186
187
|
## Contribute
|
package/lib/api/admin/types.d.ts
CHANGED
package/lib/api/app/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { UnsubscribeFunction } from "emittery";
|
|
2
|
-
import { AgentPubKey, AppAuthenticationToken, AppInfo, CapSecret, CellId, ClonedCell, DnaHash, DnaProperties, EntryHash, FunctionName, InstalledAppId, MembraneProof, MemproofMap, NetworkInfo, NetworkSeed, Nonce256Bit, RoleName, Timestamp, Transformer, WebsocketConnectionOptions, ZomeName } from "../../index.js";
|
|
2
|
+
import { AgentPubKey, AppAuthenticationToken, AppInfo, CapSecret, CellId, ClonedCell, DnaHash, DnaProperties, EntryHash, FunctionName, InstalledAppId, MembraneProof, MemproofMap, NetworkInfo, NetworkSeed, Nonce256Bit, RoleName, Timestamp, Transformer, WebsocketConnectionOptions, ZomeName, PreflightRequest, SignedAction, SignedActionHashed } from "../../index.js";
|
|
3
3
|
/**
|
|
4
4
|
* @public
|
|
5
5
|
*/
|
|
@@ -183,6 +183,211 @@ export interface NetworkInfoRequest {
|
|
|
183
183
|
*/
|
|
184
184
|
last_time_queried?: number;
|
|
185
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Cell id for which the countersigning session state is requested.
|
|
188
|
+
*
|
|
189
|
+
* @public
|
|
190
|
+
*/
|
|
191
|
+
export type GetCountersigningSessionStateRequest = CellId;
|
|
192
|
+
/**
|
|
193
|
+
* @public
|
|
194
|
+
*/
|
|
195
|
+
export type GetCountersigningSessionStateResponse = null | CountersigningSessionState;
|
|
196
|
+
/**
|
|
197
|
+
* Cell id for which the countersigning session should be abandoned.
|
|
198
|
+
*
|
|
199
|
+
* @public
|
|
200
|
+
*/
|
|
201
|
+
export type AbandonCountersigningSessionStateRequest = CellId;
|
|
202
|
+
/**
|
|
203
|
+
* @public
|
|
204
|
+
*/
|
|
205
|
+
export type AbandonCountersigningSessionStateResponse = null;
|
|
206
|
+
/**
|
|
207
|
+
* Cell id for which the countersigning session should be published.
|
|
208
|
+
*
|
|
209
|
+
* @public
|
|
210
|
+
*/
|
|
211
|
+
export type PublishCountersigningSessionStateRequest = CellId;
|
|
212
|
+
/**
|
|
213
|
+
* @public
|
|
214
|
+
*/
|
|
215
|
+
export type PublishCountersigningSessionStateResponse = null;
|
|
216
|
+
/**
|
|
217
|
+
* @public
|
|
218
|
+
*/
|
|
219
|
+
export declare enum CountersigningSessionStateType {
|
|
220
|
+
Accepted = "Accepted",
|
|
221
|
+
SignaturesCollected = "SignaturesCollected",
|
|
222
|
+
Unknown = "Unknown"
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* @public
|
|
226
|
+
*/
|
|
227
|
+
export type CountersigningSessionState =
|
|
228
|
+
/**
|
|
229
|
+
* This is the entry state. Accepting a countersigning session through the HDK will immediately
|
|
230
|
+
* register the countersigning session in this state, for management by the countersigning workflow.
|
|
231
|
+
*
|
|
232
|
+
* The session will stay in this state even when the agent commits their countersigning entry and only
|
|
233
|
+
* move to the next state when the first signature bundle is received.
|
|
234
|
+
*/
|
|
235
|
+
{
|
|
236
|
+
[CountersigningSessionStateType.Accepted]: PreflightRequest;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* This is the state where we have collected one or more signatures for a countersigning session.
|
|
240
|
+
*
|
|
241
|
+
* This state can be entered from the [CountersigningSessionState::Accepted] state, which happens
|
|
242
|
+
* when a witness returns a signature bundle to us. While the session has not timed out, we will
|
|
243
|
+
* stay in this state and wait until one of the signatures bundles we have received is valid for
|
|
244
|
+
* the session to be completed.
|
|
245
|
+
*
|
|
246
|
+
* If we entered this state from the [CountersigningSessionState::Accepted] state, we will either
|
|
247
|
+
* complete the session successfully or the session will time out. On a timeout we will move
|
|
248
|
+
* to the [CountersigningSessionState::Unknown] for a limited number of attempts to recover the session.
|
|
249
|
+
*
|
|
250
|
+
* This state can also be entered from the [CountersigningSessionState::Unknown] state, which happens when we
|
|
251
|
+
* have been able to recover the session from the source chain and have requested signed actions
|
|
252
|
+
* from agent authorities to build a signature bundle.
|
|
253
|
+
*
|
|
254
|
+
* If we entered this state from the [CountersigningSessionState::Unknown] state, we will either
|
|
255
|
+
* complete the session successfully, or if the signatures are invalid, we will return to the
|
|
256
|
+
* [CountersigningSessionState::Unknown] state.
|
|
257
|
+
*/
|
|
258
|
+
| {
|
|
259
|
+
[CountersigningSessionStateType.SignaturesCollected]: {
|
|
260
|
+
/** The preflight request that has been exchanged among countersigning peers. */
|
|
261
|
+
preflight_request: PreflightRequest;
|
|
262
|
+
/** Signed actions of the committed countersigned entries of all participating peers. */
|
|
263
|
+
signature_bundles: SignedAction[][];
|
|
264
|
+
/**
|
|
265
|
+
* This field is set when the signature bundle came from querying agent activity authorities
|
|
266
|
+
* in the unknown state. If we started from that state, we should return to it if the
|
|
267
|
+
* signature bundle is invalid. Otherwise, stay in this state and wait for more signatures.
|
|
268
|
+
*/
|
|
269
|
+
resolution?: SessionResolutionSummary;
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The session is in an unknown state and needs to be resolved.
|
|
274
|
+
*
|
|
275
|
+
* This state is used when we have lost track of the countersigning session. This happens if
|
|
276
|
+
* we have got far enough to create the countersigning entry but have crashed or restarted
|
|
277
|
+
* before we could complete the session. In this case we need to try to discover what the other
|
|
278
|
+
* agent or agents involved in the session have done.
|
|
279
|
+
*
|
|
280
|
+
* This state is also entered temporarily when we have published a signature and then the
|
|
281
|
+
* session has timed out. To avoid deadlocking with two parties both waiting for each other to
|
|
282
|
+
* proceed, we cannot stay in this state indefinitely. We will make a limited number of attempts
|
|
283
|
+
* to recover and if we cannot, we will abandon the session.
|
|
284
|
+
*
|
|
285
|
+
* The only exception to the attempt limiting is if we are unable to reach agent activity authorities
|
|
286
|
+
* to progress resolving the session. In this case, the attempts are not counted towards the
|
|
287
|
+
* configured limit. This does not protect us against a network partition where we can only see
|
|
288
|
+
* a subset of the network, but it does protect us against Holochain forcing a decision while
|
|
289
|
+
* it is unable to reach any peers.
|
|
290
|
+
*
|
|
291
|
+
* Note that because the [PreflightRequest] is stored here, we only ever enter the unknown state
|
|
292
|
+
* if we managed to keep the preflight request in memory, or if we have been able to recover it
|
|
293
|
+
* from the source chain as part of the committed [CounterSigningSessionData]. Otherwise, we
|
|
294
|
+
* are unable to discover what session we were participating in, and we must abandon the session
|
|
295
|
+
* without going through this recovery state.
|
|
296
|
+
*/
|
|
297
|
+
| {
|
|
298
|
+
[CountersigningSessionStateType.Unknown]: {
|
|
299
|
+
/** The preflight request that has been exchanged. */
|
|
300
|
+
preflight_request: PreflightRequest;
|
|
301
|
+
/** Summary of the attempts to resolve this session. */
|
|
302
|
+
resolution: SessionResolutionSummary;
|
|
303
|
+
/** Flag if the session is programmed to be force-abandoned on the next countersigning workflow run. */
|
|
304
|
+
force_abandon: boolean;
|
|
305
|
+
/** Flag if the session is programmed to be force-published on the next countersigning workflow run. */
|
|
306
|
+
force_publish: boolean;
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
/**
|
|
310
|
+
* Summary of the workflow's attempts to resolve the outcome a failed countersigning session.
|
|
311
|
+
* This tracks the numbers of attempts and the outcome of the most recent attempt.
|
|
312
|
+
*
|
|
313
|
+
* @public
|
|
314
|
+
*/
|
|
315
|
+
export interface SessionResolutionSummary {
|
|
316
|
+
/** The reason why session resolution is required. */
|
|
317
|
+
required_reason: ResolutionRequiredReason;
|
|
318
|
+
/**
|
|
319
|
+
* How many attempts have been made to resolve the session.
|
|
320
|
+
*
|
|
321
|
+
* Attempts are made according to the frequency specified by [RETRY_UNKNOWN_SESSION_STATE_DELAY].
|
|
322
|
+
*
|
|
323
|
+
* This count is only correct for the current run of the Holochain conductor. If the conductor
|
|
324
|
+
* is restarted then this counter is also reset.
|
|
325
|
+
*/
|
|
326
|
+
attempts: number;
|
|
327
|
+
/** The time of the last attempt to resolve the session. */
|
|
328
|
+
last_attempt_at?: Timestamp;
|
|
329
|
+
/** The outcome of the most recent attempt to resolve the session. */
|
|
330
|
+
outcomes: SessionResolutionOutcome[];
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* The reason why a countersigning session can not be resolved automatically and requires manual resolution.
|
|
334
|
+
*
|
|
335
|
+
* @public
|
|
336
|
+
*/
|
|
337
|
+
export declare enum ResolutionRequiredReason {
|
|
338
|
+
/** The session has timed out, so we should try to resolve its state before abandoning. */
|
|
339
|
+
Timeout = "Timeout",
|
|
340
|
+
/** Something happened, like a conductor restart, and we lost track of the session. */
|
|
341
|
+
Unknown = "Unknown"
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* The outcome for a single agent who participated in a countersigning session.
|
|
345
|
+
*
|
|
346
|
+
* [NUM_AUTHORITIES_TO_QUERY] authorities are made to agent activity authorities for each agent,
|
|
347
|
+
* and the decisions are collected into [SessionResolutionOutcome::decisions].
|
|
348
|
+
*
|
|
349
|
+
* @public
|
|
350
|
+
*/
|
|
351
|
+
export interface SessionResolutionOutcome {
|
|
352
|
+
/**
|
|
353
|
+
* The agent who participated in the countersigning session and is the subject of this
|
|
354
|
+
* resolution outcome.
|
|
355
|
+
*/
|
|
356
|
+
agent: AgentPubKey;
|
|
357
|
+
/** The resolved decision for each authority for the subject agent. */
|
|
358
|
+
decisions: SessionCompletionDecision[];
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Decision about an incomplete countersigning session.
|
|
362
|
+
*
|
|
363
|
+
* @public
|
|
364
|
+
*/
|
|
365
|
+
export declare enum SessionCompletionDecisionType {
|
|
366
|
+
/** Evidence found on the network that this session completed successfully. */
|
|
367
|
+
Complete = "Complete",
|
|
368
|
+
/**
|
|
369
|
+
* Evidence found on the network that this session was abandoned and other agents have
|
|
370
|
+
* added to their chain without completing the session.
|
|
371
|
+
*/
|
|
372
|
+
Abandoned = "Abandoned",
|
|
373
|
+
/**
|
|
374
|
+
* No evidence, or inconclusive evidence, was found on the network. Holochain will not make an
|
|
375
|
+
* automatic decision until the evidence is conclusive.
|
|
376
|
+
*/
|
|
377
|
+
Indeterminate = "Indeterminate",
|
|
378
|
+
/**There were errors encountered while trying to resolve the session. Errors such as network
|
|
379
|
+
* errors are treated differently to inconclusive evidence. We don't want to force a decision
|
|
380
|
+
* when we're offline, for example. In this case, the resolution must be retried later and this
|
|
381
|
+
* attempt should not be counted.
|
|
382
|
+
*/
|
|
383
|
+
Failed = "Failed"
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* @public
|
|
387
|
+
*/
|
|
388
|
+
export type SessionCompletionDecision = {
|
|
389
|
+
[SessionCompletionDecisionType.Complete]: SignedActionHashed;
|
|
390
|
+
} | SessionCompletionDecisionType.Abandoned | SessionCompletionDecisionType.Indeterminate | SessionCompletionDecisionType.Failed;
|
|
186
391
|
/**
|
|
187
392
|
* @public
|
|
188
393
|
*/
|
package/lib/api/app/types.js
CHANGED
|
@@ -1,3 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @public
|
|
3
|
+
*/
|
|
4
|
+
export var CountersigningSessionStateType;
|
|
5
|
+
(function (CountersigningSessionStateType) {
|
|
6
|
+
CountersigningSessionStateType["Accepted"] = "Accepted";
|
|
7
|
+
CountersigningSessionStateType["SignaturesCollected"] = "SignaturesCollected";
|
|
8
|
+
CountersigningSessionStateType["Unknown"] = "Unknown";
|
|
9
|
+
})(CountersigningSessionStateType || (CountersigningSessionStateType = {}));
|
|
10
|
+
/**
|
|
11
|
+
* The reason why a countersigning session can not be resolved automatically and requires manual resolution.
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
export var ResolutionRequiredReason;
|
|
16
|
+
(function (ResolutionRequiredReason) {
|
|
17
|
+
/** The session has timed out, so we should try to resolve its state before abandoning. */
|
|
18
|
+
ResolutionRequiredReason["Timeout"] = "Timeout";
|
|
19
|
+
/** Something happened, like a conductor restart, and we lost track of the session. */
|
|
20
|
+
ResolutionRequiredReason["Unknown"] = "Unknown";
|
|
21
|
+
})(ResolutionRequiredReason || (ResolutionRequiredReason = {}));
|
|
22
|
+
/**
|
|
23
|
+
* Decision about an incomplete countersigning session.
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
export var SessionCompletionDecisionType;
|
|
28
|
+
(function (SessionCompletionDecisionType) {
|
|
29
|
+
/** Evidence found on the network that this session completed successfully. */
|
|
30
|
+
SessionCompletionDecisionType["Complete"] = "Complete";
|
|
31
|
+
/**
|
|
32
|
+
* Evidence found on the network that this session was abandoned and other agents have
|
|
33
|
+
* added to their chain without completing the session.
|
|
34
|
+
*/
|
|
35
|
+
SessionCompletionDecisionType["Abandoned"] = "Abandoned";
|
|
36
|
+
/**
|
|
37
|
+
* No evidence, or inconclusive evidence, was found on the network. Holochain will not make an
|
|
38
|
+
* automatic decision until the evidence is conclusive.
|
|
39
|
+
*/
|
|
40
|
+
SessionCompletionDecisionType["Indeterminate"] = "Indeterminate";
|
|
41
|
+
/**There were errors encountered while trying to resolve the session. Errors such as network
|
|
42
|
+
* errors are treated differently to inconclusive evidence. We don't want to force a decision
|
|
43
|
+
* when we're offline, for example. In this case, the resolution must be retried later and this
|
|
44
|
+
* attempt should not be counted.
|
|
45
|
+
*/
|
|
46
|
+
SessionCompletionDecisionType["Failed"] = "Failed";
|
|
47
|
+
})(SessionCompletionDecisionType || (SessionCompletionDecisionType = {}));
|
|
1
48
|
/**
|
|
2
49
|
* @public
|
|
3
50
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { UnsubscribeFunction } from "emittery";
|
|
2
2
|
import { AgentPubKey, CellId, InstalledAppId, RoleName } from "../../types.js";
|
|
3
3
|
import { AppInfo, MemproofMap } from "../admin/index.js";
|
|
4
|
-
import { AppCallZomeRequest, AppClient, AppEvents, AppNetworkInfoRequest, AppCreateCloneCellRequest, AppDisableCloneCellRequest, AppEnableCloneCellRequest, SignalCb, CallZomeRequest, CallZomeRequestSigned,
|
|
4
|
+
import { AppCallZomeRequest, AppClient, AppEvents, AppNetworkInfoRequest, AppCreateCloneCellRequest, AppDisableCloneCellRequest, AppEnableCloneCellRequest, SignalCb, CallZomeRequest, CallZomeRequestSigned, NetworkInfoResponse, AppWebsocketConnectionOptions, GetCountersigningSessionStateRequest, GetCountersigningSessionStateResponse, AbandonCountersigningSessionStateRequest, PublishCountersigningSessionStateRequest } from "./types.js";
|
|
5
5
|
import { WsClient } from "../client.js";
|
|
6
6
|
/**
|
|
7
7
|
* A class to establish a websocket connection to an App interface, for a
|
|
@@ -25,6 +25,9 @@ export declare class AppWebsocket implements AppClient {
|
|
|
25
25
|
private readonly enableCloneCellRequester;
|
|
26
26
|
private readonly disableCloneCellRequester;
|
|
27
27
|
private readonly networkInfoRequester;
|
|
28
|
+
private readonly getCountersigningSessionStateRequester;
|
|
29
|
+
private readonly abandonCountersigningSessionRequester;
|
|
30
|
+
private readonly publishCountersigningSessionRequester;
|
|
28
31
|
private constructor();
|
|
29
32
|
/**
|
|
30
33
|
* Instance factory for creating an {@link AppWebsocket}.
|
|
@@ -67,33 +70,111 @@ export declare class AppWebsocket implements AppClient {
|
|
|
67
70
|
* @param timeout - A timeout to override the default.
|
|
68
71
|
* @returns The zome call's response.
|
|
69
72
|
*/
|
|
70
|
-
callZome(request: AppCallZomeRequest, timeout?: number): Promise<
|
|
73
|
+
callZome<ReturnType>(request: AppCallZomeRequest, timeout?: number): Promise<ReturnType>;
|
|
71
74
|
/**
|
|
72
75
|
* Clone an existing provisioned cell.
|
|
73
76
|
*
|
|
74
77
|
* @param args - Specify the cell to clone.
|
|
75
78
|
* @returns The created clone cell.
|
|
76
79
|
*/
|
|
77
|
-
createCloneCell(args: AppCreateCloneCellRequest): Promise<
|
|
80
|
+
createCloneCell(args: AppCreateCloneCellRequest): Promise<import("../admin/types.js").ClonedCell>;
|
|
78
81
|
/**
|
|
79
82
|
* Enable a disabled clone cell.
|
|
80
83
|
*
|
|
81
84
|
* @param args - Specify the clone cell to enable.
|
|
82
85
|
* @returns The enabled clone cell.
|
|
83
86
|
*/
|
|
84
|
-
enableCloneCell(args: AppEnableCloneCellRequest): Promise<
|
|
87
|
+
enableCloneCell(args: AppEnableCloneCellRequest): Promise<import("../admin/types.js").ClonedCell>;
|
|
85
88
|
/**
|
|
86
89
|
* Disable an enabled clone cell.
|
|
87
90
|
*
|
|
88
91
|
* @param args - Specify the clone cell to disable.
|
|
89
92
|
*/
|
|
90
|
-
disableCloneCell(args: AppDisableCloneCellRequest): Promise<
|
|
93
|
+
disableCloneCell(args: AppDisableCloneCellRequest): Promise<void>;
|
|
91
94
|
/**
|
|
92
95
|
* Request network info about gossip status.
|
|
93
96
|
* @param args - Specify the DNAs for which you want network info
|
|
94
97
|
* @returns Network info for the specified DNAs
|
|
95
98
|
*/
|
|
96
99
|
networkInfo(args: AppNetworkInfoRequest): Promise<NetworkInfoResponse>;
|
|
100
|
+
/**
|
|
101
|
+
* Get the state of a countersigning session.
|
|
102
|
+
*/
|
|
103
|
+
getCountersigningSessionState(args: GetCountersigningSessionStateRequest): Promise<GetCountersigningSessionStateResponse>;
|
|
104
|
+
/**
|
|
105
|
+
* Abandon an unresolved countersigning session.
|
|
106
|
+
*
|
|
107
|
+
* If the current session has not been resolved automatically, it can be forcefully abandoned.
|
|
108
|
+
* A condition for this call to succeed is that at least one attempt has been made to resolve
|
|
109
|
+
* it automatically.
|
|
110
|
+
*
|
|
111
|
+
* # Returns
|
|
112
|
+
*
|
|
113
|
+
* [`AppResponse::CountersigningSessionAbandoned`]
|
|
114
|
+
*
|
|
115
|
+
* The session is marked for abandoning and the countersigning workflow was triggered. The session
|
|
116
|
+
* has not been abandoned yet.
|
|
117
|
+
*
|
|
118
|
+
* Upon successful abandoning the system signal [`SystemSignal::AbandonedCountersigning`] will
|
|
119
|
+
* be emitted and the session removed from state, so that [`AppRequest::GetCountersigningSessionState`]
|
|
120
|
+
* would return `None`.
|
|
121
|
+
*
|
|
122
|
+
* In the countersigning workflow it will first be attempted to resolve the session with incoming
|
|
123
|
+
* signatures of the countersigned entries, before force-abandoning the session. In a very rare event
|
|
124
|
+
* it could happen that in just the moment where the [`AppRequest::AbandonCountersigningSession`]
|
|
125
|
+
* is made, signatures for this session come in. If they are valid, the session will be resolved and
|
|
126
|
+
* published as usual. Should they be invalid, however, the flag to abandon the session is erased.
|
|
127
|
+
* In such cases this request can be retried until the session has been abandoned successfully.
|
|
128
|
+
*
|
|
129
|
+
* # Errors
|
|
130
|
+
*
|
|
131
|
+
* [`CountersigningError::WorkspaceDoesNotExist`] likely indicates that an invalid cell id was
|
|
132
|
+
* passed in to the call.
|
|
133
|
+
*
|
|
134
|
+
* [`CountersigningError::SessionNotFound`] when no ongoing session could be found for the provided
|
|
135
|
+
* cell id.
|
|
136
|
+
*
|
|
137
|
+
* [`CountersigningError::SessionNotUnresolved`] when an attempt to resolve the session
|
|
138
|
+
* automatically has not been made.
|
|
139
|
+
*/
|
|
140
|
+
abandonCountersigningSession(args: AbandonCountersigningSessionStateRequest): Promise<null>;
|
|
141
|
+
/**
|
|
142
|
+
* Publish an unresolved countersigning session.
|
|
143
|
+
*
|
|
144
|
+
* If the current session has not been resolved automatically, it can be forcefully published.
|
|
145
|
+
* A condition for this call to succeed is that at least one attempt has been made to resolve
|
|
146
|
+
* it automatically.
|
|
147
|
+
*
|
|
148
|
+
* # Returns
|
|
149
|
+
*
|
|
150
|
+
* [`AppResponse::PublishCountersigningSessionTriggered`]
|
|
151
|
+
*
|
|
152
|
+
* The session is marked for publishing and the countersigning workflow was triggered. The session
|
|
153
|
+
* has not been published yet.
|
|
154
|
+
*
|
|
155
|
+
* Upon successful publishing the system signal [`SystemSignal::SuccessfulCountersigning`] will
|
|
156
|
+
* be emitted and the session removed from state, so that [`AppRequest::GetCountersigningSessionState`]
|
|
157
|
+
* would return `None`.
|
|
158
|
+
*
|
|
159
|
+
* In the countersigning workflow it will first be attempted to resolve the session with incoming
|
|
160
|
+
* signatures of the countersigned entries, before force-publishing the session. In a very rare event
|
|
161
|
+
* it could happen that in just the moment where the [`AppRequest::PublishCountersigningSession`]
|
|
162
|
+
* is made, signatures for this session come in. If they are valid, the session will be resolved and
|
|
163
|
+
* published as usual. Should they be invalid, however, the flag to publish the session is erased.
|
|
164
|
+
* In such cases this request can be retried until the session has been published successfully.
|
|
165
|
+
*
|
|
166
|
+
* # Errors
|
|
167
|
+
*
|
|
168
|
+
* [`CountersigningError::WorkspaceDoesNotExist`] likely indicates that an invalid cell id was
|
|
169
|
+
* passed in to the call.
|
|
170
|
+
*
|
|
171
|
+
* [`CountersigningError::SessionNotFound`] when no ongoing session could be found for the provided
|
|
172
|
+
* cell id.
|
|
173
|
+
*
|
|
174
|
+
* [`CountersigningError::SessionNotUnresolved`] when an attempt to resolve the session
|
|
175
|
+
* automatically has not been made.
|
|
176
|
+
*/
|
|
177
|
+
publishCountersigningSession(args: PublishCountersigningSessionStateRequest): Promise<null>;
|
|
97
178
|
/**
|
|
98
179
|
* Register an event listener for signals.
|
|
99
180
|
*
|
|
@@ -103,7 +184,6 @@ export declare class AppWebsocket implements AppClient {
|
|
|
103
184
|
*/
|
|
104
185
|
on<Name extends keyof AppEvents>(eventName: Name | readonly Name[], listener: SignalCb): UnsubscribeFunction;
|
|
105
186
|
private static requester;
|
|
106
|
-
private containsCell;
|
|
107
187
|
}
|
|
108
188
|
/**
|
|
109
189
|
* @public
|
package/lib/api/app/websocket.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Emittery from "emittery";
|
|
2
2
|
import { omit } from "lodash-es";
|
|
3
|
-
import { CellType } from "../admin/index.js";
|
|
3
|
+
import { CellType, } from "../admin/index.js";
|
|
4
4
|
import { catchError, DEFAULT_TIMEOUT, getBaseRoleNameFromCloneId, HolochainError, isCloneId, promiseTimeout, requesterTransformer, } from "../common.js";
|
|
5
5
|
import { getHostZomeCallSigner, getLauncherEnvironment, } from "../../environments/launcher.js";
|
|
6
6
|
import { decode, encode } from "@msgpack/msgpack";
|
|
@@ -31,6 +31,9 @@ export class AppWebsocket {
|
|
|
31
31
|
enableCloneCellRequester;
|
|
32
32
|
disableCloneCellRequester;
|
|
33
33
|
networkInfoRequester;
|
|
34
|
+
getCountersigningSessionStateRequester;
|
|
35
|
+
abandonCountersigningSessionRequester;
|
|
36
|
+
publishCountersigningSessionRequester;
|
|
34
37
|
constructor(client, appInfo, callZomeTransform, defaultTimeout) {
|
|
35
38
|
this.client = client;
|
|
36
39
|
this.myPubKey = appInfo.agent_pub_key;
|
|
@@ -47,6 +50,9 @@ export class AppWebsocket {
|
|
|
47
50
|
this.enableCloneCellRequester = AppWebsocket.requester(this.client, "enable_clone_cell", this.defaultTimeout);
|
|
48
51
|
this.disableCloneCellRequester = AppWebsocket.requester(this.client, "disable_clone_cell", this.defaultTimeout);
|
|
49
52
|
this.networkInfoRequester = AppWebsocket.requester(this.client, "network_info", this.defaultTimeout);
|
|
53
|
+
this.getCountersigningSessionStateRequester = AppWebsocket.requester(this.client, "get_countersigning_session_state", this.defaultTimeout);
|
|
54
|
+
this.abandonCountersigningSessionRequester = AppWebsocket.requester(this.client, "abandon_countersigning_session", this.defaultTimeout);
|
|
55
|
+
this.publishCountersigningSessionRequester = AppWebsocket.requester(this.client, "publish_countersigning_session", this.defaultTimeout);
|
|
50
56
|
// Ensure all super methods are bound to this instance because Emittery relies on `this` being the instance.
|
|
51
57
|
// Please retain until the upstream is fixed https://github.com/sindresorhus/emittery/issues/86.
|
|
52
58
|
Object.getOwnPropertyNames(Emittery.prototype).forEach((name) => {
|
|
@@ -77,16 +83,10 @@ export class AppWebsocket {
|
|
|
77
83
|
throw new HolochainError("ConnectionUrlMissing", `unable to connect to Conductor API - no url provided and not in a launcher environment.`);
|
|
78
84
|
}
|
|
79
85
|
const client = await WsClient.connect(options.url, options.wsClientOptions);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
if (!options.token) {
|
|
86
|
-
throw new HolochainError("AppAuthenticationTokenMissing", `unable to connect to Conductor API - no app authentication token provided.`);
|
|
87
|
-
}
|
|
88
|
-
await client.authenticate({ token: options.token });
|
|
89
|
-
}
|
|
86
|
+
const token = options.token ?? env?.APP_INTERFACE_TOKEN;
|
|
87
|
+
if (!token)
|
|
88
|
+
throw new HolochainError("AppAuthenticationTokenMissing", `unable to connect to Conductor API - no app authentication token provided.`);
|
|
89
|
+
await client.authenticate({ token });
|
|
90
90
|
const appInfo = await AppWebsocket.requester(client, "app_info", DEFAULT_TIMEOUT)(null);
|
|
91
91
|
if (!appInfo) {
|
|
92
92
|
throw new HolochainError("AppNotFound", `The app your connection token was issued for was not found. The app needs to be installed and enabled.`);
|
|
@@ -224,6 +224,90 @@ export class AppWebsocket {
|
|
|
224
224
|
agent_pub_key: this.myPubKey,
|
|
225
225
|
});
|
|
226
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Get the state of a countersigning session.
|
|
229
|
+
*/
|
|
230
|
+
async getCountersigningSessionState(args) {
|
|
231
|
+
return this.getCountersigningSessionStateRequester(args);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Abandon an unresolved countersigning session.
|
|
235
|
+
*
|
|
236
|
+
* If the current session has not been resolved automatically, it can be forcefully abandoned.
|
|
237
|
+
* A condition for this call to succeed is that at least one attempt has been made to resolve
|
|
238
|
+
* it automatically.
|
|
239
|
+
*
|
|
240
|
+
* # Returns
|
|
241
|
+
*
|
|
242
|
+
* [`AppResponse::CountersigningSessionAbandoned`]
|
|
243
|
+
*
|
|
244
|
+
* The session is marked for abandoning and the countersigning workflow was triggered. The session
|
|
245
|
+
* has not been abandoned yet.
|
|
246
|
+
*
|
|
247
|
+
* Upon successful abandoning the system signal [`SystemSignal::AbandonedCountersigning`] will
|
|
248
|
+
* be emitted and the session removed from state, so that [`AppRequest::GetCountersigningSessionState`]
|
|
249
|
+
* would return `None`.
|
|
250
|
+
*
|
|
251
|
+
* In the countersigning workflow it will first be attempted to resolve the session with incoming
|
|
252
|
+
* signatures of the countersigned entries, before force-abandoning the session. In a very rare event
|
|
253
|
+
* it could happen that in just the moment where the [`AppRequest::AbandonCountersigningSession`]
|
|
254
|
+
* is made, signatures for this session come in. If they are valid, the session will be resolved and
|
|
255
|
+
* published as usual. Should they be invalid, however, the flag to abandon the session is erased.
|
|
256
|
+
* In such cases this request can be retried until the session has been abandoned successfully.
|
|
257
|
+
*
|
|
258
|
+
* # Errors
|
|
259
|
+
*
|
|
260
|
+
* [`CountersigningError::WorkspaceDoesNotExist`] likely indicates that an invalid cell id was
|
|
261
|
+
* passed in to the call.
|
|
262
|
+
*
|
|
263
|
+
* [`CountersigningError::SessionNotFound`] when no ongoing session could be found for the provided
|
|
264
|
+
* cell id.
|
|
265
|
+
*
|
|
266
|
+
* [`CountersigningError::SessionNotUnresolved`] when an attempt to resolve the session
|
|
267
|
+
* automatically has not been made.
|
|
268
|
+
*/
|
|
269
|
+
async abandonCountersigningSession(args) {
|
|
270
|
+
return this.abandonCountersigningSessionRequester(args);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Publish an unresolved countersigning session.
|
|
274
|
+
*
|
|
275
|
+
* If the current session has not been resolved automatically, it can be forcefully published.
|
|
276
|
+
* A condition for this call to succeed is that at least one attempt has been made to resolve
|
|
277
|
+
* it automatically.
|
|
278
|
+
*
|
|
279
|
+
* # Returns
|
|
280
|
+
*
|
|
281
|
+
* [`AppResponse::PublishCountersigningSessionTriggered`]
|
|
282
|
+
*
|
|
283
|
+
* The session is marked for publishing and the countersigning workflow was triggered. The session
|
|
284
|
+
* has not been published yet.
|
|
285
|
+
*
|
|
286
|
+
* Upon successful publishing the system signal [`SystemSignal::SuccessfulCountersigning`] will
|
|
287
|
+
* be emitted and the session removed from state, so that [`AppRequest::GetCountersigningSessionState`]
|
|
288
|
+
* would return `None`.
|
|
289
|
+
*
|
|
290
|
+
* In the countersigning workflow it will first be attempted to resolve the session with incoming
|
|
291
|
+
* signatures of the countersigned entries, before force-publishing the session. In a very rare event
|
|
292
|
+
* it could happen that in just the moment where the [`AppRequest::PublishCountersigningSession`]
|
|
293
|
+
* is made, signatures for this session come in. If they are valid, the session will be resolved and
|
|
294
|
+
* published as usual. Should they be invalid, however, the flag to publish the session is erased.
|
|
295
|
+
* In such cases this request can be retried until the session has been published successfully.
|
|
296
|
+
*
|
|
297
|
+
* # Errors
|
|
298
|
+
*
|
|
299
|
+
* [`CountersigningError::WorkspaceDoesNotExist`] likely indicates that an invalid cell id was
|
|
300
|
+
* passed in to the call.
|
|
301
|
+
*
|
|
302
|
+
* [`CountersigningError::SessionNotFound`] when no ongoing session could be found for the provided
|
|
303
|
+
* cell id.
|
|
304
|
+
*
|
|
305
|
+
* [`CountersigningError::SessionNotUnresolved`] when an attempt to resolve the session
|
|
306
|
+
* automatically has not been made.
|
|
307
|
+
*/
|
|
308
|
+
async publishCountersigningSession(args) {
|
|
309
|
+
return this.publishCountersigningSessionRequester(args);
|
|
310
|
+
}
|
|
227
311
|
/**
|
|
228
312
|
* Register an event listener for signals.
|
|
229
313
|
*
|
|
@@ -237,25 +321,6 @@ export class AppWebsocket {
|
|
|
237
321
|
static requester(client, tag, defaultTimeout, transformer) {
|
|
238
322
|
return requesterTransformer((req, timeout) => promiseTimeout(client.request(req), tag, timeout || defaultTimeout).then(catchError), tag, transformer);
|
|
239
323
|
}
|
|
240
|
-
containsCell(cellId) {
|
|
241
|
-
const appInfo = this.cachedAppInfo;
|
|
242
|
-
if (!appInfo) {
|
|
243
|
-
return false;
|
|
244
|
-
}
|
|
245
|
-
for (const roleName of Object.keys(appInfo.cell_info)) {
|
|
246
|
-
for (const cellInfo of appInfo.cell_info[roleName]) {
|
|
247
|
-
const currentCellId = CellType.Provisioned in cellInfo
|
|
248
|
-
? cellInfo[CellType.Provisioned].cell_id
|
|
249
|
-
: CellType.Cloned in cellInfo
|
|
250
|
-
? cellInfo[CellType.Cloned].cell_id
|
|
251
|
-
: undefined;
|
|
252
|
-
if (currentCellId && isSameCell(currentCellId, cellId)) {
|
|
253
|
-
return true;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
324
|
}
|
|
260
325
|
const defaultCallZomeTransform = {
|
|
261
326
|
input: async (request) => {
|
|
@@ -272,8 +337,6 @@ const defaultCallZomeTransform = {
|
|
|
272
337
|
},
|
|
273
338
|
output: (response) => decode(response),
|
|
274
339
|
};
|
|
275
|
-
const isSameCell = (cellId1, cellId2) => cellId1[0].every((byte, index) => byte === cellId2[0][index]) &&
|
|
276
|
-
cellId1[1].every((byte, index) => byte === cellId2[1][index]);
|
|
277
340
|
/**
|
|
278
341
|
* @public
|
|
279
342
|
*/
|
package/lib/api/client.d.ts
CHANGED
|
@@ -23,8 +23,8 @@ export declare class WsClient extends Emittery {
|
|
|
23
23
|
options: WsClientOptions;
|
|
24
24
|
private pendingRequests;
|
|
25
25
|
private index;
|
|
26
|
+
private authenticationToken;
|
|
26
27
|
constructor(socket: IsoWebSocket, url?: URL, options?: WsClientOptions);
|
|
27
|
-
private setupSocket;
|
|
28
28
|
/**
|
|
29
29
|
* Instance factory for creating WsClients.
|
|
30
30
|
*
|
|
@@ -47,6 +47,10 @@ export declare class WsClient extends Emittery {
|
|
|
47
47
|
* @param request - The authentication request, containing an app authentication token.
|
|
48
48
|
*/
|
|
49
49
|
authenticate(request: AppAuthenticationRequest): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Close the websocket connection.
|
|
52
|
+
*/
|
|
53
|
+
close(code?: number): Promise<IsoWebSocket.CloseEvent>;
|
|
50
54
|
/**
|
|
51
55
|
* Send requests to the connected websocket.
|
|
52
56
|
*
|
|
@@ -56,10 +60,9 @@ export declare class WsClient extends Emittery {
|
|
|
56
60
|
request<Response>(request: unknown): Promise<Response>;
|
|
57
61
|
private exchange;
|
|
58
62
|
private sendMessage;
|
|
63
|
+
private registerMessageListener;
|
|
64
|
+
private registerCloseListener;
|
|
65
|
+
private reconnectWebsocket;
|
|
59
66
|
private handleResponse;
|
|
60
|
-
/**
|
|
61
|
-
* Close the websocket connection.
|
|
62
|
-
*/
|
|
63
|
-
close(code?: number): Promise<CloseEvent>;
|
|
64
67
|
}
|
|
65
68
|
export { IsoWebSocket };
|
package/lib/api/client.js
CHANGED
|
@@ -17,74 +17,16 @@ export class WsClient extends Emittery {
|
|
|
17
17
|
options;
|
|
18
18
|
pendingRequests;
|
|
19
19
|
index;
|
|
20
|
+
authenticationToken;
|
|
20
21
|
constructor(socket, url, options) {
|
|
21
22
|
super();
|
|
23
|
+
this.registerMessageListener(socket);
|
|
24
|
+
this.registerCloseListener(socket);
|
|
22
25
|
this.socket = socket;
|
|
23
26
|
this.url = url;
|
|
24
27
|
this.options = options || {};
|
|
25
28
|
this.pendingRequests = {};
|
|
26
29
|
this.index = 0;
|
|
27
|
-
this.setupSocket();
|
|
28
|
-
}
|
|
29
|
-
setupSocket() {
|
|
30
|
-
this.socket.onmessage = async (serializedMessage) => {
|
|
31
|
-
// If data is not a buffer (nodejs), it will be a blob (browser)
|
|
32
|
-
let deserializedData;
|
|
33
|
-
if (globalThis.window &&
|
|
34
|
-
serializedMessage.data instanceof globalThis.window.Blob) {
|
|
35
|
-
deserializedData = await serializedMessage.data.arrayBuffer();
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
if (typeof Buffer !== "undefined" &&
|
|
39
|
-
Buffer.isBuffer(serializedMessage.data)) {
|
|
40
|
-
deserializedData = serializedMessage.data;
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
throw new HolochainError("UnknownMessageFormat", `incoming message has unknown message format - ${deserializedData}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
const message = decode(deserializedData);
|
|
47
|
-
assertHolochainMessage(message);
|
|
48
|
-
if (message.type === "signal") {
|
|
49
|
-
if (message.data === null) {
|
|
50
|
-
throw new HolochainError("UnknownSignalFormat", "incoming signal has no data");
|
|
51
|
-
}
|
|
52
|
-
const deserializedSignal = decode(message.data);
|
|
53
|
-
assertHolochainSignal(deserializedSignal);
|
|
54
|
-
if (SignalType.System in deserializedSignal) {
|
|
55
|
-
this.emit("signal", {
|
|
56
|
-
System: deserializedSignal[SignalType.System],
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
const encodedAppSignal = deserializedSignal[SignalType.App];
|
|
61
|
-
// In order to return readable content to the UI, the signal payload must also be deserialized.
|
|
62
|
-
const payload = decode(encodedAppSignal.signal);
|
|
63
|
-
const signal = {
|
|
64
|
-
cell_id: encodedAppSignal.cell_id,
|
|
65
|
-
zome_name: encodedAppSignal.zome_name,
|
|
66
|
-
payload,
|
|
67
|
-
};
|
|
68
|
-
this.emit("signal", { App: signal });
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
else if (message.type === "response") {
|
|
72
|
-
this.handleResponse(message);
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
throw new HolochainError("UnknownMessageType", `incoming message has unknown type - ${message.type}`);
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
this.socket.onclose = (event) => {
|
|
79
|
-
const pendingRequestIds = Object.keys(this.pendingRequests).map((id) => parseInt(id));
|
|
80
|
-
if (pendingRequestIds.length) {
|
|
81
|
-
pendingRequestIds.forEach((id) => {
|
|
82
|
-
const error = new HolochainError("ClientClosedWithPendingRequests", `client closed with pending requests - close event code: ${event.code}, request id: ${id}`);
|
|
83
|
-
this.pendingRequests[id].reject(error);
|
|
84
|
-
delete this.pendingRequests[id];
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
30
|
}
|
|
89
31
|
/**
|
|
90
32
|
* Instance factory for creating WsClients.
|
|
@@ -96,13 +38,13 @@ export class WsClient extends Emittery {
|
|
|
96
38
|
static connect(url, options) {
|
|
97
39
|
return new Promise((resolve, reject) => {
|
|
98
40
|
const socket = new IsoWebSocket(url, options);
|
|
99
|
-
socket.
|
|
41
|
+
socket.addEventListener("error", (errorEvent) => {
|
|
100
42
|
reject(new HolochainError("ConnectionError", `could not connect to Holochain Conductor API at ${url} - ${errorEvent.error}`));
|
|
101
|
-
};
|
|
102
|
-
socket.
|
|
43
|
+
});
|
|
44
|
+
socket.addEventListener("open", (_) => {
|
|
103
45
|
const client = new WsClient(socket, url, options);
|
|
104
46
|
resolve(client);
|
|
105
|
-
};
|
|
47
|
+
}, { once: true });
|
|
106
48
|
});
|
|
107
49
|
}
|
|
108
50
|
/**
|
|
@@ -125,16 +67,35 @@ export class WsClient extends Emittery {
|
|
|
125
67
|
* @param request - The authentication request, containing an app authentication token.
|
|
126
68
|
*/
|
|
127
69
|
async authenticate(request) {
|
|
128
|
-
|
|
70
|
+
this.authenticationToken = request.token;
|
|
71
|
+
return this.exchange(request, (request, resolve, reject) => {
|
|
72
|
+
const invalidTokenCloseListener = (closeEvent) => {
|
|
73
|
+
this.authenticationToken = undefined;
|
|
74
|
+
reject(new HolochainError("InvalidTokenError", `could not connect to ${this.url} due to an invalid app authentication token - close code ${closeEvent.code}`));
|
|
75
|
+
};
|
|
76
|
+
this.socket.addEventListener("close", invalidTokenCloseListener, {
|
|
77
|
+
once: true,
|
|
78
|
+
});
|
|
129
79
|
const encodedMsg = encode({
|
|
130
80
|
type: "authenticate",
|
|
131
81
|
data: encode(request),
|
|
132
82
|
});
|
|
133
83
|
this.socket.send(encodedMsg);
|
|
134
|
-
//
|
|
135
|
-
|
|
84
|
+
// Wait before resolving in case authentication fails.
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
this.socket.removeEventListener("close", invalidTokenCloseListener);
|
|
87
|
+
resolve(null);
|
|
88
|
+
}, 10);
|
|
136
89
|
});
|
|
137
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Close the websocket connection.
|
|
93
|
+
*/
|
|
94
|
+
close(code = 1000) {
|
|
95
|
+
const closedPromise = new Promise((resolve) => this.socket.addEventListener("close", (closeEvent) => resolve(closeEvent), { once: true }));
|
|
96
|
+
this.socket.close(code);
|
|
97
|
+
return closedPromise;
|
|
98
|
+
}
|
|
138
99
|
/**
|
|
139
100
|
* Send requests to the connected websocket.
|
|
140
101
|
*
|
|
@@ -144,15 +105,22 @@ export class WsClient extends Emittery {
|
|
|
144
105
|
async request(request) {
|
|
145
106
|
return this.exchange(request, this.sendMessage.bind(this));
|
|
146
107
|
}
|
|
147
|
-
exchange(request, sendHandler) {
|
|
108
|
+
async exchange(request, sendHandler) {
|
|
148
109
|
if (this.socket.readyState === this.socket.OPEN) {
|
|
149
110
|
const promise = new Promise((resolve, reject) => {
|
|
150
111
|
sendHandler(request, resolve, reject);
|
|
151
112
|
});
|
|
152
113
|
return promise;
|
|
153
114
|
}
|
|
115
|
+
else if (this.url && this.authenticationToken) {
|
|
116
|
+
await this.reconnectWebsocket(this.url, this.authenticationToken);
|
|
117
|
+
this.registerMessageListener(this.socket);
|
|
118
|
+
this.registerCloseListener(this.socket);
|
|
119
|
+
const promise = new Promise((resolve, reject) => sendHandler(request, resolve, reject));
|
|
120
|
+
return promise;
|
|
121
|
+
}
|
|
154
122
|
else {
|
|
155
|
-
return Promise.reject(new
|
|
123
|
+
return Promise.reject(new HolochainError("WebsocketClosedError", "Websocket is not open"));
|
|
156
124
|
}
|
|
157
125
|
}
|
|
158
126
|
sendMessage(request, resolve, reject) {
|
|
@@ -166,6 +134,97 @@ export class WsClient extends Emittery {
|
|
|
166
134
|
this.pendingRequests[id] = { resolve, reject };
|
|
167
135
|
this.index += 1;
|
|
168
136
|
}
|
|
137
|
+
registerMessageListener(socket) {
|
|
138
|
+
socket.onmessage = async (serializedMessage) => {
|
|
139
|
+
// If data is not a buffer (nodejs), it will be a blob (browser)
|
|
140
|
+
let deserializedData;
|
|
141
|
+
if (globalThis.window &&
|
|
142
|
+
serializedMessage.data instanceof globalThis.window.Blob) {
|
|
143
|
+
deserializedData = await serializedMessage.data.arrayBuffer();
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
if (typeof Buffer !== "undefined" &&
|
|
147
|
+
Buffer.isBuffer(serializedMessage.data)) {
|
|
148
|
+
deserializedData = serializedMessage.data;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
throw new HolochainError("UnknownMessageFormat", `incoming message has unknown message format - ${deserializedData}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const message = decode(deserializedData);
|
|
155
|
+
assertHolochainMessage(message);
|
|
156
|
+
if (message.type === "signal") {
|
|
157
|
+
if (message.data === null) {
|
|
158
|
+
throw new HolochainError("UnknownSignalFormat", "incoming signal has no data");
|
|
159
|
+
}
|
|
160
|
+
const deserializedSignal = decode(message.data);
|
|
161
|
+
assertHolochainSignal(deserializedSignal);
|
|
162
|
+
if (SignalType.System in deserializedSignal) {
|
|
163
|
+
this.emit("signal", {
|
|
164
|
+
System: deserializedSignal[SignalType.System],
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const encodedAppSignal = deserializedSignal[SignalType.App];
|
|
169
|
+
// In order to return readable content to the UI, the signal payload must also be deserialized.
|
|
170
|
+
const payload = decode(encodedAppSignal.signal);
|
|
171
|
+
const signal = {
|
|
172
|
+
cell_id: encodedAppSignal.cell_id,
|
|
173
|
+
zome_name: encodedAppSignal.zome_name,
|
|
174
|
+
payload,
|
|
175
|
+
};
|
|
176
|
+
this.emit("signal", { App: signal });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else if (message.type === "response") {
|
|
180
|
+
this.handleResponse(message);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
throw new HolochainError("UnknownMessageType", `incoming message has unknown type - ${message.type}`);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
registerCloseListener(socket) {
|
|
188
|
+
socket.addEventListener("close", (closeEvent) => {
|
|
189
|
+
const pendingRequestIds = Object.keys(this.pendingRequests).map((id) => parseInt(id));
|
|
190
|
+
if (pendingRequestIds.length) {
|
|
191
|
+
pendingRequestIds.forEach((id) => {
|
|
192
|
+
const error = new HolochainError("ClientClosedWithPendingRequests", `client closed with pending requests - close event code: ${closeEvent.code}, request id: ${id}`);
|
|
193
|
+
this.pendingRequests[id].reject(error);
|
|
194
|
+
delete this.pendingRequests[id];
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}, { once: true });
|
|
198
|
+
}
|
|
199
|
+
async reconnectWebsocket(url, token) {
|
|
200
|
+
return new Promise((resolve, reject) => {
|
|
201
|
+
this.socket = new IsoWebSocket(url, this.options);
|
|
202
|
+
// This error event never occurs in tests. Could be removed?
|
|
203
|
+
this.socket.addEventListener("error", (errorEvent) => {
|
|
204
|
+
this.authenticationToken = undefined;
|
|
205
|
+
reject(new HolochainError("ConnectionError", `could not connect to Holochain Conductor API at ${url} - ${errorEvent.message}`));
|
|
206
|
+
}, { once: true });
|
|
207
|
+
const invalidTokenCloseListener = (closeEvent) => {
|
|
208
|
+
this.authenticationToken = undefined;
|
|
209
|
+
reject(new HolochainError("InvalidTokenError", `could not connect to ${this.url} due to an invalid app authentication token - close code ${closeEvent.code}`));
|
|
210
|
+
};
|
|
211
|
+
this.socket.addEventListener("close", invalidTokenCloseListener, {
|
|
212
|
+
once: true,
|
|
213
|
+
});
|
|
214
|
+
this.socket.addEventListener("open", async (_) => {
|
|
215
|
+
const encodedMsg = encode({
|
|
216
|
+
type: "authenticate",
|
|
217
|
+
data: encode({ token }),
|
|
218
|
+
});
|
|
219
|
+
this.socket.send(encodedMsg);
|
|
220
|
+
// Wait in case authentication fails.
|
|
221
|
+
setTimeout(() => {
|
|
222
|
+
this.socket.removeEventListener("close", invalidTokenCloseListener);
|
|
223
|
+
resolve();
|
|
224
|
+
}, 10);
|
|
225
|
+
}, { once: true });
|
|
226
|
+
});
|
|
227
|
+
}
|
|
169
228
|
handleResponse(msg) {
|
|
170
229
|
const id = msg.id;
|
|
171
230
|
if (this.pendingRequests[id]) {
|
|
@@ -181,20 +240,6 @@ export class WsClient extends Emittery {
|
|
|
181
240
|
console.error(`got response with no matching request. id = ${id} msg = ${msg}`);
|
|
182
241
|
}
|
|
183
242
|
}
|
|
184
|
-
/**
|
|
185
|
-
* Close the websocket connection.
|
|
186
|
-
*/
|
|
187
|
-
close(code = 1000) {
|
|
188
|
-
const closedPromise = new Promise((resolve) =>
|
|
189
|
-
// for an unknown reason "addEventListener" is seen as a non-callable
|
|
190
|
-
// property and gives a ts2349 error
|
|
191
|
-
// type assertion as workaround
|
|
192
|
-
this.socket.addEventListener("close", (event) => resolve(event))
|
|
193
|
-
// }
|
|
194
|
-
);
|
|
195
|
-
this.socket.close(code);
|
|
196
|
-
return closedPromise;
|
|
197
|
-
}
|
|
198
243
|
}
|
|
199
244
|
function assertHolochainMessage(message) {
|
|
200
245
|
if (typeof message === "object" &&
|
package/lib/api/common.js
CHANGED
package/lib/hdk/action.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { AgentPubKey, DnaHash, EntryHash, ActionHash, HoloHashed, Signature, Timestamp } from "../types.js";
|
|
2
2
|
import { Entry, EntryType } from "./entry.js";
|
|
3
3
|
import { LinkTag, LinkType, RateWeight } from "./link.js";
|
|
4
|
+
/**
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export interface SignedAction {
|
|
8
|
+
data: Action;
|
|
9
|
+
signature: Signature;
|
|
10
|
+
}
|
|
4
11
|
/**
|
|
5
12
|
* @public
|
|
6
13
|
*/
|
package/lib/tsdoc-metadata.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holochain/client",
|
|
3
|
-
"version": "0.19.0-dev.
|
|
3
|
+
"version": "0.19.0-dev.2",
|
|
4
4
|
"description": "A JavaScript client for the Holochain Conductor API",
|
|
5
5
|
"author": "Holochain Foundation <info@holochain.org> (https://holochain.org)",
|
|
6
6
|
"license": "CAL-1.0",
|
|
@@ -72,4 +72,4 @@
|
|
|
72
72
|
"tsx": "^4.7.2",
|
|
73
73
|
"typescript": "^4.9.5"
|
|
74
74
|
}
|
|
75
|
-
}
|
|
75
|
+
}
|