mcp-tenant-lib 0.3.4 → 0.3.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/dist/tenant.d.ts +31 -0
- package/dist/tenant.js +51 -4
- package/dist/types.d.ts +34 -2
- package/dist/ws.js +7 -1
- package/package.json +1 -1
package/dist/tenant.d.ts
CHANGED
|
@@ -56,6 +56,16 @@ export declare class Tenant<TSchema, TValues> {
|
|
|
56
56
|
* list_fields read should be able to see that they're done.
|
|
57
57
|
*/
|
|
58
58
|
submitted: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Timestamp of the most recent real edit to `store` (a field value
|
|
61
|
+
* changing) or a successfully-applied `restoreState` (see below) —
|
|
62
|
+
* distinct from `lastActivityAt`, which also counts reads/pings and
|
|
63
|
+
* drives idle-sweep instead. Used solely to arbitrate `resync` messages
|
|
64
|
+
* (ws.ts) when more than one browser tab reconnects to the same
|
|
65
|
+
* recreated tenant: whichever tab's last edit is newer wins, rather than
|
|
66
|
+
* whichever tab's resync happens to reach the server first.
|
|
67
|
+
*/
|
|
68
|
+
lastStateChangeAt: number;
|
|
59
69
|
wsClients: Set<WebSocket>;
|
|
60
70
|
connections: Map<string, TenantConnection>;
|
|
61
71
|
lastActivityAt: number;
|
|
@@ -139,6 +149,27 @@ export declare class Tenant<TSchema, TValues> {
|
|
|
139
149
|
rejectCall(id: string, error: string): void;
|
|
140
150
|
touch(): void;
|
|
141
151
|
applyState(schema: TSchema, values: TValues): void;
|
|
152
|
+
/**
|
|
153
|
+
* Restores schema/values/submitted from a browser page's own live state
|
|
154
|
+
* after this tenant was recreated empty (see the `recreated` init flag in
|
|
155
|
+
* ws.ts) — unlike applyState (used by define_form to redefine a form),
|
|
156
|
+
* this preserves `submitted` as reported by the page rather than always
|
|
157
|
+
* resetting it, since a resync is recovering prior state, not starting a
|
|
158
|
+
* new round.
|
|
159
|
+
*
|
|
160
|
+
* `changedAt` is the pushing page's own last-local-edit timestamp
|
|
161
|
+
* (Date.now() when the user or agent last changed a field in that tab —
|
|
162
|
+
* see mcp-form's client). When two tabs both reconnect to the same
|
|
163
|
+
* recreated tenant, each pushes its own resync independently; without
|
|
164
|
+
* this guard, whichever happens to reach the server first would win,
|
|
165
|
+
* which has nothing to do with which tab actually has the fresher data.
|
|
166
|
+
* Comparing against `lastStateChangeAt` (bumped by every real edit,
|
|
167
|
+
* including a previously-applied resync) means an older resync arriving
|
|
168
|
+
* after a newer one — or after the tenant was already touched some other
|
|
169
|
+
* way, e.g. an agent's set_field — is ignored rather than clobbering it.
|
|
170
|
+
* Returns whether the resync was applied.
|
|
171
|
+
*/
|
|
172
|
+
restoreState(schema: TSchema, values: TValues, submitted: boolean, changedAt: number): boolean;
|
|
142
173
|
broadcastReinit(): void;
|
|
143
174
|
broadcastUpdate(field: string, value: unknown): void;
|
|
144
175
|
dispose(): void;
|
package/dist/tenant.js
CHANGED
|
@@ -59,6 +59,16 @@ export class Tenant {
|
|
|
59
59
|
* list_fields read should be able to see that they're done.
|
|
60
60
|
*/
|
|
61
61
|
submitted = false;
|
|
62
|
+
/**
|
|
63
|
+
* Timestamp of the most recent real edit to `store` (a field value
|
|
64
|
+
* changing) or a successfully-applied `restoreState` (see below) —
|
|
65
|
+
* distinct from `lastActivityAt`, which also counts reads/pings and
|
|
66
|
+
* drives idle-sweep instead. Used solely to arbitrate `resync` messages
|
|
67
|
+
* (ws.ts) when more than one browser tab reconnects to the same
|
|
68
|
+
* recreated tenant: whichever tab's last edit is newer wins, rather than
|
|
69
|
+
* whichever tab's resync happens to reach the server first.
|
|
70
|
+
*/
|
|
71
|
+
lastStateChangeAt = 0;
|
|
62
72
|
wsClients;
|
|
63
73
|
connections = new Map();
|
|
64
74
|
#legacyManifest;
|
|
@@ -102,7 +112,7 @@ export class Tenant {
|
|
|
102
112
|
constructor(id, initialSchema, initialValues) {
|
|
103
113
|
this.id = id;
|
|
104
114
|
this.schema = initialSchema;
|
|
105
|
-
this.store = new Store(initialValues);
|
|
115
|
+
this.store = new Store(initialValues); // placeholder; #attachStore below wires the real onChange listener
|
|
106
116
|
this.submitBus = new EventEmitter();
|
|
107
117
|
this.submitBus.setMaxListeners(0);
|
|
108
118
|
this.submitBus.on('newListener', (event) => {
|
|
@@ -117,7 +127,7 @@ export class Tenant {
|
|
|
117
127
|
});
|
|
118
128
|
this.wsClients = new Set();
|
|
119
129
|
this.lastActivityAt = Date.now();
|
|
120
|
-
this
|
|
130
|
+
this.#attachStore(this.store);
|
|
121
131
|
}
|
|
122
132
|
/**
|
|
123
133
|
* Legacy/no-WS path: registers a manifest with no real connection behind
|
|
@@ -292,14 +302,51 @@ export class Tenant {
|
|
|
292
302
|
touch() {
|
|
293
303
|
this.lastActivityAt = Date.now();
|
|
294
304
|
}
|
|
305
|
+
#attachStore(store) {
|
|
306
|
+
this.store = store;
|
|
307
|
+
this.store.onChange((field, value) => {
|
|
308
|
+
this.lastStateChangeAt = Date.now();
|
|
309
|
+
this.broadcastUpdate(field, value);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
295
312
|
applyState(schema, values) {
|
|
296
313
|
this.store.dispose();
|
|
297
314
|
this.schema = schema;
|
|
298
|
-
this
|
|
315
|
+
this.#attachStore(new Store(values));
|
|
299
316
|
this.submitted = false;
|
|
300
|
-
this.store.onChange((field, value) => this.broadcastUpdate(field, value));
|
|
301
317
|
this.broadcastReinit();
|
|
302
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Restores schema/values/submitted from a browser page's own live state
|
|
321
|
+
* after this tenant was recreated empty (see the `recreated` init flag in
|
|
322
|
+
* ws.ts) — unlike applyState (used by define_form to redefine a form),
|
|
323
|
+
* this preserves `submitted` as reported by the page rather than always
|
|
324
|
+
* resetting it, since a resync is recovering prior state, not starting a
|
|
325
|
+
* new round.
|
|
326
|
+
*
|
|
327
|
+
* `changedAt` is the pushing page's own last-local-edit timestamp
|
|
328
|
+
* (Date.now() when the user or agent last changed a field in that tab —
|
|
329
|
+
* see mcp-form's client). When two tabs both reconnect to the same
|
|
330
|
+
* recreated tenant, each pushes its own resync independently; without
|
|
331
|
+
* this guard, whichever happens to reach the server first would win,
|
|
332
|
+
* which has nothing to do with which tab actually has the fresher data.
|
|
333
|
+
* Comparing against `lastStateChangeAt` (bumped by every real edit,
|
|
334
|
+
* including a previously-applied resync) means an older resync arriving
|
|
335
|
+
* after a newer one — or after the tenant was already touched some other
|
|
336
|
+
* way, e.g. an agent's set_field — is ignored rather than clobbering it.
|
|
337
|
+
* Returns whether the resync was applied.
|
|
338
|
+
*/
|
|
339
|
+
restoreState(schema, values, submitted, changedAt) {
|
|
340
|
+
if (changedAt < this.lastStateChangeAt)
|
|
341
|
+
return false;
|
|
342
|
+
this.store.dispose();
|
|
343
|
+
this.schema = schema;
|
|
344
|
+
this.#attachStore(new Store(values));
|
|
345
|
+
this.submitted = submitted;
|
|
346
|
+
this.lastStateChangeAt = changedAt;
|
|
347
|
+
this.broadcastReinit();
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
303
350
|
broadcastReinit() {
|
|
304
351
|
const payload = JSON.stringify({ type: 'reinit', schema: this.schema, state: this.store.snapshot(), waiting: this.waiting, submitted: this.submitted });
|
|
305
352
|
for (const client of this.wsClients) {
|
package/dist/types.d.ts
CHANGED
|
@@ -4,7 +4,14 @@ export interface SubmitPayload {
|
|
|
4
4
|
[field: string]: unknown;
|
|
5
5
|
}
|
|
6
6
|
export type ServerMessage<TSchema = unknown, TValues = unknown> = {
|
|
7
|
-
type: 'init'
|
|
7
|
+
type: 'init';
|
|
8
|
+
schema: TSchema;
|
|
9
|
+
state: TValues;
|
|
10
|
+
waiting: boolean;
|
|
11
|
+
submitted: boolean;
|
|
12
|
+
recreated: boolean;
|
|
13
|
+
} | {
|
|
14
|
+
type: 'reinit';
|
|
8
15
|
schema: TSchema;
|
|
9
16
|
state: TValues;
|
|
10
17
|
waiting: boolean;
|
|
@@ -25,6 +32,31 @@ export interface SetMessage {
|
|
|
25
32
|
field: string;
|
|
26
33
|
value: unknown;
|
|
27
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Sent by a page that reconnects to a tenant the server reports as
|
|
37
|
+
* `recreated` (see the `init` message) — i.e. the in-memory tenant was
|
|
38
|
+
* lost, most commonly an MCP server restart, while this page's own JS
|
|
39
|
+
* runtime (and therefore its last-known schema/values) survived because
|
|
40
|
+
* the tab itself never reloaded. Lets the still-live browser page push its
|
|
41
|
+
* state back up as the source of truth instead of accepting the server's
|
|
42
|
+
* freshly-recreated default state. The server applies it via the same
|
|
43
|
+
* path as define_form (Tenant.applyState) and rebroadcasts `reinit` to any
|
|
44
|
+
* other connected tabs.
|
|
45
|
+
*/
|
|
46
|
+
export interface ResyncMessage<TSchema = unknown, TValues = unknown> {
|
|
47
|
+
type: 'resync';
|
|
48
|
+
schema: TSchema;
|
|
49
|
+
values: TValues;
|
|
50
|
+
submitted: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* The pushing page's own last-local-edit timestamp (Date.now()) — lets
|
|
53
|
+
* the server arbitrate when two tabs both resync the same recreated
|
|
54
|
+
* tenant, favoring whichever has the more recently edited data rather
|
|
55
|
+
* than whichever resync message happens to arrive first. See
|
|
56
|
+
* Tenant.restoreState in mcp-tenant-lib for the comparison.
|
|
57
|
+
*/
|
|
58
|
+
changedAt: number;
|
|
59
|
+
}
|
|
28
60
|
export interface SubmitMessage {
|
|
29
61
|
type: 'submit';
|
|
30
62
|
}
|
|
@@ -93,4 +125,4 @@ export interface RenameConnectionMessage {
|
|
|
93
125
|
type: 'rename_connection';
|
|
94
126
|
appLabel: string;
|
|
95
127
|
}
|
|
96
|
-
export type ClientMessage = SetMessage | SubmitMessage | InterruptMessage | RegisterToolsMessage | CallResultMessage | RenameConnectionMessage;
|
|
128
|
+
export type ClientMessage = SetMessage | SubmitMessage | InterruptMessage | RegisterToolsMessage | CallResultMessage | RenameConnectionMessage | ResyncMessage;
|
package/dist/ws.js
CHANGED
|
@@ -60,7 +60,7 @@ export function attachWebSocketServer(httpServer, port, initialSchema, initialVa
|
|
|
60
60
|
if (state)
|
|
61
61
|
state.isAlive = true;
|
|
62
62
|
});
|
|
63
|
-
ws.send(JSON.stringify({ type: 'init', schema: t.schema, state: t.store.snapshot(), waiting: t.waiting, submitted: t.submitted }));
|
|
63
|
+
ws.send(JSON.stringify({ type: 'init', schema: t.schema, state: t.store.snapshot(), waiting: t.waiting, submitted: t.submitted, recreated }));
|
|
64
64
|
ws.on('message', (raw) => {
|
|
65
65
|
t.touch();
|
|
66
66
|
let msg;
|
|
@@ -92,6 +92,12 @@ export function attachWebSocketServer(httpServer, port, initialSchema, initialVa
|
|
|
92
92
|
else
|
|
93
93
|
t.resolveCall(msg.id, msg.result);
|
|
94
94
|
}
|
|
95
|
+
if (msg.type === 'resync') {
|
|
96
|
+
const applied = t.restoreState(msg.schema, msg.values, msg.submitted, msg.changedAt);
|
|
97
|
+
console.error(applied
|
|
98
|
+
? `[ws] resync from connection=${connectionId}: restoring tenant "${tenantId}" state pushed back by the browser`
|
|
99
|
+
: `[ws] resync from connection=${connectionId}: ignored — tenant "${tenantId}" already has state at least as recent`);
|
|
100
|
+
}
|
|
95
101
|
});
|
|
96
102
|
ws.on('close', (code, reason) => {
|
|
97
103
|
console.error(`[ws] connection closed: tenant=${tenantId} connection=${connectionId} code=${code} reason=${reason.toString() || '(none)'}`);
|