ugly-app 0.1.731 → 0.1.733
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/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/native/proxy.d.ts.map +1 -1
- package/dist/native/proxy.js +49 -26
- package/dist/native/proxy.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/version.ts +1 -1
- package/src/native/proxy.test.ts +68 -0
- package/src/native/proxy.ts +47 -18
package/dist/cli/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.1.
|
|
1
|
+
export declare const CLI_VERSION = "0.1.733";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/cli/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/native/proxy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAAmC,KAAK,WAAW,EAAE,KAAK,WAAW,EAAiB,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEvI,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,4FAA4F;AAC5F,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAgBjD,CAAC;AAEH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAGzD;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,WAAW,CAAC;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3C,+EAA+E;IAC/E,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,GAAG,UAAU,CAc3E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,WAAW,CAAC;CACrB,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/native/proxy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAAmC,KAAK,WAAW,EAAE,KAAK,WAAW,EAAiB,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEvI,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,4FAA4F;AAC5F,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAgBjD,CAAC;AAEH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAGzD;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,WAAW,CAAC;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3C,+EAA+E;IAC/E,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,GAAG,UAAU,CAc3E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,WAAW,CAAC;CACrB,GAAG,UAAU,CA2Kb;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,WAAW,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE;QAAE,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAA;KAAE,CAAC;IAC7C;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG;QAAE,QAAQ,CAAC,EAAE,eAAe,CAAA;KAAE,CAAC;CACxD,GAAG,UAAU,CAgCb;AAED,8FAA8F;AAC9F,wBAAgB,mBAAmB,CACjC,GAAG,EAAE;IAAE,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAA;CAAE,EAC3C,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,OAAO,GACZ,IAAI,CAON"}
|
package/dist/native/proxy.js
CHANGED
|
@@ -60,6 +60,12 @@ export function enableUglyProxy(opts) {
|
|
|
60
60
|
// Driven by the 'proxy:connect' event — e.g. opening a recent project that's
|
|
61
61
|
// stamped with the desktop it lives on, so the phone reaches the right host.
|
|
62
62
|
let preferredHost = null;
|
|
63
|
+
// The last host we actually connected to. A transient network drop (mobile ICE churn)
|
|
64
|
+
// clears the live client's host id, so without this a silent reconnect falls through to the
|
|
65
|
+
// picker — the phone re-prompts "choose your computer" on every blip. Remember it so
|
|
66
|
+
// reconnects stay silent + automatic (the drops are environmental; recovery shouldn't be
|
|
67
|
+
// user-visible). Cleared only on an explicit host switch (proxy:switch).
|
|
68
|
+
let lastConnectedHost = null;
|
|
63
69
|
const setStatus = (state, hostLabel) => {
|
|
64
70
|
void native.invoke('proxy.setStatus', { state, hostLabel }).catch(() => { });
|
|
65
71
|
};
|
|
@@ -71,15 +77,20 @@ export function enableUglyProxy(opts) {
|
|
|
71
77
|
connecting = (async () => {
|
|
72
78
|
try {
|
|
73
79
|
// A pinned host (preferredHost) wins over the cached one and the picker.
|
|
74
|
-
const existing = preferredHost ?? (forceRepick ? null : client.hostDeviceId());
|
|
80
|
+
const existing = preferredHost ?? (forceRepick ? null : (client.hostDeviceId() ?? lastConnectedHost));
|
|
75
81
|
console.error('[uglyproxy] ensureConnected: start', { existing, preferredHost, forceRepick });
|
|
76
82
|
if (existing) {
|
|
77
|
-
// Direct (re)connect to the cached or
|
|
83
|
+
// Direct (re)connect to the cached, pinned, or last-known host — no picker.
|
|
78
84
|
setStatus('reconnecting', lastLabel);
|
|
79
85
|
await client.connect(existing);
|
|
86
|
+
lastConnectedHost = existing;
|
|
80
87
|
setStatus('connected', lastLabel);
|
|
81
88
|
preferredHost = null;
|
|
82
89
|
console.error('[uglyproxy] ensureConnected: connected', { existing });
|
|
90
|
+
// The host-side task.listen fanout is per-CONNECTION, so the drop that forced this
|
|
91
|
+
// reconnect silently dropped every passive task subscription. Re-register them, or a
|
|
92
|
+
// session viewer stays "connected" but never receives another task event.
|
|
93
|
+
void reissueActiveTaskSubs();
|
|
83
94
|
return true;
|
|
84
95
|
}
|
|
85
96
|
forceRepick = false;
|
|
@@ -96,6 +107,7 @@ export function enableUglyProxy(opts) {
|
|
|
96
107
|
}
|
|
97
108
|
lastLabel = hosts.find((h) => h.deviceId === picked.deviceId)?.label;
|
|
98
109
|
await client.connect(picked.deviceId);
|
|
110
|
+
lastConnectedHost = picked.deviceId;
|
|
99
111
|
setStatus('connected', lastLabel);
|
|
100
112
|
console.error('[uglyproxy] ensureConnected: connected', { deviceId: picked.deviceId });
|
|
101
113
|
return true;
|
|
@@ -116,6 +128,7 @@ export function enableUglyProxy(opts) {
|
|
|
116
128
|
native.subscribe('proxy:switch', (() => {
|
|
117
129
|
forceRepick = true;
|
|
118
130
|
preferredHost = null;
|
|
131
|
+
lastConnectedHost = null; // explicit switch — don't silently reconnect to the old host
|
|
119
132
|
client.close();
|
|
120
133
|
void ensureConnected();
|
|
121
134
|
}));
|
|
@@ -146,10 +159,39 @@ export function enableUglyProxy(opts) {
|
|
|
146
159
|
client.close();
|
|
147
160
|
void ensureConnected();
|
|
148
161
|
}));
|
|
149
|
-
// task.event:<id> subscriptions need a host-side fanout handshake (task.listen) — unlike
|
|
150
|
-
// process/net streams, whose fanout is registered by the spawning invoke. Ref-count per
|
|
151
|
-
// task id so N local subscribers share one host subscription (no duplicate event delivery).
|
|
152
162
|
const taskSubs = new Map();
|
|
163
|
+
// (Re)register the host `task.listen` fanout for one task id + re-prime the snapshot. Safe
|
|
164
|
+
// to call again after a reconnect — the prior connection's subscription is already gone, so
|
|
165
|
+
// this replaces the dead `unsub`; the host de-dupes fanout by the connection's emit.
|
|
166
|
+
async function issueTaskListen(taskId, entry) {
|
|
167
|
+
if (entry.cancelled)
|
|
168
|
+
return;
|
|
169
|
+
if (!client.isOpen() && !(await ensureConnected()))
|
|
170
|
+
return;
|
|
171
|
+
try {
|
|
172
|
+
const sub = await client.subscribe('task.listen', { id: taskId });
|
|
173
|
+
if (entry.cancelled) {
|
|
174
|
+
sub.unsubscribe();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
entry.unsub = sub.unsubscribe;
|
|
178
|
+
// Prime with the current in-flight snapshot, shaped like a task event so the facade's
|
|
179
|
+
// listener handles it uniformly (also catches a viewer up on a turn already in progress).
|
|
180
|
+
if (sub.snapshot !== undefined)
|
|
181
|
+
entry.cb({ event: 'snapshot', data: sub.snapshot });
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
/* host gone — bus stays wired; a reconnect re-issues */
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// A reconnect drops every host-side task.listen (they're per-connection), so re-register
|
|
188
|
+
// them all. Without this a session viewer stays "connected" yet silent after any blip.
|
|
189
|
+
async function reissueActiveTaskSubs() {
|
|
190
|
+
for (const [taskId, entry] of taskSubs) {
|
|
191
|
+
if (!entry.cancelled)
|
|
192
|
+
await issueTaskListen(taskId, entry);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
153
195
|
const subscribe = ((event, cb) => {
|
|
154
196
|
const off = native.subscribe(event, cb);
|
|
155
197
|
const m = /^task\.event:(.+)$/.exec(event);
|
|
@@ -158,28 +200,9 @@ export function enableUglyProxy(opts) {
|
|
|
158
200
|
const taskId = m[1];
|
|
159
201
|
let entry = taskSubs.get(taskId);
|
|
160
202
|
if (!entry) {
|
|
161
|
-
entry = { count: 0, unsub: null, cancelled: false };
|
|
203
|
+
entry = { count: 0, unsub: null, cancelled: false, cb };
|
|
162
204
|
taskSubs.set(taskId, entry);
|
|
163
|
-
void (
|
|
164
|
-
if (!client.isOpen() && !(await ensureConnected()))
|
|
165
|
-
return;
|
|
166
|
-
try {
|
|
167
|
-
const sub = await client.subscribe('task.listen', { id: taskId });
|
|
168
|
-
const e = taskSubs.get(taskId);
|
|
169
|
-
if (!e || e.cancelled) {
|
|
170
|
-
sub.unsubscribe();
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
e.unsub = sub.unsubscribe;
|
|
174
|
-
// Prime the (first) subscriber with the current in-flight snapshot, shaped like a
|
|
175
|
-
// task event so the facade's listener handles it uniformly.
|
|
176
|
-
if (sub.snapshot !== undefined)
|
|
177
|
-
cb({ event: 'snapshot', data: sub.snapshot });
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
/* host gone — bus stays wired; a reconnect re-issues on the next subscribe */
|
|
181
|
-
}
|
|
182
|
-
})();
|
|
205
|
+
void issueTaskListen(taskId, entry);
|
|
183
206
|
}
|
|
184
207
|
entry.count++;
|
|
185
208
|
return () => {
|
package/dist/native/proxy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/native/proxy.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAsE,MAAM,kBAAkB,CAAC;AACvI,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAG3E,4FAA4F;AAC5F,MAAM,CAAC,MAAM,kBAAkB,GAAwB,IAAI,GAAG,CAAC;IAC7D,IAAI;IACJ,SAAS;IACT,KAAK;IACL,OAAO;IACP,SAAS;IACT,UAAU;IACV,SAAS;IACT,KAAK;IACL,SAAS;IACT,QAAQ;IACR,MAAM;IACN,mFAAmF;IACnF,uFAAuF;IACvF,qFAAqF;IACrF,aAAa;CACd,CAAC,CAAC;AAEH,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IACpF,OAAO,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AACpC,CAAC;AAcD;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,IAAgB,EAAE,IAAmB;IAC7D,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC,EAAU,EAAE,EAAE,CAAC,gBAAgB,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACvG,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,MAAM,EAAE,CAAC,CAAC,OAAe,EAAE,OAAgB,EAAE,EAAE;YAC7C,IAAI,WAAW,CAAC,OAAO,CAAC;gBAAE,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YACtE,IAAI,IAAI,CAAC,wBAAwB,IAAI,gBAAgB,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC/D,0EAA0E;gBAC1E,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,sBAAsB,CAAC,OAAO,CAAC,CAAC,CAAC;YAC7D,CAAC;YACD,OAAO,IAAI,CAAC,MAAM,CAAC,OAAgB,EAAE,OAAgB,CAAC,CAAC;QACzD,CAAC,CAAyB;QAC1B,SAAS,EAAE,IAAI,CAAC,SAAS;KAC1B,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,IAG/B;IACC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAChC,IAAI,UAAU,GAA4B,IAAI,CAAC;IAC/C,IAAI,SAA6B,CAAC;IAClC,IAAI,WAAW,GAAG,KAAK,CAAC;IACxB,gFAAgF;IAChF,6EAA6E;IAC7E,6EAA6E;IAC7E,IAAI,aAAa,GAAkB,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/native/proxy.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ugly-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.733",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"comment:files": "Allowlist what ships to npm. dist = runtime; src = sourcemap targets (dist/*.js.map reference ../../src/); templates = CLI scaffold. Everything else at repo root (.pgdata local Postgres, coverage, assets/icons sources, test/, test-results/) is excluded by omission. The !negations strip the scaffold's installed deps + cruft (templates/node_modules is 200MB+ and must never ship). package.json/README/LICENSE ship automatically.",
|
|
6
6
|
"files": [
|
package/src/cli/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by prebuild — do not edit manually
|
|
2
|
-
export const CLI_VERSION = "0.1.
|
|
2
|
+
export const CLI_VERSION = "0.1.733";
|
package/src/native/proxy.test.ts
CHANGED
|
@@ -124,6 +124,39 @@ describe('enableUglyProxy task.listen bridge', () => {
|
|
|
124
124
|
expect(client._unsub).toHaveBeenCalledTimes(1);
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
it('re-issues active task.listen handshakes after a reconnect (viewer stays live)', async () => {
|
|
128
|
+
let open = true;
|
|
129
|
+
const unsub = vi.fn();
|
|
130
|
+
const client = {
|
|
131
|
+
listHosts: async () => [],
|
|
132
|
+
connect: vi.fn(async () => { open = true; }),
|
|
133
|
+
invoke: vi.fn(async () => ({})),
|
|
134
|
+
isOpen: () => open,
|
|
135
|
+
hostDeviceId: () => 'h', // the real client retains the last host id across a drop
|
|
136
|
+
close: () => { open = false; },
|
|
137
|
+
subscribe: vi.fn(async () => ({ snapshot: { turn: 'idle' }, unsubscribe: unsub })),
|
|
138
|
+
};
|
|
139
|
+
const handlers: Record<string, (d?: unknown) => void> = {};
|
|
140
|
+
const base: UglyNative = {
|
|
141
|
+
platform: 'ios',
|
|
142
|
+
invoke: vi.fn(async () => ({})) as unknown as UglyNative['invoke'],
|
|
143
|
+
subscribe: vi.fn((e: string, h: (d?: unknown) => void) => { handlers[e] = h; return () => {}; }) as unknown as UglyNative['subscribe'],
|
|
144
|
+
};
|
|
145
|
+
const n = enableUglyProxy({ native: base, client: client as unknown as ProxyClient });
|
|
146
|
+
n.subscribe('task.event:t1' as never, (() => {}) as never);
|
|
147
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
148
|
+
expect(client.subscribe).toHaveBeenCalledTimes(1); // initial handshake
|
|
149
|
+
|
|
150
|
+
// A drop + self-heal retry (proxy:retry) reconnects to the same host.
|
|
151
|
+
open = false;
|
|
152
|
+
handlers['proxy:retry']?.();
|
|
153
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
154
|
+
expect(client.connect).toHaveBeenCalledWith('h');
|
|
155
|
+
// The per-connection fanout was re-registered — without this the viewer goes silent.
|
|
156
|
+
expect(client.subscribe).toHaveBeenCalledTimes(2);
|
|
157
|
+
expect(client.subscribe).toHaveBeenLastCalledWith('task.listen', { id: 't1' });
|
|
158
|
+
});
|
|
159
|
+
|
|
127
160
|
it('does not handshake for non-task event subscriptions', async () => {
|
|
128
161
|
const client = fakeSubClient(true);
|
|
129
162
|
const n = enableUglyProxy({ native: fakeBase(), client: client as unknown as ProxyClient });
|
|
@@ -132,3 +165,38 @@ describe('enableUglyProxy task.listen bridge', () => {
|
|
|
132
165
|
expect(client.subscribe).not.toHaveBeenCalled();
|
|
133
166
|
});
|
|
134
167
|
});
|
|
168
|
+
|
|
169
|
+
describe('enableUglyProxy silent reconnect', () => {
|
|
170
|
+
it('reconnects to the LAST host after a drop without re-showing the picker', async () => {
|
|
171
|
+
let open = false;
|
|
172
|
+
const client = {
|
|
173
|
+
listHosts: async () => [{ deviceId: 'h1', label: 'Mac', lastSeen: 0 }],
|
|
174
|
+
connect: vi.fn(async () => { open = true; }),
|
|
175
|
+
invoke: vi.fn(async () => ({ ok: true })),
|
|
176
|
+
isOpen: () => open,
|
|
177
|
+
hostDeviceId: () => (open ? 'h1' : null), // a drop clears the live client's host id
|
|
178
|
+
close: () => { open = false; },
|
|
179
|
+
};
|
|
180
|
+
const base: UglyNative = {
|
|
181
|
+
platform: 'ios',
|
|
182
|
+
invoke: vi.fn(async (ch: string) => (ch === 'proxy.pickHost' ? { deviceId: 'h1' } : {})) as unknown as UglyNative['invoke'],
|
|
183
|
+
subscribe: vi.fn(() => () => {}) as unknown as UglyNative['subscribe'],
|
|
184
|
+
};
|
|
185
|
+
const n = enableUglyProxy({ native: base, client: client as unknown as ProxyClient });
|
|
186
|
+
|
|
187
|
+
// First desktop-cap call: no host yet → picker → connect h1.
|
|
188
|
+
await n.invoke('fs.readFile' as never, {} as never);
|
|
189
|
+
const picks1 = (base.invoke as unknown as { mock: { calls: unknown[][] } }).mock.calls.filter((c) => c[0] === 'proxy.pickHost');
|
|
190
|
+
expect(picks1.length, 'first connect uses the picker').toBe(1);
|
|
191
|
+
|
|
192
|
+
// Transient drop clears the live host id.
|
|
193
|
+
open = false;
|
|
194
|
+
(base.invoke as unknown as { mockClear: () => void }).mockClear();
|
|
195
|
+
|
|
196
|
+
// Next desktop-cap call must SILENTLY reconnect to the remembered host — no picker.
|
|
197
|
+
await n.invoke('fs.readFile' as never, {} as never);
|
|
198
|
+
const picks2 = (base.invoke as unknown as { mock: { calls: unknown[][] } }).mock.calls.filter((c) => c[0] === 'proxy.pickHost');
|
|
199
|
+
expect(picks2.length, 'reconnect after a drop must NOT re-prompt the host picker').toBe(0);
|
|
200
|
+
expect(client.connect).toHaveBeenLastCalledWith('h1');
|
|
201
|
+
});
|
|
202
|
+
});
|
package/src/native/proxy.ts
CHANGED
|
@@ -85,6 +85,12 @@ export function enableUglyProxy(opts: {
|
|
|
85
85
|
// Driven by the 'proxy:connect' event — e.g. opening a recent project that's
|
|
86
86
|
// stamped with the desktop it lives on, so the phone reaches the right host.
|
|
87
87
|
let preferredHost: string | null = null;
|
|
88
|
+
// The last host we actually connected to. A transient network drop (mobile ICE churn)
|
|
89
|
+
// clears the live client's host id, so without this a silent reconnect falls through to the
|
|
90
|
+
// picker — the phone re-prompts "choose your computer" on every blip. Remember it so
|
|
91
|
+
// reconnects stay silent + automatic (the drops are environmental; recovery shouldn't be
|
|
92
|
+
// user-visible). Cleared only on an explicit host switch (proxy:switch).
|
|
93
|
+
let lastConnectedHost: string | null = null;
|
|
88
94
|
|
|
89
95
|
const setStatus = (state: string, hostLabel?: string): void => {
|
|
90
96
|
void native.invoke('proxy.setStatus' as never, { state, hostLabel } as never).catch(() => {});
|
|
@@ -96,15 +102,20 @@ export function enableUglyProxy(opts: {
|
|
|
96
102
|
connecting = (async () => {
|
|
97
103
|
try {
|
|
98
104
|
// A pinned host (preferredHost) wins over the cached one and the picker.
|
|
99
|
-
const existing = preferredHost ?? (forceRepick ? null : client.hostDeviceId());
|
|
105
|
+
const existing = preferredHost ?? (forceRepick ? null : (client.hostDeviceId() ?? lastConnectedHost));
|
|
100
106
|
console.error('[uglyproxy] ensureConnected: start', { existing, preferredHost, forceRepick });
|
|
101
107
|
if (existing) {
|
|
102
|
-
// Direct (re)connect to the cached or
|
|
108
|
+
// Direct (re)connect to the cached, pinned, or last-known host — no picker.
|
|
103
109
|
setStatus('reconnecting', lastLabel);
|
|
104
110
|
await client.connect(existing);
|
|
111
|
+
lastConnectedHost = existing;
|
|
105
112
|
setStatus('connected', lastLabel);
|
|
106
113
|
preferredHost = null;
|
|
107
114
|
console.error('[uglyproxy] ensureConnected: connected', { existing });
|
|
115
|
+
// The host-side task.listen fanout is per-CONNECTION, so the drop that forced this
|
|
116
|
+
// reconnect silently dropped every passive task subscription. Re-register them, or a
|
|
117
|
+
// session viewer stays "connected" but never receives another task event.
|
|
118
|
+
void reissueActiveTaskSubs();
|
|
108
119
|
return true;
|
|
109
120
|
}
|
|
110
121
|
forceRepick = false;
|
|
@@ -118,6 +129,7 @@ export function enableUglyProxy(opts: {
|
|
|
118
129
|
if (!picked) { setStatus('none'); return false; }
|
|
119
130
|
lastLabel = hosts.find((h) => h.deviceId === picked.deviceId)?.label;
|
|
120
131
|
await client.connect(picked.deviceId);
|
|
132
|
+
lastConnectedHost = picked.deviceId;
|
|
121
133
|
setStatus('connected', lastLabel);
|
|
122
134
|
console.error('[uglyproxy] ensureConnected: connected', { deviceId: picked.deviceId });
|
|
123
135
|
return true;
|
|
@@ -137,6 +149,7 @@ export function enableUglyProxy(opts: {
|
|
|
137
149
|
native.subscribe('proxy:switch' as never, (() => {
|
|
138
150
|
forceRepick = true;
|
|
139
151
|
preferredHost = null;
|
|
152
|
+
lastConnectedHost = null; // explicit switch — don't silently reconnect to the old host
|
|
140
153
|
client.close();
|
|
141
154
|
void ensureConnected();
|
|
142
155
|
}) as never);
|
|
@@ -168,7 +181,36 @@ export function enableUglyProxy(opts: {
|
|
|
168
181
|
// task.event:<id> subscriptions need a host-side fanout handshake (task.listen) — unlike
|
|
169
182
|
// process/net streams, whose fanout is registered by the spawning invoke. Ref-count per
|
|
170
183
|
// task id so N local subscribers share one host subscription (no duplicate event delivery).
|
|
171
|
-
|
|
184
|
+
// Keep the first subscriber's `cb` on the entry so a reconnect can re-prime the snapshot
|
|
185
|
+
// (the local event bus stays wired across reconnects; only the host handshake is lost).
|
|
186
|
+
type TaskSub = { count: number; unsub: (() => void) | null; cancelled: boolean; cb: (data: unknown) => void };
|
|
187
|
+
const taskSubs = new Map<string, TaskSub>();
|
|
188
|
+
|
|
189
|
+
// (Re)register the host `task.listen` fanout for one task id + re-prime the snapshot. Safe
|
|
190
|
+
// to call again after a reconnect — the prior connection's subscription is already gone, so
|
|
191
|
+
// this replaces the dead `unsub`; the host de-dupes fanout by the connection's emit.
|
|
192
|
+
async function issueTaskListen(taskId: string, entry: TaskSub): Promise<void> {
|
|
193
|
+
if (entry.cancelled) return;
|
|
194
|
+
if (!client.isOpen() && !(await ensureConnected())) return;
|
|
195
|
+
try {
|
|
196
|
+
const sub = await client.subscribe('task.listen', { id: taskId });
|
|
197
|
+
if (entry.cancelled) { sub.unsubscribe(); return; }
|
|
198
|
+
entry.unsub = sub.unsubscribe;
|
|
199
|
+
// Prime with the current in-flight snapshot, shaped like a task event so the facade's
|
|
200
|
+
// listener handles it uniformly (also catches a viewer up on a turn already in progress).
|
|
201
|
+
if (sub.snapshot !== undefined) entry.cb({ event: 'snapshot', data: sub.snapshot });
|
|
202
|
+
} catch {
|
|
203
|
+
/* host gone — bus stays wired; a reconnect re-issues */
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// A reconnect drops every host-side task.listen (they're per-connection), so re-register
|
|
208
|
+
// them all. Without this a session viewer stays "connected" yet silent after any blip.
|
|
209
|
+
async function reissueActiveTaskSubs(): Promise<void> {
|
|
210
|
+
for (const [taskId, entry] of taskSubs) {
|
|
211
|
+
if (!entry.cancelled) await issueTaskListen(taskId, entry);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
172
214
|
|
|
173
215
|
const subscribe = ((event: string, cb: (data: unknown) => void) => {
|
|
174
216
|
const off = native.subscribe(event as never, cb as never);
|
|
@@ -177,22 +219,9 @@ export function enableUglyProxy(opts: {
|
|
|
177
219
|
const taskId = m[1];
|
|
178
220
|
let entry = taskSubs.get(taskId);
|
|
179
221
|
if (!entry) {
|
|
180
|
-
entry = { count: 0, unsub: null, cancelled: false };
|
|
222
|
+
entry = { count: 0, unsub: null, cancelled: false, cb };
|
|
181
223
|
taskSubs.set(taskId, entry);
|
|
182
|
-
void (
|
|
183
|
-
if (!client.isOpen() && !(await ensureConnected())) return;
|
|
184
|
-
try {
|
|
185
|
-
const sub = await client.subscribe('task.listen', { id: taskId });
|
|
186
|
-
const e = taskSubs.get(taskId);
|
|
187
|
-
if (!e || e.cancelled) { sub.unsubscribe(); return; }
|
|
188
|
-
e.unsub = sub.unsubscribe;
|
|
189
|
-
// Prime the (first) subscriber with the current in-flight snapshot, shaped like a
|
|
190
|
-
// task event so the facade's listener handles it uniformly.
|
|
191
|
-
if (sub.snapshot !== undefined) cb({ event: 'snapshot', data: sub.snapshot });
|
|
192
|
-
} catch {
|
|
193
|
-
/* host gone — bus stays wired; a reconnect re-issues on the next subscribe */
|
|
194
|
-
}
|
|
195
|
-
})();
|
|
224
|
+
void issueTaskListen(taskId, entry);
|
|
196
225
|
}
|
|
197
226
|
entry.count++;
|
|
198
227
|
return () => {
|