statewire 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/client/connection.d.ts +5 -2
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +54 -19
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts +2 -2
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +5 -4
- package/dist/client/core.js.map +1 -1
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +6 -2
- package/dist/client/session.js.map +1 -1
- package/dist/host/http.d.ts +2 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +36 -3
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +11 -2
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +135 -6
- package/dist/host/socket.js.map +1 -1
- package/dist/host-internal.d.ts +9 -1
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/statewire.d.ts +16 -11
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +2 -2
- package/dist/statewire.js.map +1 -1
- package/dist/transports/sse.d.ts +2 -0
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +4 -0
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/ws.d.ts.map +1 -1
- package/dist/transports/ws.js +4 -0
- package/dist/transports/ws.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +67 -20
- package/src/client/core.ts +4 -3
- package/src/client/session.ts +10 -2
- package/src/host/http.ts +40 -2
- package/src/host/socket.ts +220 -6
- package/src/host-internal.ts +14 -1
- package/src/statewire.ts +23 -10
- package/src/transports/sse.ts +6 -0
- package/src/transports/ws.ts +4 -0
package/src/host/socket.ts
CHANGED
|
@@ -53,6 +53,14 @@ export type Conn = {
|
|
|
53
53
|
lease: string | null;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
type PendingConn = {
|
|
57
|
+
socket: StatewireHostInternal.Socket;
|
|
58
|
+
open: boolean;
|
|
59
|
+
authorizing: boolean;
|
|
60
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
61
|
+
connection: StatewireHostInternal.SocketConnection | null;
|
|
62
|
+
};
|
|
63
|
+
|
|
56
64
|
// ─── frame parsing ──────────────────────────────────────
|
|
57
65
|
|
|
58
66
|
const violation = (reason: string): never => {
|
|
@@ -247,11 +255,13 @@ const cmdOf = (adm: Admission, outcome: CommandsOutcome): StatewireCmd => {
|
|
|
247
255
|
|
|
248
256
|
export type SocketHostState = {
|
|
249
257
|
heartbeatMs: number;
|
|
258
|
+
wsInitTimeoutMs: number;
|
|
250
259
|
clientRecordLimit: number;
|
|
251
260
|
finOnIdle: boolean;
|
|
252
261
|
authorize: StatewireHostInternal.SocketHostOptions["authorize"];
|
|
253
262
|
clients: Map<string, ClientRec>;
|
|
254
263
|
conns: Set<Conn>;
|
|
264
|
+
pendingConns: Set<PendingConn>;
|
|
255
265
|
pendingOps: WireOp[];
|
|
256
266
|
pendingApplied: Map<string, number[]>;
|
|
257
267
|
pendingCmds: Map<string, StatewireCmd[]>;
|
|
@@ -267,6 +277,7 @@ export type SocketHostState = {
|
|
|
267
277
|
connect: (
|
|
268
278
|
clientId: string,
|
|
269
279
|
socket: StatewireHostInternal.Socket,
|
|
280
|
+
options?: StatewireHostInternal.SocketConnectOptions,
|
|
270
281
|
) => StatewireHostInternal.SocketConnection | null;
|
|
271
282
|
finish: (fin: StatewireHost.Fin) => void;
|
|
272
283
|
stream: (request: Request) => Promise<Response>;
|
|
@@ -667,6 +678,198 @@ const message = (h: SocketHostState, conn: Conn, data: string) => {
|
|
|
667
678
|
}
|
|
668
679
|
};
|
|
669
680
|
|
|
681
|
+
const closePending = (
|
|
682
|
+
h: SocketHostState,
|
|
683
|
+
pending: PendingConn,
|
|
684
|
+
code?: number,
|
|
685
|
+
reason?: string,
|
|
686
|
+
) => {
|
|
687
|
+
if (!pending.open) return;
|
|
688
|
+
pending.open = false;
|
|
689
|
+
h.pendingConns.delete(pending);
|
|
690
|
+
if (pending.timer !== null) clearTimeout(pending.timer);
|
|
691
|
+
try {
|
|
692
|
+
pending.socket.close(code, reason);
|
|
693
|
+
} catch {
|
|
694
|
+
/* peer already gone */
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const sendPending = (pending: PendingConn, data: string): boolean => {
|
|
699
|
+
if (!pending.open) return false;
|
|
700
|
+
try {
|
|
701
|
+
pending.socket.send(data);
|
|
702
|
+
return true;
|
|
703
|
+
} catch {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const rejectPendingAuthorization = (
|
|
709
|
+
h: SocketHostState,
|
|
710
|
+
pending: PendingConn,
|
|
711
|
+
error: unknown,
|
|
712
|
+
) => {
|
|
713
|
+
const response = error instanceof Response ? error : null;
|
|
714
|
+
const unauthorized = response?.status === 401 || response?.status === 403;
|
|
715
|
+
const status = response?.status;
|
|
716
|
+
const message = unauthorized
|
|
717
|
+
? "unauthorized"
|
|
718
|
+
: response?.statusText ||
|
|
719
|
+
(status === undefined ? "authorization failed" : `HTTP ${status}`);
|
|
720
|
+
sendPending(
|
|
721
|
+
pending,
|
|
722
|
+
JSON.stringify({
|
|
723
|
+
fin: {
|
|
724
|
+
reason: unauthorized ? "unauthorized" : "error",
|
|
725
|
+
message,
|
|
726
|
+
...(status !== undefined && { status }),
|
|
727
|
+
},
|
|
728
|
+
}),
|
|
729
|
+
);
|
|
730
|
+
closePending(h, pending, unauthorized ? 1008 : 1011, message);
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
const normalizedHeaders = (
|
|
734
|
+
headers: Record<string, string>,
|
|
735
|
+
): Record<string, string> => Object.fromEntries(new Headers(headers));
|
|
736
|
+
|
|
737
|
+
const connectAuthorized = (
|
|
738
|
+
h: SocketHostState,
|
|
739
|
+
clientId: string,
|
|
740
|
+
socket: StatewireHostInternal.Socket,
|
|
741
|
+
options: StatewireHostInternal.SocketConnectOptions,
|
|
742
|
+
): StatewireHostInternal.SocketConnection | null => {
|
|
743
|
+
const authorize = h.authorize;
|
|
744
|
+
if (authorize === undefined || options.authorized === true) {
|
|
745
|
+
const registration = register(
|
|
746
|
+
h,
|
|
747
|
+
clientId,
|
|
748
|
+
socket,
|
|
749
|
+
options.authorized === true,
|
|
750
|
+
);
|
|
751
|
+
return registration?.connection ?? null;
|
|
752
|
+
}
|
|
753
|
+
const refuse = (code: number, reason: string) => {
|
|
754
|
+
try {
|
|
755
|
+
socket.close(code, reason);
|
|
756
|
+
} catch {
|
|
757
|
+
/* peer already gone */
|
|
758
|
+
}
|
|
759
|
+
return null;
|
|
760
|
+
};
|
|
761
|
+
if (h.disposed) return refuse(1001, "disposed");
|
|
762
|
+
if (!CLIENT_ID_RE.test(clientId)) return refuse(1008, "invalid client id");
|
|
763
|
+
flush(h);
|
|
764
|
+
if (h.finished !== null) {
|
|
765
|
+
try {
|
|
766
|
+
socket.send(JSON.stringify({ fin: h.finished }));
|
|
767
|
+
} catch {
|
|
768
|
+
/* peer already gone */
|
|
769
|
+
}
|
|
770
|
+
return refuse(1000, "finished");
|
|
771
|
+
}
|
|
772
|
+
const pending: PendingConn = {
|
|
773
|
+
socket,
|
|
774
|
+
open: true,
|
|
775
|
+
authorizing: false,
|
|
776
|
+
timer: null,
|
|
777
|
+
connection: null,
|
|
778
|
+
};
|
|
779
|
+
h.pendingConns.add(pending);
|
|
780
|
+
pending.timer = setTimeout(
|
|
781
|
+
() =>
|
|
782
|
+
rejectPendingAuthorization(
|
|
783
|
+
h,
|
|
784
|
+
pending,
|
|
785
|
+
new Response(null, { status: 401 }),
|
|
786
|
+
),
|
|
787
|
+
h.wsInitTimeoutMs,
|
|
788
|
+
);
|
|
789
|
+
(pending.timer as unknown as { unref?: () => void }).unref?.();
|
|
790
|
+
return {
|
|
791
|
+
clientId,
|
|
792
|
+
message: (data) => {
|
|
793
|
+
if (pending.connection !== null) {
|
|
794
|
+
pending.connection.message(data);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (!pending.open || h.disposed || h.finished !== null) return;
|
|
798
|
+
if (pending.authorizing) {
|
|
799
|
+
closePending(h, pending, 1008, "authorization pending");
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
let frame: ParsedFrame;
|
|
803
|
+
try {
|
|
804
|
+
frame = parseFrame(data, true);
|
|
805
|
+
} catch (error) {
|
|
806
|
+
const reason = violationReason(error);
|
|
807
|
+
if (reason === null) throw error;
|
|
808
|
+
sendPending(
|
|
809
|
+
pending,
|
|
810
|
+
JSON.stringify({
|
|
811
|
+
fin: {
|
|
812
|
+
reason: "error",
|
|
813
|
+
message: `protocol violation: ${reason}`,
|
|
814
|
+
},
|
|
815
|
+
}),
|
|
816
|
+
);
|
|
817
|
+
closePending(h, pending, 1008, reason);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
if (frame.headers === null) {
|
|
821
|
+
rejectPendingAuthorization(
|
|
822
|
+
h,
|
|
823
|
+
pending,
|
|
824
|
+
new Response(null, { status: 401 }),
|
|
825
|
+
);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
let headers: Record<string, string>;
|
|
829
|
+
try {
|
|
830
|
+
headers = normalizedHeaders(frame.headers);
|
|
831
|
+
} catch {
|
|
832
|
+
sendPending(
|
|
833
|
+
pending,
|
|
834
|
+
JSON.stringify({
|
|
835
|
+
fin: {
|
|
836
|
+
reason: "error",
|
|
837
|
+
message: "protocol violation: malformed headers",
|
|
838
|
+
},
|
|
839
|
+
}),
|
|
840
|
+
);
|
|
841
|
+
closePending(h, pending, 1008, "malformed headers");
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
pending.authorizing = true;
|
|
845
|
+
Promise.resolve()
|
|
846
|
+
.then(() => authorize({ headers }))
|
|
847
|
+
.then(() => {
|
|
848
|
+
if (!pending.open || h.disposed || h.finished !== null) return;
|
|
849
|
+
options.onAuthorized?.();
|
|
850
|
+
pending.open = false;
|
|
851
|
+
h.pendingConns.delete(pending);
|
|
852
|
+
if (pending.timer !== null) clearTimeout(pending.timer);
|
|
853
|
+
const registration = register(h, clientId, socket, true);
|
|
854
|
+
if (registration === null) return;
|
|
855
|
+
pending.connection = registration.connection;
|
|
856
|
+
if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
|
|
857
|
+
const admission = admitFrame(h, registration.rec, frame);
|
|
858
|
+
if (admission.type === "refused") {
|
|
859
|
+
sendTo(h, registration.conn, JSON.stringify(admission.syn));
|
|
860
|
+
}
|
|
861
|
+
})
|
|
862
|
+
.catch((error: unknown) => {
|
|
863
|
+
if (pending.open) rejectPendingAuthorization(h, pending, error);
|
|
864
|
+
});
|
|
865
|
+
},
|
|
866
|
+
close: () => {
|
|
867
|
+
if (pending.connection !== null) pending.connection.close();
|
|
868
|
+
else closePending(h, pending);
|
|
869
|
+
},
|
|
870
|
+
};
|
|
871
|
+
};
|
|
872
|
+
|
|
670
873
|
const gcClients = (h: SocketHostState) => {
|
|
671
874
|
let excess = h.clients.size - h.clientRecordLimit;
|
|
672
875
|
if (excess <= 0) return;
|
|
@@ -690,6 +893,7 @@ export const register = (
|
|
|
690
893
|
h: SocketHostState,
|
|
691
894
|
clientId: string,
|
|
692
895
|
socket: StatewireHostInternal.Socket,
|
|
896
|
+
sawFrame = false,
|
|
693
897
|
): Registration | null => {
|
|
694
898
|
const refuse = (code: number, reason: string) => {
|
|
695
899
|
try {
|
|
@@ -732,7 +936,7 @@ export const register = (
|
|
|
732
936
|
clientId,
|
|
733
937
|
socket,
|
|
734
938
|
open: true,
|
|
735
|
-
sawFrame
|
|
939
|
+
sawFrame,
|
|
736
940
|
lastSentAt: 0,
|
|
737
941
|
lease: null,
|
|
738
942
|
};
|
|
@@ -768,14 +972,18 @@ const connect = (
|
|
|
768
972
|
h: SocketHostState,
|
|
769
973
|
clientId: string,
|
|
770
974
|
socket: StatewireHostInternal.Socket,
|
|
975
|
+
options: StatewireHostInternal.SocketConnectOptions = {},
|
|
771
976
|
): StatewireHostInternal.SocketConnection | null => {
|
|
772
|
-
|
|
773
|
-
return registration?.connection ?? null;
|
|
977
|
+
return connectAuthorized(h, clientId, socket, options);
|
|
774
978
|
};
|
|
775
979
|
|
|
776
980
|
const finish = (h: SocketHostState, fin: StatewireHost.Fin) => {
|
|
777
981
|
if (h.finished !== null || h.disposed) return;
|
|
778
982
|
h.finished = fin;
|
|
983
|
+
for (const pending of [...h.pendingConns]) {
|
|
984
|
+
sendPending(pending, JSON.stringify({ fin }));
|
|
985
|
+
closePending(h, pending, 1000, fin.reason);
|
|
986
|
+
}
|
|
779
987
|
flush(h, fin);
|
|
780
988
|
};
|
|
781
989
|
|
|
@@ -803,11 +1011,13 @@ const useSocketHost = (
|
|
|
803
1011
|
const [h] = useState((): SocketHostState => {
|
|
804
1012
|
const host: SocketHostState = {
|
|
805
1013
|
heartbeatMs: 15_000,
|
|
1014
|
+
wsInitTimeoutMs: 5_000,
|
|
806
1015
|
clientRecordLimit: CLIENT_RECORD_LIMIT,
|
|
807
1016
|
finOnIdle: false,
|
|
808
1017
|
authorize: undefined,
|
|
809
1018
|
clients: new Map(),
|
|
810
1019
|
conns: new Set(),
|
|
1020
|
+
pendingConns: new Set(),
|
|
811
1021
|
pendingOps: [],
|
|
812
1022
|
pendingApplied: new Map(),
|
|
813
1023
|
pendingCmds: new Map(),
|
|
@@ -823,7 +1033,8 @@ const useSocketHost = (
|
|
|
823
1033
|
host.flushScheduled = false;
|
|
824
1034
|
flush(host);
|
|
825
1035
|
}),
|
|
826
|
-
connect: (clientId, socket) =>
|
|
1036
|
+
connect: (clientId, socket, connectOptions) =>
|
|
1037
|
+
connect(host, clientId, socket, connectOptions),
|
|
827
1038
|
finish: (fin) => finish(host, fin),
|
|
828
1039
|
stream: (request) => serveStream(host, request),
|
|
829
1040
|
frames: (request) => serveFrames(host, request),
|
|
@@ -831,6 +1042,7 @@ const useSocketHost = (
|
|
|
831
1042
|
return host;
|
|
832
1043
|
});
|
|
833
1044
|
setHeartbeatMs(h, options?.heartbeatIntervalMs ?? 15_000);
|
|
1045
|
+
h.wsInitTimeoutMs = options?.wsInitTimeoutMs ?? 5_000;
|
|
834
1046
|
h.clientRecordLimit = options?.clientRecordLimit ?? CLIENT_RECORD_LIMIT;
|
|
835
1047
|
h.finOnIdle = options?.finOnIdle ?? false;
|
|
836
1048
|
h.authorize = options?.authorize;
|
|
@@ -862,8 +1074,10 @@ export const StatewireSocketHost = (
|
|
|
862
1074
|
const root = createTapRoot(SocketHostRoot);
|
|
863
1075
|
let disposed = false;
|
|
864
1076
|
return {
|
|
865
|
-
connect: (clientId, socket) =>
|
|
866
|
-
flushTapSync(() =>
|
|
1077
|
+
connect: (clientId, socket, connectOptions) =>
|
|
1078
|
+
flushTapSync(() =>
|
|
1079
|
+
root.getValue().connect(clientId, socket, connectOptions),
|
|
1080
|
+
),
|
|
867
1081
|
stream: (request) => flushTapSync(() => root.getValue().stream(request)),
|
|
868
1082
|
frames: (request) => flushTapSync(() => root.getValue().frames(request)),
|
|
869
1083
|
finish: (fin) => flushTapSync(() => root.getValue().finish(fin)),
|
package/src/host-internal.ts
CHANGED
|
@@ -14,9 +14,20 @@ export namespace StatewireHostInternal {
|
|
|
14
14
|
close: () => void;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
export type SocketConnectOptions = {
|
|
18
|
+
/** The adapter restored this socket from an already-authorized session. */
|
|
19
|
+
authorized?: boolean;
|
|
20
|
+
/** Persist authorization before the first snapshot is released. */
|
|
21
|
+
onAuthorized?: () => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
17
24
|
export type SocketHost = {
|
|
18
25
|
/** Null means refused: the socket was told (fin and/or close) and is dead. */
|
|
19
|
-
connect: (
|
|
26
|
+
connect: (
|
|
27
|
+
clientId: string,
|
|
28
|
+
socket: Socket,
|
|
29
|
+
options?: SocketConnectOptions,
|
|
30
|
+
) => SocketConnection | null;
|
|
20
31
|
stream: (request: Request) => Promise<Response>;
|
|
21
32
|
frames: (request: Request) => Promise<Response>;
|
|
22
33
|
finish: (fin: StatewireHost.Fin) => void;
|
|
@@ -27,6 +38,8 @@ export namespace StatewireHostInternal {
|
|
|
27
38
|
authorize?: (ctx: {
|
|
28
39
|
headers: Record<string, string>;
|
|
29
40
|
}) => void | Promise<void>;
|
|
41
|
+
/** Time allowed to complete WebSocket initialization and authorization. */
|
|
42
|
+
wsInitTimeoutMs?: number;
|
|
30
43
|
heartbeatIntervalMs?: number;
|
|
31
44
|
/** LRU cap on stored client records; live or in-flight records are exempt. */
|
|
32
45
|
clientRecordLimit?: number;
|
package/src/statewire.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
withKey,
|
|
10
10
|
} from "@assistant-ui/tap";
|
|
11
11
|
import type { ResourceElement } from "@assistant-ui/tap";
|
|
12
|
-
import {
|
|
12
|
+
import { CONNECTED_CONNECTION, useStatewireCore } from "./client/core";
|
|
13
13
|
import type { Statewire } from "./types";
|
|
14
14
|
import type { TERMINAL } from "./client/connection";
|
|
15
15
|
import { SessionStorageContext } from "./client/sessionStore";
|
|
@@ -64,7 +64,7 @@ export const useStatewire = <
|
|
|
64
64
|
core.getFailed,
|
|
65
65
|
core.getFailed,
|
|
66
66
|
);
|
|
67
|
-
const connection = value.connection ??
|
|
67
|
+
const connection = value.connection ?? CONNECTED_CONNECTION;
|
|
68
68
|
|
|
69
69
|
return useMemo(() => {
|
|
70
70
|
const failures = (): NonNullable<typeof value.failures> => {
|
|
@@ -211,20 +211,29 @@ export namespace StatewireClient {
|
|
|
211
211
|
discardFailed: (entry: FailedCommand) => void;
|
|
212
212
|
};
|
|
213
213
|
|
|
214
|
+
/** `cause`/`nextRetryAt`/`lastError` are present only while waiting out the backoff timer; a dial in flight carries `attempt` alone. */
|
|
214
215
|
export type ConnectionState =
|
|
215
|
-
| { status: "connecting" }
|
|
216
|
-
| { status: "live" }
|
|
217
|
-
| { status: "idle" }
|
|
218
216
|
| {
|
|
219
|
-
status: "
|
|
220
|
-
|
|
217
|
+
status: "disconnected";
|
|
218
|
+
reason: "deferred" | "idle" | "gone";
|
|
219
|
+
message?: string;
|
|
220
|
+
payload?: unknown;
|
|
221
|
+
}
|
|
222
|
+
| {
|
|
223
|
+
status: "connecting";
|
|
224
|
+
/** 0 = the episode's first dial, >0 = retry. */
|
|
221
225
|
attempt: number;
|
|
222
|
-
|
|
226
|
+
cause?: "dropped" | "evicted" | "error" | "unauthorized";
|
|
227
|
+
nextRetryAt?: number;
|
|
223
228
|
lastError?: string;
|
|
224
229
|
message?: string;
|
|
225
230
|
}
|
|
226
|
-
| { status: "
|
|
227
|
-
export type Connection = ConnectionState & {
|
|
231
|
+
| { status: "connected" };
|
|
232
|
+
export type Connection = ConnectionState & {
|
|
233
|
+
reconnect: () => void;
|
|
234
|
+
/** True while retrying (`connecting` with `attempt > 0`) or parked `gone`. */
|
|
235
|
+
degraded: boolean;
|
|
236
|
+
};
|
|
228
237
|
export type ConnectionStatus = Connection["status"];
|
|
229
238
|
|
|
230
239
|
/**
|
|
@@ -355,6 +364,8 @@ export namespace StatewireClient {
|
|
|
355
364
|
export type SSEOptions = {
|
|
356
365
|
sessionStore?: SessionStorage | undefined;
|
|
357
366
|
url: string;
|
|
367
|
+
/** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */
|
|
368
|
+
isNew?: boolean;
|
|
358
369
|
headers?: HeadersOption;
|
|
359
370
|
fetch?: typeof globalThis.fetch;
|
|
360
371
|
onSessionLost?: OnSessionLost;
|
|
@@ -366,6 +377,8 @@ export namespace StatewireClient {
|
|
|
366
377
|
export type WSOptions = {
|
|
367
378
|
sessionStore?: SessionStorage | undefined;
|
|
368
379
|
url: string;
|
|
380
|
+
/** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */
|
|
381
|
+
isNew?: boolean;
|
|
369
382
|
headers?: HeadersOption;
|
|
370
383
|
protocols?: string | string[];
|
|
371
384
|
query?:
|
package/src/transports/sse.ts
CHANGED
|
@@ -51,6 +51,8 @@ export type ConnectionState = StatewireClient.ConnectionState;
|
|
|
51
51
|
/** The live wire a remote session drives: connection status plus frame submission. */
|
|
52
52
|
export type RemoteLink = {
|
|
53
53
|
connection: StatewireClient.Connection;
|
|
54
|
+
/** A deferred first attach still waiting; `reconnect()` releases it. */
|
|
55
|
+
parked: boolean;
|
|
54
56
|
send: (frame: StatewireFrame) => Promise<StatewireSubmitResult>;
|
|
55
57
|
};
|
|
56
58
|
|
|
@@ -269,6 +271,7 @@ const useSseLink = (
|
|
|
269
271
|
const [auth] = useState((): AuthState => ({ stale: false }));
|
|
270
272
|
const c = useStatewireConn({
|
|
271
273
|
url: options.url,
|
|
274
|
+
isNew: options.isNew,
|
|
272
275
|
onChange: options.onChange,
|
|
273
276
|
ctx,
|
|
274
277
|
attemptOnce: (c, abort) => attemptStream(c, options, auth, abort),
|
|
@@ -278,6 +281,9 @@ const useSseLink = (
|
|
|
278
281
|
get connection() {
|
|
279
282
|
return c.connection;
|
|
280
283
|
},
|
|
284
|
+
get parked() {
|
|
285
|
+
return c.parked;
|
|
286
|
+
},
|
|
281
287
|
send: (frame) => sendFrame(c, latest.current, auth, frame),
|
|
282
288
|
}),
|
|
283
289
|
);
|
package/src/transports/ws.ts
CHANGED
|
@@ -178,6 +178,7 @@ const useWsLink = (
|
|
|
178
178
|
const [w] = useState((): WsChannel => ({ socket: null, authStale: false }));
|
|
179
179
|
const c = useStatewireConn({
|
|
180
180
|
url: options.url,
|
|
181
|
+
isNew: options.isNew,
|
|
181
182
|
onChange: options.onChange,
|
|
182
183
|
ctx,
|
|
183
184
|
attemptOnce: (c, abort) => attemptSocket(c, w, options, abort),
|
|
@@ -187,6 +188,9 @@ const useWsLink = (
|
|
|
187
188
|
get connection() {
|
|
188
189
|
return c.connection;
|
|
189
190
|
},
|
|
191
|
+
get parked() {
|
|
192
|
+
return c.parked;
|
|
193
|
+
},
|
|
190
194
|
send: (frame) => sendFrame(w, frame),
|
|
191
195
|
}),
|
|
192
196
|
);
|