@crewhaus/call-session 0.1.4 → 0.1.6
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/dist/adapters/livekit-sip.d.ts +14 -0
- package/dist/adapters/livekit-sip.js +75 -0
- package/dist/adapters/twilio.d.ts +17 -0
- package/dist/adapters/twilio.js +96 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +144 -0
- package/package.json +9 -6
- package/src/adapters/adapters.test.ts +0 -433
- package/src/adapters/livekit-sip.ts +0 -81
- package/src/adapters/twilio.ts +0 -104
- package/src/index.test.ts +0 -224
- package/src/index.ts +0 -213
package/src/index.test.ts
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
CallSessionError,
|
|
4
|
-
type CallTransition,
|
|
5
|
-
type TelephonyAdapter,
|
|
6
|
-
createCallSession,
|
|
7
|
-
createInMemoryTelephonyAdapter,
|
|
8
|
-
} from "./index.js";
|
|
9
|
-
|
|
10
|
-
describe("createCallSession (T1 + T9 state machine)", () => {
|
|
11
|
-
test("happy path: idle → dialing → connected → on-hold → connected → terminated", async () => {
|
|
12
|
-
const adapter = createInMemoryTelephonyAdapter();
|
|
13
|
-
const transitions: CallTransition[] = [];
|
|
14
|
-
const session = createCallSession({ adapter });
|
|
15
|
-
session.on((t) => transitions.push(t));
|
|
16
|
-
|
|
17
|
-
await session.dial("tel:+15555550100");
|
|
18
|
-
expect(session.state).toBe("dialing");
|
|
19
|
-
await session.answer();
|
|
20
|
-
expect(session.state).toBe("connected");
|
|
21
|
-
await session.hold();
|
|
22
|
-
expect(session.state).toBe("on-hold");
|
|
23
|
-
await session.resume();
|
|
24
|
-
expect(session.state).toBe("connected");
|
|
25
|
-
await session.end({ reason: "user hangup" });
|
|
26
|
-
expect(session.state).toBe("terminated");
|
|
27
|
-
|
|
28
|
-
expect(transitions.map((t) => `${t.from}→${t.to}`)).toEqual([
|
|
29
|
-
"idle→dialing",
|
|
30
|
-
"dialing→connected",
|
|
31
|
-
"connected→on-hold",
|
|
32
|
-
"on-hold→connected",
|
|
33
|
-
"connected→terminated",
|
|
34
|
-
]);
|
|
35
|
-
expect(adapter.calls.map((c) => c.verb)).toEqual(["dial", "answer", "hold", "resume", "end"]);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("transfer path: idle → dialing → connected → transferred → terminated", async () => {
|
|
39
|
-
const adapter = createInMemoryTelephonyAdapter();
|
|
40
|
-
const session = createCallSession({ adapter });
|
|
41
|
-
await session.dial("tel:a");
|
|
42
|
-
await session.answer();
|
|
43
|
-
await session.transfer("tel:b");
|
|
44
|
-
expect(session.state).toBe("transferred");
|
|
45
|
-
await session.end();
|
|
46
|
-
expect(session.state).toBe("terminated");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("illegal transitions throw CallSessionError without changing state", async () => {
|
|
50
|
-
const adapter = createInMemoryTelephonyAdapter();
|
|
51
|
-
const session = createCallSession({ adapter });
|
|
52
|
-
await expect(session.answer()).rejects.toThrow(CallSessionError); // idle → connected illegal
|
|
53
|
-
expect(session.state).toBe("idle");
|
|
54
|
-
await expect(session.hold()).rejects.toThrow(CallSessionError);
|
|
55
|
-
expect(session.state).toBe("idle");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("dial failure auto-transitions to terminated", async () => {
|
|
59
|
-
const adapter = createInMemoryTelephonyAdapter();
|
|
60
|
-
adapter.failNextDial("network down");
|
|
61
|
-
const session = createCallSession({ adapter });
|
|
62
|
-
await expect(session.dial("tel:a")).rejects.toThrow(/network down/);
|
|
63
|
-
expect(session.state).toBe("terminated");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("listener errors are isolated and do not block transitions", async () => {
|
|
67
|
-
const adapter = createInMemoryTelephonyAdapter();
|
|
68
|
-
const session = createCallSession({ adapter });
|
|
69
|
-
session.on(() => {
|
|
70
|
-
throw new Error("listener oops");
|
|
71
|
-
});
|
|
72
|
-
let okListenerHits = 0;
|
|
73
|
-
session.on(() => {
|
|
74
|
-
okListenerHits += 1;
|
|
75
|
-
});
|
|
76
|
-
await session.dial("tel:a");
|
|
77
|
-
expect(okListenerHits).toBe(1);
|
|
78
|
-
expect(session.state).toBe("dialing");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("history returns transitions in order with timestamps", async () => {
|
|
82
|
-
const adapter = createInMemoryTelephonyAdapter();
|
|
83
|
-
const session = createCallSession({ adapter });
|
|
84
|
-
await session.dial("tel:a");
|
|
85
|
-
await session.answer();
|
|
86
|
-
const log = session.history();
|
|
87
|
-
expect(log.length).toBe(2);
|
|
88
|
-
expect(log[0]?.from).toBe("idle");
|
|
89
|
-
expect(log[0]?.to).toBe("dialing");
|
|
90
|
-
expect(typeof log[0]?.at).toBe("string");
|
|
91
|
-
expect(log[1]?.to).toBe("connected");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("T9 property: every state's outgoing transitions are exactly the configured set", async () => {
|
|
95
|
-
// The state-machine definition is encoded as a constant; ensure
|
|
96
|
-
// illegal transitions are rejected uniformly.
|
|
97
|
-
const adapter = createInMemoryTelephonyAdapter();
|
|
98
|
-
const session = createCallSession({ adapter });
|
|
99
|
-
await session.dial("a");
|
|
100
|
-
await session.answer();
|
|
101
|
-
await session.end();
|
|
102
|
-
// Once terminated, every verb throws.
|
|
103
|
-
await expect(session.dial("b")).rejects.toThrow(CallSessionError);
|
|
104
|
-
await expect(session.hold()).rejects.toThrow(CallSessionError);
|
|
105
|
-
await expect(session.transfer("c")).rejects.toThrow(CallSessionError);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("on() returns an unsubscribe that stops further notifications", async () => {
|
|
109
|
-
const adapter = createInMemoryTelephonyAdapter();
|
|
110
|
-
const session = createCallSession({ adapter });
|
|
111
|
-
let hits = 0;
|
|
112
|
-
const off = session.on(() => {
|
|
113
|
-
hits += 1;
|
|
114
|
-
});
|
|
115
|
-
await session.dial("tel:a"); // listener active → 1 hit
|
|
116
|
-
expect(hits).toBe(1);
|
|
117
|
-
off(); // exercises the unsubscribe closure
|
|
118
|
-
await session.answer(); // listener removed → still 1
|
|
119
|
-
expect(hits).toBe(1);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("unsubscribing a never-fired listener is a harmless no-op", () => {
|
|
123
|
-
const adapter = createInMemoryTelephonyAdapter();
|
|
124
|
-
const session = createCallSession({ adapter });
|
|
125
|
-
const off = session.on(() => {
|
|
126
|
-
throw new Error("should never run");
|
|
127
|
-
});
|
|
128
|
-
// Calling unsubscribe before any transition must not throw.
|
|
129
|
-
expect(() => off()).not.toThrow();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test("reason is threaded into the transition record and omitted when absent", async () => {
|
|
133
|
-
const adapter = createInMemoryTelephonyAdapter();
|
|
134
|
-
const session = createCallSession({ adapter });
|
|
135
|
-
await session.dial("tel:a", { reason: "outbound campaign" });
|
|
136
|
-
await session.answer(); // no reason supplied
|
|
137
|
-
const log = session.history();
|
|
138
|
-
expect(log[0]?.reason).toBe("outbound campaign");
|
|
139
|
-
expect("reason" in (log[1] ?? {})).toBe(false);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('end() defaults the adapter reason to "end" when none is given', async () => {
|
|
143
|
-
const adapter = createInMemoryTelephonyAdapter();
|
|
144
|
-
const session = createCallSession({ adapter });
|
|
145
|
-
await session.dial("tel:a");
|
|
146
|
-
await session.answer();
|
|
147
|
-
await session.end(); // no reason
|
|
148
|
-
const endCall = adapter.calls.find((c) => c.verb === "end");
|
|
149
|
-
expect(endCall?.arg).toBe("end");
|
|
150
|
-
// No transition-level reason recorded when none supplied.
|
|
151
|
-
const last = session.history().at(-1);
|
|
152
|
-
expect(last?.to).toBe("terminated");
|
|
153
|
-
expect("reason" in (last ?? {})).toBe(false);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test("end() forwards an explicit reason to the adapter", async () => {
|
|
157
|
-
const adapter = createInMemoryTelephonyAdapter();
|
|
158
|
-
const session = createCallSession({ adapter });
|
|
159
|
-
await session.dial("tel:a");
|
|
160
|
-
await session.end({ reason: "callee busy" });
|
|
161
|
-
const endCall = adapter.calls.find((c) => c.verb === "end");
|
|
162
|
-
expect(endCall?.arg).toBe("callee busy");
|
|
163
|
-
expect(session.history().at(-1)?.reason).toBe("callee busy");
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test("dial failure surfaced from a non-Error rejection falls back to String()", async () => {
|
|
167
|
-
// Adapter whose dial rejects with a non-Error value — exercises the
|
|
168
|
-
// `?? String(err)` branch in the dial-failure path.
|
|
169
|
-
const adapter: TelephonyAdapter = {
|
|
170
|
-
kind: "weird",
|
|
171
|
-
async dial() {
|
|
172
|
-
throw "boom-string"; // not an Error, so `.message` is undefined
|
|
173
|
-
},
|
|
174
|
-
async answer() {},
|
|
175
|
-
async hold() {},
|
|
176
|
-
async resume() {},
|
|
177
|
-
async transfer() {},
|
|
178
|
-
async end() {},
|
|
179
|
-
};
|
|
180
|
-
const session = createCallSession({ adapter });
|
|
181
|
-
await expect(session.dial("tel:a")).rejects.toBe("boom-string");
|
|
182
|
-
expect(session.state).toBe("terminated");
|
|
183
|
-
expect(session.history().at(-1)?.reason).toContain("boom-string");
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test("injected now() controls transition timestamps deterministically", async () => {
|
|
187
|
-
const adapter = createInMemoryTelephonyAdapter();
|
|
188
|
-
const fixed = new Date("2026-06-04T12:00:00.000Z");
|
|
189
|
-
const session = createCallSession({ adapter, now: () => fixed });
|
|
190
|
-
await session.dial("tel:a");
|
|
191
|
-
expect(session.history()[0]?.at).toBe("2026-06-04T12:00:00.000Z");
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("in-memory adapter: failNextDial uses its default message and resets after one dial", async () => {
|
|
195
|
-
const adapter = createInMemoryTelephonyAdapter();
|
|
196
|
-
adapter.failNextDial(); // default message branch
|
|
197
|
-
const s1 = createCallSession({ adapter });
|
|
198
|
-
await expect(s1.dial("tel:a")).rejects.toThrow(/in-memory: dial failed/);
|
|
199
|
-
// Failure is one-shot: a fresh session dials cleanly.
|
|
200
|
-
const s2 = createCallSession({ adapter });
|
|
201
|
-
await s2.dial("tel:b");
|
|
202
|
-
expect(s2.state).toBe("dialing");
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test("in-memory adapter records every verb and argument in order", async () => {
|
|
206
|
-
const adapter = createInMemoryTelephonyAdapter();
|
|
207
|
-
expect(adapter.kind).toBe("in-memory");
|
|
208
|
-
const session = createCallSession({ adapter });
|
|
209
|
-
await session.dial("tel:a");
|
|
210
|
-
await session.answer();
|
|
211
|
-
await session.hold();
|
|
212
|
-
await session.resume();
|
|
213
|
-
await session.transfer("tel:b");
|
|
214
|
-
await session.end({ reason: "done" });
|
|
215
|
-
expect(adapter.calls).toEqual([
|
|
216
|
-
{ verb: "dial", arg: "tel:a" },
|
|
217
|
-
{ verb: "answer" },
|
|
218
|
-
{ verb: "hold" },
|
|
219
|
-
{ verb: "resume" },
|
|
220
|
-
{ verb: "transfer", arg: "tel:b" },
|
|
221
|
-
{ verb: "end", arg: "done" },
|
|
222
|
-
]);
|
|
223
|
-
});
|
|
224
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Catalog R16 `call-session` — Section 24 VOICE.
|
|
3
|
-
*
|
|
4
|
-
* Call lifecycle state machine with pluggable telephony adapter slots.
|
|
5
|
-
*
|
|
6
|
-
* idle ─dial()→ dialing ─answer()→ connected ─hold()→ on-hold
|
|
7
|
-
* │ │
|
|
8
|
-
* │ └─resume()→ connected
|
|
9
|
-
* ├─transfer()→ transferred ─end()→ terminated
|
|
10
|
-
* └─end()─────────────────────→ terminated
|
|
11
|
-
*
|
|
12
|
-
* Telephony adapters (Twilio, LiveKit SIP, Vonage) are STUBS in v0 —
|
|
13
|
-
* the interface is exercised by an in-memory adapter the tests + the
|
|
14
|
-
* smoke harness use to drive transitions deterministically. Real
|
|
15
|
-
* telephony lands in follow-up PRs (the kickoff explicitly defers it).
|
|
16
|
-
*
|
|
17
|
-
* Per-state hooks fire on transitions — the daemon uses them to log
|
|
18
|
-
* trace events, measure connect-time, etc. Hook errors are isolated:
|
|
19
|
-
* a misbehaving listener is logged but cannot wedge the state machine.
|
|
20
|
-
*/
|
|
21
|
-
import { CrewhausError } from "@crewhaus/errors";
|
|
22
|
-
|
|
23
|
-
export class CallSessionError extends CrewhausError {
|
|
24
|
-
override readonly name = "CallSessionError";
|
|
25
|
-
constructor(message: string, cause?: unknown) {
|
|
26
|
-
super("config", message, cause);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Section 30 — additional telephony adapters
|
|
31
|
-
export {
|
|
32
|
-
createTwilioTelephonyAdapter,
|
|
33
|
-
type TwilioAdapterOptions,
|
|
34
|
-
} from "./adapters/twilio";
|
|
35
|
-
export {
|
|
36
|
-
createLiveKitSipAdapter,
|
|
37
|
-
type LiveKitSipAdapterOptions,
|
|
38
|
-
} from "./adapters/livekit-sip";
|
|
39
|
-
|
|
40
|
-
export type CallState = "idle" | "dialing" | "connected" | "on-hold" | "transferred" | "terminated";
|
|
41
|
-
|
|
42
|
-
export type CallTransition = {
|
|
43
|
-
readonly from: CallState;
|
|
44
|
-
readonly to: CallState;
|
|
45
|
-
readonly at: string;
|
|
46
|
-
readonly reason?: string;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export type CallStateListener = (transition: CallTransition) => void;
|
|
50
|
-
|
|
51
|
-
export interface TelephonyAdapter {
|
|
52
|
-
readonly kind: string;
|
|
53
|
-
/** Begin the outbound dial — promise resolves on adapter ack. */
|
|
54
|
-
dial(toUri: string): Promise<void>;
|
|
55
|
-
/** Answer an incoming call (no-op for outbound flows). */
|
|
56
|
-
answer(): Promise<void>;
|
|
57
|
-
/** Briefly suspend the bidirectional audio path. */
|
|
58
|
-
hold(): Promise<void>;
|
|
59
|
-
resume(): Promise<void>;
|
|
60
|
-
/** Hand the call off to another agent / number. */
|
|
61
|
-
transfer(toUri: string): Promise<void>;
|
|
62
|
-
/** Hang up. */
|
|
63
|
-
end(reason: string): Promise<void>;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface CallSession {
|
|
67
|
-
readonly state: CallState;
|
|
68
|
-
dial(toUri: string, opts?: { reason?: string }): Promise<void>;
|
|
69
|
-
answer(opts?: { reason?: string }): Promise<void>;
|
|
70
|
-
hold(opts?: { reason?: string }): Promise<void>;
|
|
71
|
-
resume(opts?: { reason?: string }): Promise<void>;
|
|
72
|
-
transfer(toUri: string, opts?: { reason?: string }): Promise<void>;
|
|
73
|
-
end(opts?: { reason?: string }): Promise<void>;
|
|
74
|
-
on(listener: CallStateListener): () => void;
|
|
75
|
-
/** Diagnostic — full transition history (newest last). */
|
|
76
|
-
history(): ReadonlyArray<CallTransition>;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export type CreateCallSessionOptions = {
|
|
80
|
-
readonly adapter: TelephonyAdapter;
|
|
81
|
-
readonly now?: () => Date;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const TRANSITIONS: Readonly<Record<CallState, ReadonlyArray<CallState>>> = {
|
|
85
|
-
idle: ["dialing"],
|
|
86
|
-
dialing: ["connected", "terminated"],
|
|
87
|
-
connected: ["on-hold", "transferred", "terminated"],
|
|
88
|
-
"on-hold": ["connected", "terminated"],
|
|
89
|
-
transferred: ["terminated"],
|
|
90
|
-
terminated: [],
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
export function createCallSession(opts: CreateCallSessionOptions): CallSession {
|
|
94
|
-
const now = opts.now ?? (() => new Date());
|
|
95
|
-
const listeners = new Set<CallStateListener>();
|
|
96
|
-
const log: CallTransition[] = [];
|
|
97
|
-
let state: CallState = "idle";
|
|
98
|
-
|
|
99
|
-
function transitionTo(target: CallState, reason?: string): void {
|
|
100
|
-
const allowed = TRANSITIONS[state];
|
|
101
|
-
if (!allowed.includes(target)) {
|
|
102
|
-
throw new CallSessionError(
|
|
103
|
-
`illegal transition ${state} → ${target}${reason ? ` (reason: ${reason})` : ""}`,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
const t: CallTransition = {
|
|
107
|
-
from: state,
|
|
108
|
-
to: target,
|
|
109
|
-
at: now().toISOString(),
|
|
110
|
-
...(reason !== undefined ? { reason } : {}),
|
|
111
|
-
};
|
|
112
|
-
state = target;
|
|
113
|
-
log.push(t);
|
|
114
|
-
for (const l of listeners) {
|
|
115
|
-
try {
|
|
116
|
-
l(t);
|
|
117
|
-
} catch {
|
|
118
|
-
// Isolate listener errors.
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
get state() {
|
|
125
|
-
return state;
|
|
126
|
-
},
|
|
127
|
-
async dial(toUri, callOpts = {}) {
|
|
128
|
-
transitionTo("dialing", callOpts.reason);
|
|
129
|
-
try {
|
|
130
|
-
await opts.adapter.dial(toUri);
|
|
131
|
-
} catch (err) {
|
|
132
|
-
transitionTo("terminated", `dial failed: ${(err as Error).message ?? String(err)}`);
|
|
133
|
-
throw err;
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
async answer(callOpts = {}) {
|
|
137
|
-
await opts.adapter.answer();
|
|
138
|
-
transitionTo("connected", callOpts.reason);
|
|
139
|
-
},
|
|
140
|
-
async hold(callOpts = {}) {
|
|
141
|
-
await opts.adapter.hold();
|
|
142
|
-
transitionTo("on-hold", callOpts.reason);
|
|
143
|
-
},
|
|
144
|
-
async resume(callOpts = {}) {
|
|
145
|
-
await opts.adapter.resume();
|
|
146
|
-
transitionTo("connected", callOpts.reason);
|
|
147
|
-
},
|
|
148
|
-
async transfer(toUri, callOpts = {}) {
|
|
149
|
-
await opts.adapter.transfer(toUri);
|
|
150
|
-
transitionTo("transferred", callOpts.reason);
|
|
151
|
-
},
|
|
152
|
-
async end(callOpts = {}) {
|
|
153
|
-
await opts.adapter.end(callOpts.reason ?? "end");
|
|
154
|
-
transitionTo("terminated", callOpts.reason);
|
|
155
|
-
},
|
|
156
|
-
on(listener) {
|
|
157
|
-
listeners.add(listener);
|
|
158
|
-
return () => {
|
|
159
|
-
listeners.delete(listener);
|
|
160
|
-
};
|
|
161
|
-
},
|
|
162
|
-
history() {
|
|
163
|
-
return [...log];
|
|
164
|
-
},
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ---------------------------------------------------------------------------
|
|
169
|
-
// In-memory adapter — for tests + the smoke harness.
|
|
170
|
-
// ---------------------------------------------------------------------------
|
|
171
|
-
|
|
172
|
-
export interface InMemoryTelephonyAdapter extends TelephonyAdapter {
|
|
173
|
-
readonly kind: "in-memory";
|
|
174
|
-
/** Test seam — every adapter call is recorded here in order. */
|
|
175
|
-
readonly calls: ReadonlyArray<{ readonly verb: string; readonly arg?: string }>;
|
|
176
|
-
/** Inject a failure on the next `dial(...)` call. */
|
|
177
|
-
failNextDial(message?: string): void;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export function createInMemoryTelephonyAdapter(): InMemoryTelephonyAdapter {
|
|
181
|
-
const calls: { verb: string; arg?: string }[] = [];
|
|
182
|
-
let nextDialFailure: string | undefined;
|
|
183
|
-
return {
|
|
184
|
-
kind: "in-memory",
|
|
185
|
-
calls,
|
|
186
|
-
failNextDial(message = "in-memory: dial failed") {
|
|
187
|
-
nextDialFailure = message;
|
|
188
|
-
},
|
|
189
|
-
async dial(toUri) {
|
|
190
|
-
calls.push({ verb: "dial", arg: toUri });
|
|
191
|
-
if (nextDialFailure !== undefined) {
|
|
192
|
-
const m = nextDialFailure;
|
|
193
|
-
nextDialFailure = undefined;
|
|
194
|
-
throw new CallSessionError(m);
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
async answer() {
|
|
198
|
-
calls.push({ verb: "answer" });
|
|
199
|
-
},
|
|
200
|
-
async hold() {
|
|
201
|
-
calls.push({ verb: "hold" });
|
|
202
|
-
},
|
|
203
|
-
async resume() {
|
|
204
|
-
calls.push({ verb: "resume" });
|
|
205
|
-
},
|
|
206
|
-
async transfer(toUri) {
|
|
207
|
-
calls.push({ verb: "transfer", arg: toUri });
|
|
208
|
-
},
|
|
209
|
-
async end(reason) {
|
|
210
|
-
calls.push({ verb: "end", arg: reason });
|
|
211
|
-
},
|
|
212
|
-
};
|
|
213
|
-
}
|