@structured-id/ui-core 0.1.5
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/LICENSE +661 -0
- package/README.md +47 -0
- package/dist/browser-vp/index.cjs +349 -0
- package/dist/browser-vp/index.d.cts +287 -0
- package/dist/browser-vp/index.d.ts +287 -0
- package/dist/browser-vp/index.js +349 -0
- package/dist/chunk-C3Q766CY.cjs +125 -0
- package/dist/chunk-J5YXRXB5.js +125 -0
- package/dist/index.cjs +19360 -0
- package/dist/index.d.cts +9536 -0
- package/dist/index.d.ts +9536 -0
- package/dist/index.js +19360 -0
- package/dist/storage-4CB2JTLW.cjs +22 -0
- package/dist/storage-THRWREZP.js +22 -0
- package/package.json +93 -0
- package/src/browser-vp/crypto.ts +254 -0
- package/src/browser-vp/index.test.ts +10 -0
- package/src/browser-vp/index.ts +118 -0
- package/src/browser-vp/prf.ts +181 -0
- package/src/browser-vp/storage.test.ts +9 -0
- package/src/browser-vp/storage.ts +165 -0
- package/src/browser-vp/types.ts +97 -0
- package/src/browser-vp/vp.test.ts +43 -0
- package/src/browser-vp/vp.ts +113 -0
- package/src/components/SidAvatar.vue +106 -0
- package/src/components/SidConfirmDialog.vue +125 -0
- package/src/components/SidStatusBadge.vue +71 -0
- package/src/components/index.ts +5 -0
- package/src/composables/index.ts +10 -0
- package/src/composables/useAuth.ts +40 -0
- package/src/composables/useBffAuth.ts +147 -0
- package/src/composables/useGrpc.test.ts +196 -0
- package/src/composables/useGrpc.ts +120 -0
- package/src/composables/useSession.ts +80 -0
- package/src/generated/google/api/http.ts +687 -0
- package/src/generated/google/protobuf/descriptor.ts +4865 -0
- package/src/generated/google/protobuf/duration.ts +230 -0
- package/src/generated/google/protobuf/empty.ts +94 -0
- package/src/generated/google/protobuf/struct.ts +484 -0
- package/src/generated/google/protobuf/timestamp.ts +287 -0
- package/src/generated/sid/v1/account/account.client.ts +527 -0
- package/src/generated/sid/v1/account/account.ts +3261 -0
- package/src/generated/sid/v1/admin/admin.client.ts +136 -0
- package/src/generated/sid/v1/admin/admin.ts +846 -0
- package/src/generated/sid/v1/admin/admin_roles.client.ts +181 -0
- package/src/generated/sid/v1/admin/admin_roles.ts +1606 -0
- package/src/generated/sid/v1/admin/analytics.client.ts +106 -0
- package/src/generated/sid/v1/admin/analytics.ts +1017 -0
- package/src/generated/sid/v1/admin/branding.client.ts +166 -0
- package/src/generated/sid/v1/admin/branding.ts +1906 -0
- package/src/generated/sid/v1/admin/enrollment.client.ts +165 -0
- package/src/generated/sid/v1/admin/enrollment.ts +1704 -0
- package/src/generated/sid/v1/admin/events.client.ts +195 -0
- package/src/generated/sid/v1/admin/events.ts +1900 -0
- package/src/generated/sid/v1/admin/flow.client.ts +183 -0
- package/src/generated/sid/v1/admin/flow.ts +1552 -0
- package/src/generated/sid/v1/admin/keys.client.ts +107 -0
- package/src/generated/sid/v1/admin/keys.ts +931 -0
- package/src/generated/sid/v1/admin/login_experience.client.ts +160 -0
- package/src/generated/sid/v1/admin/login_experience.ts +1061 -0
- package/src/generated/sid/v1/admin/migration.client.ts +127 -0
- package/src/generated/sid/v1/admin/migration.ts +1948 -0
- package/src/generated/sid/v1/admin/notifications.client.ts +128 -0
- package/src/generated/sid/v1/admin/notifications.ts +1332 -0
- package/src/generated/sid/v1/admin/operations.client.ts +241 -0
- package/src/generated/sid/v1/admin/operations.ts +2648 -0
- package/src/generated/sid/v1/admin/organizations.client.ts +57 -0
- package/src/generated/sid/v1/admin/organizations.ts +349 -0
- package/src/generated/sid/v1/admin/scopes.client.ts +171 -0
- package/src/generated/sid/v1/admin/scopes.ts +1324 -0
- package/src/generated/sid/v1/admin/security.client.ts +412 -0
- package/src/generated/sid/v1/admin/security.ts +4086 -0
- package/src/generated/sid/v1/admin/sessions.client.ts +114 -0
- package/src/generated/sid/v1/admin/sessions.ts +923 -0
- package/src/generated/sid/v1/admin/settings.client.ts +340 -0
- package/src/generated/sid/v1/admin/settings.ts +2395 -0
- package/src/generated/sid/v1/admin/user_profile.client.ts +57 -0
- package/src/generated/sid/v1/admin/user_profile.ts +730 -0
- package/src/generated/sid/v1/admin/webhooks.client.ts +269 -0
- package/src/generated/sid/v1/admin/webhooks.ts +2804 -0
- package/src/generated/sid/v1/attestation/attestation.client.ts +115 -0
- package/src/generated/sid/v1/attestation/attestation.ts +813 -0
- package/src/generated/sid/v1/authn/auth.client.ts +900 -0
- package/src/generated/sid/v1/authn/auth.ts +9287 -0
- package/src/generated/sid/v1/authn/flows.client.ts +413 -0
- package/src/generated/sid/v1/authn/flows.ts +3217 -0
- package/src/generated/sid/v1/authz/authz.client.ts +459 -0
- package/src/generated/sid/v1/authz/authz.ts +5343 -0
- package/src/generated/sid/v1/authz/governance.client.ts +102 -0
- package/src/generated/sid/v1/authz/governance.ts +818 -0
- package/src/generated/sid/v1/common/errors.ts +184 -0
- package/src/generated/sid/v1/common/pagination.ts +152 -0
- package/src/generated/sid/v1/common/transport.ts +227 -0
- package/src/generated/sid/v1/common/types.ts +151 -0
- package/src/generated/sid/v1/events/event_stream.client.ts +67 -0
- package/src/generated/sid/v1/events/event_stream.ts +374 -0
- package/src/generated/sid/v1/federation/upstream.client.ts +142 -0
- package/src/generated/sid/v1/federation/upstream.ts +1736 -0
- package/src/generated/sid/v1/identity/identity.client.ts +467 -0
- package/src/generated/sid/v1/identity/identity.ts +5613 -0
- package/src/generated/sid/v1/machine/machine_user.client.ts +219 -0
- package/src/generated/sid/v1/machine/machine_user.ts +2400 -0
- package/src/generated/sid/v1/machine/pat.client.ts +147 -0
- package/src/generated/sid/v1/machine/pat.ts +1268 -0
- package/src/generated/sid/v1/projects/projects.client.ts +286 -0
- package/src/generated/sid/v1/projects/projects.ts +2714 -0
- package/src/generated/sid/v1/scim/scim.client.ts +274 -0
- package/src/generated/sid/v1/scim/scim.ts +3043 -0
- package/src/generated/sid/v1/test/test.client.ts +232 -0
- package/src/generated/sid/v1/test/test.ts +1136 -0
- package/src/i18n/index.ts +40 -0
- package/src/index.ts +157 -0
- package/src/profile/auth.ts +15 -0
- package/src/profile/components/ConsentManager.vue +88 -0
- package/src/profile/components/ProfileEditor.vue +146 -0
- package/src/profile/components/SecuritySettings.vue +170 -0
- package/src/profile/components/SessionList.vue +100 -0
- package/src/profile/components/SidLoginForm.test.ts +80 -0
- package/src/profile/components/SidLoginForm.vue +230 -0
- package/src/profile/components/SidMfaChallenge.test.ts +127 -0
- package/src/profile/components/SidMfaChallenge.vue +243 -0
- package/src/profile/components/SidRegistrationForm.test.ts +79 -0
- package/src/profile/components/SidRegistrationForm.vue +317 -0
- package/src/profile/components/index.ts +11 -0
- package/src/profile/composables/index.ts +6 -0
- package/src/profile/composables/useAccountApi.ts +203 -0
- package/src/profile/composables/useAuthApi.opaque.test.ts +140 -0
- package/src/profile/composables/useAuthApi.ts +617 -0
- package/src/profile/composables/useIdentityApi.ts +123 -0
- package/src/profile/composables/useLoginSuccess.test.ts +112 -0
- package/src/profile/composables/useLoginSuccess.ts +65 -0
- package/src/profile/composables/useOpaqueAuth.test.ts +290 -0
- package/src/profile/composables/useOpaqueAuth.ts +191 -0
- package/src/profile/composables/useSecurityLevel.test.ts +198 -0
- package/src/profile/composables/useSecurityLevel.ts +324 -0
- package/src/profile/index.ts +18 -0
- package/src/quasar/SidPrincipalInput.vue +209 -0
- package/src/quasar/index.ts +8 -0
- package/src/stores/auth.test.ts +83 -0
- package/src/stores/auth.ts +92 -0
- package/src/stores/index.ts +1 -0
- package/src/transport/adaptive.test.ts +728 -0
- package/src/transport/adaptive.ts +308 -0
- package/src/transport/framing.test.ts +227 -0
- package/src/transport/framing.ts +98 -0
- package/src/transport/webtransport.test.ts +728 -0
- package/src/transport/webtransport.ts +323 -0
- package/src/types/auth.ts +58 -0
- package/src/types/index.ts +1 -0
- package/src/utils/formatters.test.ts +26 -0
- package/src/utils/formatters.ts +69 -0
- package/src/utils/index.ts +16 -0
- package/src/utils/normalizePrincipal.test.ts +188 -0
- package/src/utils/normalizePrincipal.ts +159 -0
- package/src/utils/validators.test.ts +75 -0
- package/src/utils/validators.ts +36 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { Deferred, RpcError, UnaryCall } from "@protobuf-ts/runtime-rpc";
|
|
3
|
+
import type {
|
|
4
|
+
RpcTransport,
|
|
5
|
+
MethodInfo,
|
|
6
|
+
RpcOptions,
|
|
7
|
+
RpcMetadata,
|
|
8
|
+
RpcStatus,
|
|
9
|
+
} from "@protobuf-ts/runtime-rpc";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Mock WebTransport global (does not exist in Node.js)
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
class MockWebTransport {
|
|
16
|
+
ready = Promise.resolve();
|
|
17
|
+
closed = new Promise<void>(() => {});
|
|
18
|
+
close = vi.fn();
|
|
19
|
+
createBidirectionalStream = vi.fn();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Mock modules BEFORE importing the module under test
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
// Mock GrpcWebFetchTransport
|
|
27
|
+
vi.mock("@protobuf-ts/grpcweb-transport", () => ({
|
|
28
|
+
GrpcWebFetchTransport: vi
|
|
29
|
+
.fn()
|
|
30
|
+
.mockImplementation(() => createMockTransport()),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
// Mock WebTransportConnection and WebTransportRpcTransport
|
|
34
|
+
const mockWtConnect = vi.fn<[], Promise<void>>();
|
|
35
|
+
const mockWtClose = vi.fn();
|
|
36
|
+
let mockWtTransportInstance: RpcTransport;
|
|
37
|
+
|
|
38
|
+
vi.mock("./webtransport", () => ({
|
|
39
|
+
WebTransportConnection: vi.fn().mockImplementation(() => ({
|
|
40
|
+
connect: mockWtConnect,
|
|
41
|
+
close: mockWtClose,
|
|
42
|
+
})),
|
|
43
|
+
WebTransportRpcTransport: vi.fn().mockImplementation(() => {
|
|
44
|
+
return mockWtTransportInstance;
|
|
45
|
+
}),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
// Now import the module under test (AFTER vi.mock calls)
|
|
49
|
+
import { AdaptiveRpcTransport } from "./adaptive";
|
|
50
|
+
import type { AdaptiveTransportOptions } from "./adaptive";
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Helpers
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
interface MockMsg {
|
|
57
|
+
value: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createMethodInfo(): MethodInfo<MockMsg, MockMsg> {
|
|
61
|
+
return {
|
|
62
|
+
service: { typeName: "test.TestService", methods: [], options: {} },
|
|
63
|
+
name: "TestMethod",
|
|
64
|
+
localName: "testMethod",
|
|
65
|
+
idempotency: undefined,
|
|
66
|
+
serverStreaming: false,
|
|
67
|
+
clientStreaming: false,
|
|
68
|
+
I: {
|
|
69
|
+
typeName: "TestInput",
|
|
70
|
+
toBinary: () => new Uint8Array(),
|
|
71
|
+
fromBinary: () => ({ value: "input" }),
|
|
72
|
+
},
|
|
73
|
+
O: {
|
|
74
|
+
typeName: "TestOutput",
|
|
75
|
+
toBinary: () => new Uint8Array(),
|
|
76
|
+
fromBinary: () => ({ value: "output" }),
|
|
77
|
+
},
|
|
78
|
+
options: {},
|
|
79
|
+
} as unknown as MethodInfo<MockMsg, MockMsg>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function createMockTransport(): RpcTransport {
|
|
83
|
+
return {
|
|
84
|
+
mergeOptions: (opts?: Partial<RpcOptions>) => opts ?? {},
|
|
85
|
+
unary: vi.fn(),
|
|
86
|
+
serverStreaming: vi.fn(),
|
|
87
|
+
clientStreaming: vi.fn(),
|
|
88
|
+
duplex: vi.fn(),
|
|
89
|
+
} as unknown as RpcTransport;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Create a resolved UnaryCall with the given response. */
|
|
93
|
+
function resolvedUnaryCall<I extends object, O extends object>(
|
|
94
|
+
method: MethodInfo<I, O>,
|
|
95
|
+
input: I,
|
|
96
|
+
response: O,
|
|
97
|
+
): UnaryCall<I, O> {
|
|
98
|
+
const dH = new Deferred<RpcMetadata>();
|
|
99
|
+
const dM = new Deferred<O>();
|
|
100
|
+
const dS = new Deferred<RpcStatus>();
|
|
101
|
+
const dT = new Deferred<RpcMetadata>();
|
|
102
|
+
|
|
103
|
+
dH.resolve({});
|
|
104
|
+
dM.resolve(response);
|
|
105
|
+
dS.resolve({ code: "OK", detail: "" });
|
|
106
|
+
dT.resolve({});
|
|
107
|
+
|
|
108
|
+
return new UnaryCall(
|
|
109
|
+
method,
|
|
110
|
+
{},
|
|
111
|
+
input,
|
|
112
|
+
dH.promise,
|
|
113
|
+
dM.promise,
|
|
114
|
+
dS.promise,
|
|
115
|
+
dT.promise,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Create a rejected UnaryCall that fails with the given RpcError. */
|
|
120
|
+
function rejectedUnaryCall<I extends object, O extends object>(
|
|
121
|
+
method: MethodInfo<I, O>,
|
|
122
|
+
input: I,
|
|
123
|
+
error: RpcError,
|
|
124
|
+
): UnaryCall<I, O> {
|
|
125
|
+
const dH = new Deferred<RpcMetadata>();
|
|
126
|
+
const dM = new Deferred<O>();
|
|
127
|
+
const dS = new Deferred<RpcStatus>();
|
|
128
|
+
const dT = new Deferred<RpcMetadata>();
|
|
129
|
+
|
|
130
|
+
dH.reject(error);
|
|
131
|
+
dM.reject(error);
|
|
132
|
+
dS.reject(error);
|
|
133
|
+
dT.reject(error);
|
|
134
|
+
|
|
135
|
+
return new UnaryCall(
|
|
136
|
+
method,
|
|
137
|
+
{},
|
|
138
|
+
input,
|
|
139
|
+
dH.promise,
|
|
140
|
+
dM.promise,
|
|
141
|
+
dS.promise,
|
|
142
|
+
dT.promise,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getGrpcWebTransport(adaptive: AdaptiveRpcTransport): RpcTransport {
|
|
147
|
+
// Access the private fallback field
|
|
148
|
+
return (adaptive as unknown as { fallback: RpcTransport }).fallback;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Tests
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
describe("AdaptiveRpcTransport", () => {
|
|
156
|
+
const method = createMethodInfo();
|
|
157
|
+
const input: MockMsg = { value: "hello" };
|
|
158
|
+
const options: RpcOptions = { meta: {} };
|
|
159
|
+
|
|
160
|
+
beforeEach(() => {
|
|
161
|
+
vi.useFakeTimers();
|
|
162
|
+
vi.clearAllMocks();
|
|
163
|
+
mockWtConnect.mockResolvedValue(undefined);
|
|
164
|
+
mockWtTransportInstance = createMockTransport();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
afterEach(() => {
|
|
168
|
+
vi.useRealTimers();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("when WebTransport is unavailable (no URL)", () => {
|
|
172
|
+
it("always uses gRPC-web", async () => {
|
|
173
|
+
const opts: AdaptiveTransportOptions = {
|
|
174
|
+
grpcWebUrl: "https://grpc.sid.example.com",
|
|
175
|
+
// no webTransportUrl
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const adaptive = new AdaptiveRpcTransport(opts);
|
|
179
|
+
await adaptive.init();
|
|
180
|
+
|
|
181
|
+
expect(adaptive.activeTransport).toBe("grpc-web");
|
|
182
|
+
|
|
183
|
+
const fallback = getGrpcWebTransport(adaptive);
|
|
184
|
+
const mockCall = resolvedUnaryCall(method, input, { value: "ok" });
|
|
185
|
+
(fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(mockCall);
|
|
186
|
+
|
|
187
|
+
adaptive.unary(method, input, options);
|
|
188
|
+
|
|
189
|
+
expect(fallback.unary).toHaveBeenCalledWith(method, input, options);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("when WebTransport is available", () => {
|
|
194
|
+
let adaptive: AdaptiveRpcTransport;
|
|
195
|
+
|
|
196
|
+
beforeEach(async () => {
|
|
197
|
+
// Make WebTransport global exist so the constructor enables WT
|
|
198
|
+
(globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
|
|
199
|
+
|
|
200
|
+
const opts: AdaptiveTransportOptions = {
|
|
201
|
+
grpcWebUrl: "https://grpc.sid.example.com",
|
|
202
|
+
webTransportUrl: "https://wt.sid.example.com",
|
|
203
|
+
reconnectDelay: 1000,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
adaptive = new AdaptiveRpcTransport(opts);
|
|
207
|
+
await adaptive.init();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
afterEach(() => {
|
|
211
|
+
adaptive.close();
|
|
212
|
+
delete (globalThis as Record<string, unknown>).WebTransport;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("uses WebTransport as primary transport", () => {
|
|
216
|
+
expect(adaptive.activeTransport).toBe("webtransport");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("sends unary calls through WebTransport primary", async () => {
|
|
220
|
+
const mockCall = resolvedUnaryCall(method, input, { value: "wt-ok" });
|
|
221
|
+
(
|
|
222
|
+
mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
|
|
223
|
+
).mockReturnValue(mockCall);
|
|
224
|
+
|
|
225
|
+
const call = adaptive.unary(method, input, options);
|
|
226
|
+
const response = await call.response;
|
|
227
|
+
|
|
228
|
+
expect(response).toEqual({ value: "wt-ok" });
|
|
229
|
+
expect(mockWtTransportInstance.unary).toHaveBeenCalledWith(
|
|
230
|
+
method,
|
|
231
|
+
input,
|
|
232
|
+
options,
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("fails over to gRPC-web on transport error (UNAVAILABLE)", async () => {
|
|
237
|
+
// Primary rejects with UNAVAILABLE (transport error)
|
|
238
|
+
const transportErr = new RpcError("connection lost", "UNAVAILABLE");
|
|
239
|
+
const failedCall = rejectedUnaryCall(method, input, transportErr);
|
|
240
|
+
(
|
|
241
|
+
mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
|
|
242
|
+
).mockReturnValue(failedCall);
|
|
243
|
+
|
|
244
|
+
// Fallback succeeds
|
|
245
|
+
const fallback = getGrpcWebTransport(adaptive);
|
|
246
|
+
const successCall = resolvedUnaryCall(method, input, {
|
|
247
|
+
value: "fallback-ok",
|
|
248
|
+
});
|
|
249
|
+
(fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(successCall);
|
|
250
|
+
|
|
251
|
+
const call = adaptive.unary(method, input, options);
|
|
252
|
+
const response = await call.response;
|
|
253
|
+
|
|
254
|
+
expect(response).toEqual({ value: "fallback-ok" });
|
|
255
|
+
expect(fallback.unary).toHaveBeenCalledWith(method, input, options);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("does NOT failover on application error (NOT_FOUND)", async () => {
|
|
259
|
+
const appErr = new RpcError("not found", "NOT_FOUND");
|
|
260
|
+
const failedCall = rejectedUnaryCall(method, input, appErr);
|
|
261
|
+
(
|
|
262
|
+
mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
|
|
263
|
+
).mockReturnValue(failedCall);
|
|
264
|
+
|
|
265
|
+
const fallback = getGrpcWebTransport(adaptive);
|
|
266
|
+
|
|
267
|
+
const call = adaptive.unary(method, input, options);
|
|
268
|
+
|
|
269
|
+
await expect(call.response).rejects.toThrow("not found");
|
|
270
|
+
expect(fallback.unary).not.toHaveBeenCalled();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("does NOT failover on PERMISSION_DENIED", async () => {
|
|
274
|
+
const appErr = new RpcError("forbidden", "PERMISSION_DENIED");
|
|
275
|
+
const failedCall = rejectedUnaryCall(method, input, appErr);
|
|
276
|
+
(
|
|
277
|
+
mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
|
|
278
|
+
).mockReturnValue(failedCall);
|
|
279
|
+
|
|
280
|
+
const fallback = getGrpcWebTransport(adaptive);
|
|
281
|
+
|
|
282
|
+
const call = adaptive.unary(method, input, options);
|
|
283
|
+
|
|
284
|
+
await expect(call.response).rejects.toThrow("forbidden");
|
|
285
|
+
expect(fallback.unary).not.toHaveBeenCalled();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("schedules reconnect after failover", async () => {
|
|
289
|
+
const transportErr = new RpcError("connection lost", "UNAVAILABLE");
|
|
290
|
+
const failedCall = rejectedUnaryCall(method, input, transportErr);
|
|
291
|
+
(
|
|
292
|
+
mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
|
|
293
|
+
).mockReturnValue(failedCall);
|
|
294
|
+
|
|
295
|
+
const fallback = getGrpcWebTransport(adaptive);
|
|
296
|
+
const successCall = resolvedUnaryCall(method, input, { value: "ok" });
|
|
297
|
+
(fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(successCall);
|
|
298
|
+
|
|
299
|
+
const call = adaptive.unary(method, input, options);
|
|
300
|
+
await call.response;
|
|
301
|
+
|
|
302
|
+
// After failover, transport should be grpc-web
|
|
303
|
+
expect(adaptive.activeTransport).toBe("grpc-web");
|
|
304
|
+
|
|
305
|
+
// Reconnect is scheduled — advance timer
|
|
306
|
+
mockWtConnect.mockResolvedValue(undefined);
|
|
307
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
308
|
+
|
|
309
|
+
// After successful reconnect, primary should be available again
|
|
310
|
+
expect(adaptive.activeTransport).toBe("webtransport");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("retries reconnect if reconnect attempt fails", async () => {
|
|
314
|
+
const transportErr = new RpcError("connection lost", "UNAVAILABLE");
|
|
315
|
+
const failedCall = rejectedUnaryCall(method, input, transportErr);
|
|
316
|
+
(
|
|
317
|
+
mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
|
|
318
|
+
).mockReturnValue(failedCall);
|
|
319
|
+
|
|
320
|
+
const fallback = getGrpcWebTransport(adaptive);
|
|
321
|
+
const successCall = resolvedUnaryCall(method, input, { value: "ok" });
|
|
322
|
+
(fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(successCall);
|
|
323
|
+
|
|
324
|
+
const call = adaptive.unary(method, input, options);
|
|
325
|
+
await call.response;
|
|
326
|
+
|
|
327
|
+
// First reconnect attempt fails
|
|
328
|
+
mockWtConnect.mockRejectedValueOnce(new Error("still down"));
|
|
329
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
330
|
+
|
|
331
|
+
// Still on gRPC-web
|
|
332
|
+
expect(adaptive.activeTransport).toBe("grpc-web");
|
|
333
|
+
|
|
334
|
+
// Second reconnect succeeds
|
|
335
|
+
mockWtConnect.mockResolvedValue(undefined);
|
|
336
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
337
|
+
|
|
338
|
+
expect(adaptive.activeTransport).toBe("webtransport");
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe("activeTransport getter", () => {
|
|
343
|
+
it("returns grpc-web when WebTransport URL not provided", async () => {
|
|
344
|
+
const adaptive = new AdaptiveRpcTransport({
|
|
345
|
+
grpcWebUrl: "https://grpc.sid.example.com",
|
|
346
|
+
});
|
|
347
|
+
await adaptive.init();
|
|
348
|
+
|
|
349
|
+
expect(adaptive.activeTransport).toBe("grpc-web");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("returns webtransport after successful init", async () => {
|
|
353
|
+
(globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
|
|
354
|
+
|
|
355
|
+
const adaptive = new AdaptiveRpcTransport({
|
|
356
|
+
grpcWebUrl: "https://grpc.sid.example.com",
|
|
357
|
+
webTransportUrl: "https://wt.sid.example.com",
|
|
358
|
+
});
|
|
359
|
+
await adaptive.init();
|
|
360
|
+
|
|
361
|
+
expect(adaptive.activeTransport).toBe("webtransport");
|
|
362
|
+
|
|
363
|
+
adaptive.close();
|
|
364
|
+
delete (globalThis as Record<string, unknown>).WebTransport;
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("returns grpc-web after init failure", async () => {
|
|
368
|
+
(globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
|
|
369
|
+
mockWtConnect.mockRejectedValueOnce(new Error("connect failed"));
|
|
370
|
+
|
|
371
|
+
const adaptive = new AdaptiveRpcTransport({
|
|
372
|
+
grpcWebUrl: "https://grpc.sid.example.com",
|
|
373
|
+
webTransportUrl: "https://wt.sid.example.com",
|
|
374
|
+
});
|
|
375
|
+
await adaptive.init();
|
|
376
|
+
|
|
377
|
+
expect(adaptive.activeTransport).toBe("grpc-web");
|
|
378
|
+
|
|
379
|
+
adaptive.close();
|
|
380
|
+
delete (globalThis as Record<string, unknown>).WebTransport;
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("schedules reconnect after init() failure", async () => {
|
|
384
|
+
(globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
|
|
385
|
+
mockWtConnect.mockRejectedValueOnce(new Error("connect failed"));
|
|
386
|
+
|
|
387
|
+
const adaptive = new AdaptiveRpcTransport({
|
|
388
|
+
grpcWebUrl: "https://grpc.sid.example.com",
|
|
389
|
+
webTransportUrl: "https://wt.sid.example.com",
|
|
390
|
+
reconnectDelay: 2000,
|
|
391
|
+
});
|
|
392
|
+
await adaptive.init();
|
|
393
|
+
|
|
394
|
+
expect(adaptive.activeTransport).toBe("grpc-web");
|
|
395
|
+
mockWtConnect.mockResolvedValue(undefined);
|
|
396
|
+
|
|
397
|
+
// Reconnect fires after delay
|
|
398
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
399
|
+
expect(mockWtConnect).toHaveBeenCalledTimes(2); // init fail + reconnect
|
|
400
|
+
expect(adaptive.activeTransport).toBe("webtransport");
|
|
401
|
+
|
|
402
|
+
adaptive.close();
|
|
403
|
+
delete (globalThis as Record<string, unknown>).WebTransport;
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
describe("onTransportChange callback", () => {
|
|
408
|
+
it("fires when transport changes during failover", async () => {
|
|
409
|
+
(globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
|
|
410
|
+
|
|
411
|
+
const onChange = vi.fn();
|
|
412
|
+
const adaptive = new AdaptiveRpcTransport({
|
|
413
|
+
grpcWebUrl: "https://grpc.sid.example.com",
|
|
414
|
+
webTransportUrl: "https://wt.sid.example.com",
|
|
415
|
+
reconnectDelay: 1000,
|
|
416
|
+
onTransportChange: onChange,
|
|
417
|
+
});
|
|
418
|
+
await adaptive.init();
|
|
419
|
+
|
|
420
|
+
// init() calls onTransportChange with "webtransport"
|
|
421
|
+
expect(onChange).toHaveBeenCalledWith("webtransport");
|
|
422
|
+
|
|
423
|
+
// Trigger failover
|
|
424
|
+
const transportErr = new RpcError("down", "UNAVAILABLE");
|
|
425
|
+
const failedCall = rejectedUnaryCall(method, input, transportErr);
|
|
426
|
+
(
|
|
427
|
+
mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
|
|
428
|
+
).mockReturnValue(failedCall);
|
|
429
|
+
|
|
430
|
+
const fallback = getGrpcWebTransport(adaptive);
|
|
431
|
+
const successCall = resolvedUnaryCall(method, input, { value: "ok" });
|
|
432
|
+
(fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(successCall);
|
|
433
|
+
|
|
434
|
+
const call = adaptive.unary(method, input, options);
|
|
435
|
+
await call.response;
|
|
436
|
+
|
|
437
|
+
expect(onChange).toHaveBeenCalledWith("grpc-web");
|
|
438
|
+
|
|
439
|
+
adaptive.close();
|
|
440
|
+
delete (globalThis as Record<string, unknown>).WebTransport;
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
describe("close()", () => {
|
|
445
|
+
it("cancels pending reconnect timer", async () => {
|
|
446
|
+
(globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
|
|
447
|
+
|
|
448
|
+
const adaptive = new AdaptiveRpcTransport({
|
|
449
|
+
grpcWebUrl: "https://grpc.sid.example.com",
|
|
450
|
+
webTransportUrl: "https://wt.sid.example.com",
|
|
451
|
+
reconnectDelay: 5000,
|
|
452
|
+
});
|
|
453
|
+
await adaptive.init();
|
|
454
|
+
|
|
455
|
+
// Trigger failover to schedule reconnect
|
|
456
|
+
const transportErr = new RpcError("down", "UNAVAILABLE");
|
|
457
|
+
const failedCall = rejectedUnaryCall(method, input, transportErr);
|
|
458
|
+
(
|
|
459
|
+
mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
|
|
460
|
+
).mockReturnValue(failedCall);
|
|
461
|
+
|
|
462
|
+
const fallback = getGrpcWebTransport(adaptive);
|
|
463
|
+
const successCall = resolvedUnaryCall(method, input, { value: "ok" });
|
|
464
|
+
(fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(successCall);
|
|
465
|
+
|
|
466
|
+
const call = adaptive.unary(method, input, options);
|
|
467
|
+
await call.response;
|
|
468
|
+
|
|
469
|
+
// Close before reconnect fires
|
|
470
|
+
adaptive.close();
|
|
471
|
+
|
|
472
|
+
// Advance past reconnect delay — should NOT attempt reconnect
|
|
473
|
+
mockWtConnect.mockClear();
|
|
474
|
+
await vi.advanceTimersByTimeAsync(10000);
|
|
475
|
+
|
|
476
|
+
// connect should not be called again (only the init() call)
|
|
477
|
+
expect(mockWtConnect).not.toHaveBeenCalled();
|
|
478
|
+
|
|
479
|
+
delete (globalThis as Record<string, unknown>).WebTransport;
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
describe("clientStreaming / duplex", () => {
|
|
484
|
+
it("clientStreaming throws RpcError", () => {
|
|
485
|
+
const adaptive = new AdaptiveRpcTransport({
|
|
486
|
+
grpcWebUrl: "https://grpc.sid.example.com",
|
|
487
|
+
});
|
|
488
|
+
expect(() => adaptive.clientStreaming(method, options)).toThrow(RpcError);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("duplex throws RpcError", () => {
|
|
492
|
+
const adaptive = new AdaptiveRpcTransport({
|
|
493
|
+
grpcWebUrl: "https://grpc.sid.example.com",
|
|
494
|
+
});
|
|
495
|
+
expect(() => adaptive.duplex(method, options)).toThrow(RpcError);
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
describe("failover triggers (all transport error codes)", () => {
|
|
500
|
+
it("fails over on CANCELLED", async () => {
|
|
501
|
+
(globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
|
|
502
|
+
const adaptive = new AdaptiveRpcTransport({
|
|
503
|
+
grpcWebUrl: "https://grpc.sid.example.com",
|
|
504
|
+
webTransportUrl: "https://wt.sid.example.com",
|
|
505
|
+
});
|
|
506
|
+
await adaptive.init();
|
|
507
|
+
|
|
508
|
+
const err = new RpcError("cancelled", "CANCELLED");
|
|
509
|
+
(
|
|
510
|
+
mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
|
|
511
|
+
).mockReturnValue(rejectedUnaryCall(method, input, err));
|
|
512
|
+
|
|
513
|
+
const fallback = getGrpcWebTransport(adaptive);
|
|
514
|
+
(fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(
|
|
515
|
+
resolvedUnaryCall(method, input, { value: "ok" }),
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
await adaptive.unary(method, input, options).response;
|
|
519
|
+
expect(adaptive.activeTransport).toBe("grpc-web");
|
|
520
|
+
|
|
521
|
+
adaptive.close();
|
|
522
|
+
delete (globalThis as Record<string, unknown>).WebTransport;
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("fails over on UNKNOWN", async () => {
|
|
526
|
+
(globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
|
|
527
|
+
const adaptive = new AdaptiveRpcTransport({
|
|
528
|
+
grpcWebUrl: "https://grpc.sid.example.com",
|
|
529
|
+
webTransportUrl: "https://wt.sid.example.com",
|
|
530
|
+
});
|
|
531
|
+
await adaptive.init();
|
|
532
|
+
|
|
533
|
+
const err = new RpcError("unknown", "UNKNOWN");
|
|
534
|
+
(
|
|
535
|
+
mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
|
|
536
|
+
).mockReturnValue(rejectedUnaryCall(method, input, err));
|
|
537
|
+
|
|
538
|
+
const fallback = getGrpcWebTransport(adaptive);
|
|
539
|
+
(fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(
|
|
540
|
+
resolvedUnaryCall(method, input, { value: "ok" }),
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
await adaptive.unary(method, input, options).response;
|
|
544
|
+
expect(adaptive.activeTransport).toBe("grpc-web");
|
|
545
|
+
|
|
546
|
+
adaptive.close();
|
|
547
|
+
delete (globalThis as Record<string, unknown>).WebTransport;
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("fails over on non-RpcError (plain Error from stream failure)", async () => {
|
|
551
|
+
(globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
|
|
552
|
+
const adaptive = new AdaptiveRpcTransport({
|
|
553
|
+
grpcWebUrl: "https://grpc.sid.example.com",
|
|
554
|
+
webTransportUrl: "https://wt.sid.example.com",
|
|
555
|
+
});
|
|
556
|
+
await adaptive.init();
|
|
557
|
+
|
|
558
|
+
// Simulate a plain Error (not RpcError) from the primary transport
|
|
559
|
+
// This happens when WebTransport stream throws a non-gRPC error
|
|
560
|
+
const defH = new Deferred<RpcMetadata>();
|
|
561
|
+
const defM = new Deferred<MockMsg>();
|
|
562
|
+
const defS = new Deferred<RpcStatus>();
|
|
563
|
+
const defT = new Deferred<RpcMetadata>();
|
|
564
|
+
const plainErr = new Error("stream aborted");
|
|
565
|
+
defH.reject(plainErr);
|
|
566
|
+
defM.reject(plainErr);
|
|
567
|
+
defS.reject(plainErr);
|
|
568
|
+
defT.reject(plainErr);
|
|
569
|
+
const crashedCall = new UnaryCall(
|
|
570
|
+
method,
|
|
571
|
+
{},
|
|
572
|
+
input,
|
|
573
|
+
defH.promise,
|
|
574
|
+
defM.promise,
|
|
575
|
+
defS.promise,
|
|
576
|
+
defT.promise,
|
|
577
|
+
);
|
|
578
|
+
(
|
|
579
|
+
mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
|
|
580
|
+
).mockReturnValue(crashedCall);
|
|
581
|
+
|
|
582
|
+
const fallback = getGrpcWebTransport(adaptive);
|
|
583
|
+
(fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(
|
|
584
|
+
resolvedUnaryCall(method, input, { value: "fallback-ok" }),
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
const response = await adaptive.unary(method, input, options).response;
|
|
588
|
+
expect(response).toEqual({ value: "fallback-ok" });
|
|
589
|
+
expect(adaptive.activeTransport).toBe("grpc-web");
|
|
590
|
+
|
|
591
|
+
adaptive.close();
|
|
592
|
+
delete (globalThis as Record<string, unknown>).WebTransport;
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("propagates error when fallback also fails", async () => {
|
|
596
|
+
(globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
|
|
597
|
+
const adaptive = new AdaptiveRpcTransport({
|
|
598
|
+
grpcWebUrl: "https://grpc.sid.example.com",
|
|
599
|
+
webTransportUrl: "https://wt.sid.example.com",
|
|
600
|
+
});
|
|
601
|
+
await adaptive.init();
|
|
602
|
+
|
|
603
|
+
// Primary fails with transport error
|
|
604
|
+
const transportErr = new RpcError("primary down", "UNAVAILABLE");
|
|
605
|
+
(
|
|
606
|
+
mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
|
|
607
|
+
).mockReturnValue(rejectedUnaryCall(method, input, transportErr));
|
|
608
|
+
|
|
609
|
+
// Fallback also fails
|
|
610
|
+
const fallbackErr = new RpcError("fallback down", "UNAVAILABLE");
|
|
611
|
+
const fallback = getGrpcWebTransport(adaptive);
|
|
612
|
+
(fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(
|
|
613
|
+
rejectedUnaryCall(method, input, fallbackErr),
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
const err = await adaptive
|
|
617
|
+
.unary(method, input, options)
|
|
618
|
+
.response.catch((e: unknown) => e);
|
|
619
|
+
expect((err as RpcError).message).toBe("fallback down");
|
|
620
|
+
|
|
621
|
+
adaptive.close();
|
|
622
|
+
delete (globalThis as Record<string, unknown>).WebTransport;
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
describe("scheduleReconnect idempotency", () => {
|
|
627
|
+
it("does not schedule multiple reconnect timers on concurrent failovers", async () => {
|
|
628
|
+
(globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
|
|
629
|
+
const adaptive = new AdaptiveRpcTransport({
|
|
630
|
+
grpcWebUrl: "https://grpc.sid.example.com",
|
|
631
|
+
webTransportUrl: "https://wt.sid.example.com",
|
|
632
|
+
reconnectDelay: 5000,
|
|
633
|
+
});
|
|
634
|
+
await adaptive.init();
|
|
635
|
+
|
|
636
|
+
const transportErr = new RpcError("down", "UNAVAILABLE");
|
|
637
|
+
const fallback = getGrpcWebTransport(adaptive);
|
|
638
|
+
|
|
639
|
+
// Two concurrent unary calls both fail with UNAVAILABLE
|
|
640
|
+
(
|
|
641
|
+
mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
|
|
642
|
+
).mockReturnValue(rejectedUnaryCall(method, input, transportErr));
|
|
643
|
+
(fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(
|
|
644
|
+
resolvedUnaryCall(method, input, { value: "ok" }),
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
await Promise.all([
|
|
648
|
+
adaptive.unary(method, input, options).response,
|
|
649
|
+
adaptive.unary(method, input, options).response,
|
|
650
|
+
]);
|
|
651
|
+
|
|
652
|
+
// Advance past the reconnect delay — connect should fire exactly once
|
|
653
|
+
mockWtConnect.mockResolvedValue(undefined);
|
|
654
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
655
|
+
|
|
656
|
+
// Only one reconnect attempt, not two (init=1, reconnect=1 — second failover is no-op)
|
|
657
|
+
expect(mockWtConnect).toHaveBeenCalledTimes(2);
|
|
658
|
+
|
|
659
|
+
adaptive.close();
|
|
660
|
+
delete (globalThis as Record<string, unknown>).WebTransport;
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
describe("onTransportChange on reconnect", () => {
|
|
665
|
+
it("fires 'webtransport' when reconnect restores primary", async () => {
|
|
666
|
+
(globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
|
|
667
|
+
|
|
668
|
+
const onChange = vi.fn();
|
|
669
|
+
const adaptive = new AdaptiveRpcTransport({
|
|
670
|
+
grpcWebUrl: "https://grpc.sid.example.com",
|
|
671
|
+
webTransportUrl: "https://wt.sid.example.com",
|
|
672
|
+
reconnectDelay: 1000,
|
|
673
|
+
onTransportChange: onChange,
|
|
674
|
+
});
|
|
675
|
+
await adaptive.init();
|
|
676
|
+
|
|
677
|
+
// Trigger failover
|
|
678
|
+
const transportErr = new RpcError("down", "UNAVAILABLE");
|
|
679
|
+
(
|
|
680
|
+
mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
|
|
681
|
+
).mockReturnValue(rejectedUnaryCall(method, input, transportErr));
|
|
682
|
+
const fallback = getGrpcWebTransport(adaptive);
|
|
683
|
+
(fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(
|
|
684
|
+
resolvedUnaryCall(method, input, { value: "ok" }),
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
await adaptive.unary(method, input, options).response;
|
|
688
|
+
expect(onChange).toHaveBeenLastCalledWith("grpc-web");
|
|
689
|
+
|
|
690
|
+
// Reconnect
|
|
691
|
+
mockWtConnect.mockResolvedValue(undefined);
|
|
692
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
693
|
+
expect(onChange).toHaveBeenLastCalledWith("webtransport");
|
|
694
|
+
|
|
695
|
+
adaptive.close();
|
|
696
|
+
delete (globalThis as Record<string, unknown>).WebTransport;
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
describe("serverStreaming", () => {
|
|
701
|
+
it("always routes to gRPC-web fallback (WebTransport does not support streaming)", async () => {
|
|
702
|
+
(globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
|
|
703
|
+
const adaptive = new AdaptiveRpcTransport({
|
|
704
|
+
grpcWebUrl: "https://grpc.sid.example.com",
|
|
705
|
+
webTransportUrl: "https://wt.sid.example.com",
|
|
706
|
+
});
|
|
707
|
+
await adaptive.init();
|
|
708
|
+
|
|
709
|
+
const fallback = getGrpcWebTransport(adaptive);
|
|
710
|
+
const mockStream = {} as ReturnType<RpcTransport["serverStreaming"]>;
|
|
711
|
+
(fallback.serverStreaming as ReturnType<typeof vi.fn>).mockReturnValue(
|
|
712
|
+
mockStream,
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
const result = adaptive.serverStreaming(method, input, options);
|
|
716
|
+
|
|
717
|
+
expect(fallback.serverStreaming).toHaveBeenCalledWith(
|
|
718
|
+
method,
|
|
719
|
+
input,
|
|
720
|
+
options,
|
|
721
|
+
);
|
|
722
|
+
expect(result).toBe(mockStream);
|
|
723
|
+
|
|
724
|
+
adaptive.close();
|
|
725
|
+
delete (globalThis as Record<string, unknown>).WebTransport;
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
});
|