expo-openclaw-chat 0.2.3 → 0.2.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/package.json +13 -2
- package/src/chat/engine.ts +10 -1
- package/src/core/client.ts +166 -63
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-openclaw-chat",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Minimal chat SDK for Expo apps to connect to OpenClaw gateway",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -20,11 +20,22 @@
|
|
|
20
20
|
"@noble/hashes": "^2.0.0"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
+
"@expo/vector-icons": ">=14.0.0",
|
|
24
|
+
"expo-image-manipulator": ">=12.0.0",
|
|
25
|
+
"expo-image-picker": ">=14.0.0",
|
|
23
26
|
"expo-secure-store": ">=13.0.0",
|
|
24
27
|
"react": ">=18.0.0",
|
|
25
|
-
"react-native": ">=0.72.0"
|
|
28
|
+
"react-native": ">=0.72.0",
|
|
29
|
+
"react-native-keyboard-controller": ">=1.12.0",
|
|
30
|
+
"react-native-safe-area-context": ">=4.0.0"
|
|
26
31
|
},
|
|
27
32
|
"peerDependenciesMeta": {
|
|
33
|
+
"expo-image-manipulator": {
|
|
34
|
+
"optional": true
|
|
35
|
+
},
|
|
36
|
+
"expo-image-picker": {
|
|
37
|
+
"optional": true
|
|
38
|
+
},
|
|
28
39
|
"expo-secure-store": {
|
|
29
40
|
"optional": true
|
|
30
41
|
}
|
package/src/chat/engine.ts
CHANGED
|
@@ -199,10 +199,19 @@ export class ChatEngine {
|
|
|
199
199
|
idempotencyKey: generateIdempotencyKey(),
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
+
// Skip the placeholder if delta/complete events for this runId already
|
|
203
|
+
// arrived over the WS while we were awaiting chatSend — otherwise we'd
|
|
204
|
+
// append an empty bubble next to the already-rendered reply.
|
|
205
|
+
const alreadyHandled = this._messages.some(
|
|
206
|
+
(m) => m.runId === response.runId && m.role === "assistant",
|
|
207
|
+
);
|
|
208
|
+
if (alreadyHandled) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
202
212
|
this._activeRunId = response.runId;
|
|
203
213
|
this._isStreaming = true;
|
|
204
214
|
|
|
205
|
-
// Add streaming placeholder
|
|
206
215
|
const placeholder: UIMessage = {
|
|
207
216
|
id: `asst-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
|
|
208
217
|
role: "assistant",
|
package/src/core/client.ts
CHANGED
|
@@ -65,6 +65,8 @@ export interface GatewayClientOptions {
|
|
|
65
65
|
platform?: string;
|
|
66
66
|
/** Client ID for gateway registration (default: openclaw-ios) */
|
|
67
67
|
clientId?: string;
|
|
68
|
+
/** Extra headers to include in the WebSocket upgrade request (React Native only) */
|
|
69
|
+
headers?: Record<string, string>;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
// ─── Event Listener Types ───────────────────────────────────────────────────────
|
|
@@ -154,9 +156,14 @@ export class GatewayClient {
|
|
|
154
156
|
// Sequence tracking
|
|
155
157
|
private lastSeq = -1;
|
|
156
158
|
|
|
157
|
-
// Handshake
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
// Handshake — every overlapping connect() call pushes its own resolver pair
|
|
160
|
+
// here. Settled atomically (drain + clear) on hello-ok, connect failure, or
|
|
161
|
+
// close so callers can't be silently dropped if a later attempt overwrites
|
|
162
|
+
// the slot.
|
|
163
|
+
private connectPromisePending: Array<{
|
|
164
|
+
resolve: (value: HelloOk) => void;
|
|
165
|
+
reject: (reason: Error) => void;
|
|
166
|
+
}> = [];
|
|
160
167
|
private challengeNonce: string | null = null;
|
|
161
168
|
private helloOk: HelloOk | null = null;
|
|
162
169
|
|
|
@@ -166,6 +173,10 @@ export class GatewayClient {
|
|
|
166
173
|
// Pairing flow state
|
|
167
174
|
private awaitingPairing = false;
|
|
168
175
|
|
|
176
|
+
// Connection protection: when true, disconnect() is a no-op.
|
|
177
|
+
// Set during connect(), cleared on hello-ok or connect failure.
|
|
178
|
+
private _connectInFlight = false;
|
|
179
|
+
|
|
169
180
|
constructor(url: string, options: GatewayClientOptions = {}) {
|
|
170
181
|
this.url = url;
|
|
171
182
|
this.options = {
|
|
@@ -207,22 +218,41 @@ export class GatewayClient {
|
|
|
207
218
|
* Connect to the gateway. Resolves with HelloOk on successful handshake.
|
|
208
219
|
*/
|
|
209
220
|
async connect(): Promise<HelloOk> {
|
|
221
|
+
if (this._connectionState === "connected" && this.helloOk) {
|
|
222
|
+
return this.helloOk;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// If already connecting or reconnecting, attach to the in-flight handshake
|
|
226
|
+
// instead of starting another one. This prevents the autoConnect effect
|
|
227
|
+
// from creating duplicate connection attempts during React lifecycle
|
|
228
|
+
// re-runs (e.g. when clientOptions or token deps change mid-handshake).
|
|
210
229
|
if (
|
|
211
|
-
this._connectionState === "
|
|
212
|
-
this._connectionState === "
|
|
230
|
+
this._connectionState === "connecting" ||
|
|
231
|
+
this._connectionState === "reconnecting"
|
|
213
232
|
) {
|
|
214
|
-
|
|
233
|
+
return new Promise<HelloOk>((resolve, reject) => {
|
|
234
|
+
this.connectPromisePending.push({ resolve, reject });
|
|
235
|
+
});
|
|
215
236
|
}
|
|
216
237
|
|
|
217
238
|
this.intentionalClose = false;
|
|
239
|
+
this._connectInFlight = true;
|
|
218
240
|
this.setConnectionState("connecting");
|
|
219
241
|
|
|
220
|
-
// Load device identity before opening WebSocket
|
|
221
|
-
|
|
242
|
+
// Load device identity before opening WebSocket. If this throws, any
|
|
243
|
+
// overlapping connect() callers that queued themselves during the await
|
|
244
|
+
// would otherwise hang forever and wedge the state machine at
|
|
245
|
+
// "connecting" — drain them with the same error.
|
|
246
|
+
try {
|
|
247
|
+
await this.ensureIdentity();
|
|
248
|
+
} catch (err) {
|
|
249
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
250
|
+
this.handleConnectFailure(error);
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
222
253
|
|
|
223
254
|
return new Promise<HelloOk>((resolve, reject) => {
|
|
224
|
-
this.
|
|
225
|
-
this.connectPromiseReject = reject;
|
|
255
|
+
this.connectPromisePending.push({ resolve, reject });
|
|
226
256
|
this.openWebSocket();
|
|
227
257
|
});
|
|
228
258
|
}
|
|
@@ -231,6 +261,10 @@ export class GatewayClient {
|
|
|
231
261
|
* Cleanly disconnect from the gateway.
|
|
232
262
|
*/
|
|
233
263
|
disconnect(): void {
|
|
264
|
+
// Don't kill a connection that's mid-handshake (race with React effect cleanup)
|
|
265
|
+
if (this._connectInFlight) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
234
268
|
this.intentionalClose = true;
|
|
235
269
|
this.awaitingPairing = false;
|
|
236
270
|
this.clearReconnectTimer();
|
|
@@ -253,6 +287,13 @@ export class GatewayClient {
|
|
|
253
287
|
this.ws = null;
|
|
254
288
|
}
|
|
255
289
|
|
|
290
|
+
// Drain any callers that queued via connect() while we were
|
|
291
|
+
// reconnecting — without this they hang on a connection that's
|
|
292
|
+
// intentionally going away.
|
|
293
|
+
if (this.connectPromisePending.length > 0) {
|
|
294
|
+
this.drainConnectPending((p) => p.reject(new Error("Client disconnected")));
|
|
295
|
+
}
|
|
296
|
+
|
|
256
297
|
this.setConnectionState("disconnected");
|
|
257
298
|
}
|
|
258
299
|
|
|
@@ -274,24 +315,29 @@ export class GatewayClient {
|
|
|
274
315
|
if (this.options.autoReconnect !== false && this._connectionState !== "disconnected") {
|
|
275
316
|
const waitTimeout = 5000;
|
|
276
317
|
let settled = false;
|
|
318
|
+
// Capture the timer so the listener path can clear it; otherwise
|
|
319
|
+
// each settled-by-listener request still pins a 5s timer + closure
|
|
320
|
+
// until it fires, which compounds badly during fan-out reconnects.
|
|
321
|
+
const waitTimer = setTimeout(() => {
|
|
322
|
+
if (settled) return;
|
|
323
|
+
settled = true;
|
|
324
|
+
unsub();
|
|
325
|
+
reject(new Error("Not connected"));
|
|
326
|
+
}, waitTimeout);
|
|
277
327
|
const unsub = this.onConnectionStateChange((state) => {
|
|
278
328
|
if (settled) return;
|
|
279
329
|
if (state === "connected") {
|
|
280
330
|
settled = true;
|
|
331
|
+
clearTimeout(waitTimer);
|
|
281
332
|
unsub();
|
|
282
333
|
this.request<T>(method, params, timeoutMs).then(resolve, reject);
|
|
283
334
|
} else if (state === "disconnected") {
|
|
284
335
|
settled = true;
|
|
336
|
+
clearTimeout(waitTimer);
|
|
285
337
|
unsub();
|
|
286
338
|
reject(new Error("Not connected"));
|
|
287
339
|
}
|
|
288
340
|
});
|
|
289
|
-
setTimeout(() => {
|
|
290
|
-
if (settled) return;
|
|
291
|
-
settled = true;
|
|
292
|
-
unsub();
|
|
293
|
-
reject(new Error("Not connected"));
|
|
294
|
-
}, waitTimeout);
|
|
295
341
|
return;
|
|
296
342
|
}
|
|
297
343
|
return reject(new Error("Not connected"));
|
|
@@ -526,6 +572,17 @@ export class GatewayClient {
|
|
|
526
572
|
// ─── Private: WebSocket Lifecycle ──────────────────────────────────────────
|
|
527
573
|
|
|
528
574
|
private openWebSocket(): void {
|
|
575
|
+
// Don't open a new WebSocket if we already have a live one. The React
|
|
576
|
+
// lifecycle can race with reconnect attempts, leading to multiple
|
|
577
|
+
// overlapping connect frames and orphaned sockets.
|
|
578
|
+
if (
|
|
579
|
+
this.ws &&
|
|
580
|
+
(this.ws.readyState === WebSocket.OPEN ||
|
|
581
|
+
this.ws.readyState === WebSocket.CONNECTING)
|
|
582
|
+
) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
529
586
|
// Auto-add wss:// if protocol is missing
|
|
530
587
|
let url = this.url;
|
|
531
588
|
if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
|
|
@@ -533,18 +590,31 @@ export class GatewayClient {
|
|
|
533
590
|
}
|
|
534
591
|
|
|
535
592
|
try {
|
|
536
|
-
|
|
593
|
+
const wsHeaders = this.options.headers;
|
|
594
|
+
if (wsHeaders && Object.keys(wsHeaders).length > 0) {
|
|
595
|
+
// React Native WebSocket accepts a 3rd argument for headers.
|
|
596
|
+
// Standard browser WebSocket does not, so we use a cast here.
|
|
597
|
+
const WS = WebSocket as unknown as new (
|
|
598
|
+
url: string,
|
|
599
|
+
protocols: null,
|
|
600
|
+
options: { headers: Record<string, string> },
|
|
601
|
+
) => WebSocket;
|
|
602
|
+
this.ws = new WS(url, null, { headers: wsHeaders });
|
|
603
|
+
} else {
|
|
604
|
+
this.ws = new WebSocket(url);
|
|
605
|
+
}
|
|
537
606
|
} catch (err) {
|
|
538
607
|
this.handleConnectFailure(err as Error);
|
|
539
608
|
return;
|
|
540
609
|
}
|
|
541
610
|
|
|
542
611
|
this.ws.onopen = () => {
|
|
543
|
-
// Wait for
|
|
544
|
-
//
|
|
545
|
-
//
|
|
612
|
+
// Wait briefly for an optional connect.challenge from the server. If a
|
|
613
|
+
// challenge arrives, handleChallenge() sends the connect frame with its
|
|
614
|
+
// nonce; otherwise we send with a self-generated nonce after the timeout.
|
|
615
|
+
// The React-lifecycle race that previously killed the WS during this
|
|
616
|
+
// window is now blocked by the _connectInFlight guard in disconnect().
|
|
546
617
|
this.challengeNonce = null;
|
|
547
|
-
// Give 2s for an optional challenge, then connect anyway
|
|
548
618
|
setTimeout(() => {
|
|
549
619
|
if (this._connectionState === "connecting" && !this.challengeNonce) {
|
|
550
620
|
this.sendConnectFrame();
|
|
@@ -637,7 +707,6 @@ export class GatewayClient {
|
|
|
637
707
|
|
|
638
708
|
private handleEvent(frame: EventFrame): void {
|
|
639
709
|
const { event, payload, seq } = frame;
|
|
640
|
-
|
|
641
710
|
// Sequence gap detection
|
|
642
711
|
if (seq != null) {
|
|
643
712
|
if (this.lastSeq >= 0 && seq > this.lastSeq + 1) {
|
|
@@ -668,15 +737,17 @@ export class GatewayClient {
|
|
|
668
737
|
});
|
|
669
738
|
break;
|
|
670
739
|
|
|
671
|
-
case GatewayEvents.CHAT:
|
|
740
|
+
case GatewayEvents.CHAT: {
|
|
741
|
+
const chatPayload = payload as ChatEventPayload;
|
|
672
742
|
this.chatEventListeners.forEach((cb) => {
|
|
673
743
|
try {
|
|
674
|
-
cb(
|
|
744
|
+
cb(chatPayload);
|
|
675
745
|
} catch {
|
|
676
746
|
// Ignore listener errors to avoid breaking event dispatch
|
|
677
747
|
}
|
|
678
748
|
});
|
|
679
749
|
break;
|
|
750
|
+
}
|
|
680
751
|
|
|
681
752
|
case GatewayEvents.AGENT:
|
|
682
753
|
this.agentEventListeners.forEach((cb) => {
|
|
@@ -724,8 +795,14 @@ export class GatewayClient {
|
|
|
724
795
|
this.awaitingPairing = false;
|
|
725
796
|
|
|
726
797
|
if (payload.decision === "approved") {
|
|
727
|
-
//
|
|
728
|
-
|
|
798
|
+
// If the WS is still open we can re-handshake immediately. If it's
|
|
799
|
+
// already closed (the gateway typically closes with 1008 before
|
|
800
|
+
// approval lands), let the auto-reconnect cycle that's already in
|
|
801
|
+
// flight do the next attempt — the device is now approved on the
|
|
802
|
+
// server, so the next connect will succeed.
|
|
803
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
804
|
+
this.sendConnectFrame();
|
|
805
|
+
}
|
|
729
806
|
} else {
|
|
730
807
|
// Rejected - fail the connection
|
|
731
808
|
this.handleConnectFailure(new Error("Device pairing was denied"));
|
|
@@ -742,6 +819,9 @@ export class GatewayClient {
|
|
|
742
819
|
this.helloOk = helloOk;
|
|
743
820
|
this.reconnectAttempt = 0;
|
|
744
821
|
this.lastSeq = -1;
|
|
822
|
+
// Clear any pending reconnect timer — a stale timer from a prior
|
|
823
|
+
// failed attempt could otherwise stomp on this fresh connection.
|
|
824
|
+
this.clearReconnectTimer();
|
|
745
825
|
|
|
746
826
|
// Configure tick interval from policy
|
|
747
827
|
if (helloOk.policy?.tickIntervalMs) {
|
|
@@ -751,11 +831,27 @@ export class GatewayClient {
|
|
|
751
831
|
this.startTickMonitor();
|
|
752
832
|
this.setConnectionState("connected");
|
|
753
833
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
834
|
+
this.drainConnectPending((p) => p.resolve(helloOk));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Settle every pending connect() caller atomically. Captures the queue,
|
|
839
|
+
* clears it, then invokes each callback with throws isolated so one bad
|
|
840
|
+
* listener can't strand the others. Called from hello-ok (resolve), connect
|
|
841
|
+
* failure (reject), and close (reject).
|
|
842
|
+
*/
|
|
843
|
+
private drainConnectPending(
|
|
844
|
+
settle: (p: { resolve: (v: HelloOk) => void; reject: (e: Error) => void }) => void,
|
|
845
|
+
): void {
|
|
846
|
+
this._connectInFlight = false;
|
|
847
|
+
const pending = this.connectPromisePending;
|
|
848
|
+
this.connectPromisePending = [];
|
|
849
|
+
for (const p of pending) {
|
|
850
|
+
try {
|
|
851
|
+
settle(p);
|
|
852
|
+
} catch {
|
|
853
|
+
// Ignore listener throws — other callers must still be settled.
|
|
854
|
+
}
|
|
759
855
|
}
|
|
760
856
|
}
|
|
761
857
|
|
|
@@ -795,9 +891,17 @@ export class GatewayClient {
|
|
|
795
891
|
const signedAtMs = Date.now();
|
|
796
892
|
const authToken = (auth.token as string) ?? (auth.deviceToken as string);
|
|
797
893
|
|
|
894
|
+
// Generate a random nonce if no challenge was received from the gateway.
|
|
895
|
+
// OpenClaw 2026.3.x requires the nonce field in all device connect params.
|
|
896
|
+
const nonce =
|
|
897
|
+
this.challengeNonce ??
|
|
898
|
+
Array.from(crypto.getRandomValues(new Uint8Array(16)))
|
|
899
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
900
|
+
.join("");
|
|
901
|
+
|
|
798
902
|
// Build signature payload (matches Swift implementation)
|
|
799
903
|
const payload = buildSignaturePayload({
|
|
800
|
-
nonce
|
|
904
|
+
nonce,
|
|
801
905
|
deviceId: identity.deviceId,
|
|
802
906
|
clientId,
|
|
803
907
|
clientMode,
|
|
@@ -816,7 +920,7 @@ export class GatewayClient {
|
|
|
816
920
|
publicKey,
|
|
817
921
|
signature,
|
|
818
922
|
signedAt: signedAtMs,
|
|
819
|
-
nonce
|
|
923
|
+
nonce,
|
|
820
924
|
};
|
|
821
925
|
}
|
|
822
926
|
}
|
|
@@ -876,12 +980,7 @@ export class GatewayClient {
|
|
|
876
980
|
}
|
|
877
981
|
|
|
878
982
|
private handleConnectFailure(error: Error): void {
|
|
879
|
-
|
|
880
|
-
this.connectPromiseReject(error);
|
|
881
|
-
this.connectPromiseResolve = null;
|
|
882
|
-
this.connectPromiseReject = null;
|
|
883
|
-
}
|
|
884
|
-
|
|
983
|
+
this.drainConnectPending((p) => p.reject(error));
|
|
885
984
|
this.setConnectionState("disconnected");
|
|
886
985
|
}
|
|
887
986
|
|
|
@@ -902,7 +1001,11 @@ export class GatewayClient {
|
|
|
902
1001
|
return;
|
|
903
1002
|
}
|
|
904
1003
|
|
|
905
|
-
// Gateway closed with 1008 "pairing required" —
|
|
1004
|
+
// Gateway closed with 1008 "pairing required" — emit event for UI and
|
|
1005
|
+
// fall through to auto-reconnect. On hosted gateways the server runs a
|
|
1006
|
+
// background loop that approves new devices within a few seconds, so a
|
|
1007
|
+
// backoff retry will succeed once approval lands. On self-hosted gateways
|
|
1008
|
+
// the user approves manually; auto-reconnect keeps trying in the meantime.
|
|
906
1009
|
if (
|
|
907
1010
|
code === 1008 &&
|
|
908
1011
|
reason.toLowerCase().includes("pairing") &&
|
|
@@ -912,18 +1015,19 @@ export class GatewayClient {
|
|
|
912
1015
|
this.emitEvent("pairing.required", {
|
|
913
1016
|
deviceId: this.deviceIdentity?.deviceId,
|
|
914
1017
|
});
|
|
915
|
-
// Don't
|
|
916
|
-
// then the UI will retry the connection
|
|
917
|
-
return;
|
|
1018
|
+
// Don't return — let auto-reconnect schedule the retry below.
|
|
918
1019
|
}
|
|
919
1020
|
|
|
920
|
-
//
|
|
921
|
-
|
|
922
|
-
|
|
1021
|
+
// Reject any in-flight connect() promises so callers don't hang. The
|
|
1022
|
+
// auto-reconnect path will create fresh promises on the next attempt —
|
|
1023
|
+
// this just unblocks anything queued against the dying socket.
|
|
1024
|
+
if (this.connectPromisePending.length > 0) {
|
|
1025
|
+
const closeError = new Error(
|
|
1026
|
+
`WebSocket closed during connect: ${code} ${reason}`,
|
|
1027
|
+
);
|
|
1028
|
+
this.drainConnectPending((p) => p.reject(closeError));
|
|
923
1029
|
if (!this.options.autoReconnect) {
|
|
924
|
-
this.
|
|
925
|
-
new Error(`WebSocket closed during connect: ${code} ${reason}`),
|
|
926
|
-
);
|
|
1030
|
+
this.setConnectionState("disconnected");
|
|
927
1031
|
return;
|
|
928
1032
|
}
|
|
929
1033
|
}
|
|
@@ -966,20 +1070,19 @@ export class GatewayClient {
|
|
|
966
1070
|
this.setConnectionState("connecting");
|
|
967
1071
|
this.challengeNonce = null;
|
|
968
1072
|
|
|
969
|
-
//
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
};
|
|
1073
|
+
// If no caller is waiting on a connect() (we're just reconnecting after a
|
|
1074
|
+
// dropped session), seed an internal pending entry whose reject schedules
|
|
1075
|
+
// another retry. handleHelloOk will drain it as a no-op resolve.
|
|
1076
|
+
if (this.connectPromisePending.length === 0) {
|
|
1077
|
+
this.connectPromisePending.push({
|
|
1078
|
+
resolve: () => {},
|
|
1079
|
+
reject: () => {
|
|
1080
|
+
if (!this.intentionalClose && this.options.autoReconnect) {
|
|
1081
|
+
this.setConnectionState("reconnecting");
|
|
1082
|
+
this.scheduleReconnect();
|
|
1083
|
+
}
|
|
1084
|
+
},
|
|
1085
|
+
});
|
|
983
1086
|
}
|
|
984
1087
|
|
|
985
1088
|
this.openWebSocket();
|