@rxferrix/client 0.8.0 → 0.10.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/dist/auth.d.ts +2 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +15 -0
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +6 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +69 -3
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +2 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/remote_query_set.d.ts +8 -0
- package/dist/remote_query_set.d.ts.map +1 -1
- package/dist/remote_query_set.js +16 -1
- package/dist/remote_query_set.js.map +1 -1
- package/dist/request_manager.d.ts +15 -2
- package/dist/request_manager.d.ts.map +1 -1
- package/dist/request_manager.js +36 -5
- package/dist/request_manager.js.map +1 -1
- package/package.json +1 -1
- package/src/auth.ts +15 -0
- package/src/client.ts +64 -3
- package/src/errors.ts +2 -0
- package/src/index.ts +1 -1
- package/src/protocol.ts +1 -1
- package/src/remote_query_set.ts +15 -1
- package/src/request_manager.ts +39 -4
package/dist/protocol.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAC7B,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC;AAC/B,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEtG,eAAO,MAAM,cAAc,
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
|
package/dist/protocol.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wire types for the RxFerrix sync protocol (v1). See `docs/PROTOCOL.md` in the repository; that document is normative.
|
|
3
3
|
*/
|
|
4
|
-
export const CLIENT_VERSION = "0.
|
|
4
|
+
export const CLIENT_VERSION = "0.10.0";
|
|
5
5
|
export function versionsEqual(a, b) {
|
|
6
6
|
return a.querySet === b.querySet && a.identity === b.identity && a.ts === b.ts;
|
|
7
7
|
}
|
package/dist/protocol.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,CAAC,MAAM,cAAc,GAAG,
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC;AAyIvC,MAAM,UAAU,aAAa,CAAC,CAAe,EAAE,CAAe;IAC5D,OAAO,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;AACjF,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,CAAe;IAC3C,OAAO,aAAa,CAAC,CAAC,QAAQ,aAAa,CAAC,CAAC,QAAQ,OAAO,CAAC,CAAC,EAAE,GAAG,CAAC;AACtE,CAAC"}
|
|
@@ -3,6 +3,14 @@ import type { QueryState } from "./query_state.js";
|
|
|
3
3
|
export declare class TransitionVersionMismatch extends Error {
|
|
4
4
|
constructor(expected: StateVersion, got: StateVersion);
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Freezes `value` and everything in it not frozen yet, and returns it. Query results are frozen: they are the base the
|
|
8
|
+
* server's next diff applies to, and they are shared with every caller (`useQuery`, `getQuery` in optimistic updates),
|
|
9
|
+
* so changing one in place (`results.sort()`, `store.getQuery(q).push(x)`) would silently corrupt every later update.
|
|
10
|
+
* Freezing makes that a `TypeError` instead. A diff reuses the unchanged parts of the previous result (frozen already),
|
|
11
|
+
* so only what it built is walked. Typed arrays (`Bytes`) can't be frozen and are left as they are.
|
|
12
|
+
*/
|
|
13
|
+
export declare function deepFreeze<T>(value: T): T;
|
|
6
14
|
export interface TransitionResult {
|
|
7
15
|
/** Queries whose server result changed (updated, failed or removed). */
|
|
8
16
|
changed: QueryId[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote_query_set.d.ts","sourceRoot":"","sources":["../src/remote_query_set.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,OAAO,EAEZ,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACvB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,QAAQ,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY;CAGtD;AAED,MAAM,WAAW,gBAAgB;IAC/B,wEAAwE;IACxE,OAAO,EAAE,OAAO,EAAE,CAAC;IACnB,QAAQ,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;CACnD;AAED;;;GAGG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAqD;IACrE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAE1D,IAAI,OAAO,IAAI,YAAY,CAE1B;IAED,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS;IAIhD;;;;OAIG;IACH,UAAU,CAAC,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,GAAG,gBAAgB;IA+ChG,0EAA0E;IAC1E,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAI9B;;;OAGG;IACH,YAAY,IAAI,IAAI;CAGrB"}
|
|
1
|
+
{"version":3,"file":"remote_query_set.d.ts","sourceRoot":"","sources":["../src/remote_query_set.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,OAAO,EAEZ,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACvB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,QAAQ,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY;CAGtD;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAKzC;AAED,MAAM,WAAW,gBAAgB;IAC/B,wEAAwE;IACxE,OAAO,EAAE,OAAO,EAAE,CAAC;IACnB,QAAQ,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;CACnD;AAED;;;GAGG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAqD;IACrE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAE1D,IAAI,OAAO,IAAI,YAAY,CAE1B;IAED,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS;IAIhD;;;;OAIG;IACH,UAAU,CAAC,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,GAAG,gBAAgB;IA+ChG,0EAA0E;IAC1E,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAI9B;;;OAGG;IACH,YAAY,IAAI,IAAI;CAGrB"}
|
package/dist/remote_query_set.js
CHANGED
|
@@ -6,6 +6,21 @@ export class TransitionVersionMismatch extends Error {
|
|
|
6
6
|
super(`Transition startVersion ${formatVersion(got)} does not match client version ${formatVersion(expected)}`);
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Freezes `value` and everything in it not frozen yet, and returns it. Query results are frozen: they are the base the
|
|
11
|
+
* server's next diff applies to, and they are shared with every caller (`useQuery`, `getQuery` in optimistic updates),
|
|
12
|
+
* so changing one in place (`results.sort()`, `store.getQuery(q).push(x)`) would silently corrupt every later update.
|
|
13
|
+
* Freezing makes that a `TypeError` instead. A diff reuses the unchanged parts of the previous result (frozen already),
|
|
14
|
+
* so only what it built is walked. Typed arrays (`Bytes`) can't be frozen and are left as they are.
|
|
15
|
+
*/
|
|
16
|
+
export function deepFreeze(value) {
|
|
17
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value) || ArrayBuffer.isView(value))
|
|
18
|
+
return value;
|
|
19
|
+
Object.freeze(value);
|
|
20
|
+
for (const child of Object.values(value))
|
|
21
|
+
deepFreeze(child);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
9
24
|
/**
|
|
10
25
|
* The server's view of the world for the current socket: the StateVersion and the latest result for every query.
|
|
11
26
|
* Transitions are validated (`startVersion` must equal the current version) and then applied atomically.
|
|
@@ -49,7 +64,7 @@ export class RemoteQuerySet {
|
|
|
49
64
|
case "QueryUpdated":
|
|
50
65
|
this.results.set(queryId, {
|
|
51
66
|
status: "success",
|
|
52
|
-
value: "diff" in modification ? diffed.get(modification) : modification.value,
|
|
67
|
+
value: deepFreeze("diff" in modification ? diffed.get(modification) : modification.value),
|
|
53
68
|
});
|
|
54
69
|
break;
|
|
55
70
|
case "QueryFailed":
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote_query_set.js","sourceRoot":"","sources":["../src/remote_query_set.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EACL,aAAa,EACb,aAAa,GAKd,MAAM,eAAe,CAAC;AAGvB,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IAClD,YAAY,QAAsB,EAAE,GAAiB;QACnD,KAAK,CAAC,2BAA2B,aAAa,CAAC,GAAG,CAAC,kCAAkC,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAClH,CAAC;CACF;AAQD;;;GAGG;AACH,MAAM,OAAO,cAAc;IACjB,QAAQ,GAAiB,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;IACpD,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IAE1D,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED,MAAM,CAAC,OAAgB;QACrB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IAED;;;;OAIG;IACH,UAAU,CAAC,OAA0B,EAAE,OAAsC;QAC3E,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YACxD,MAAM,IAAI,yBAAyB,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;QAC3E,CAAC;QACD,kGAAkG;QAClG,MAAM,MAAM,GAAG,IAAI,GAAG,EAA8B,CAAC;QACrD,KAAK,MAAM,YAAY,IAAI,OAAO,CAAC,aAAa,IAAI,EAAE,EAAE,CAAC;YACvD,IAAI,YAAY,CAAC,IAAI,KAAK,cAAc,IAAI,CAAC,CAAC,MAAM,IAAI,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC;gBAAE,SAAS;YAClH,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;YACvD,IAAI,OAAO,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;gBAClC,MAAM,IAAI,YAAY,CAAC,SAAS,YAAY,CAAC,OAAO,mCAAmC,CAAC,CAAC;YAC3F,CAAC;YACD,MAAM,CAAC,GAAG,CAAC,YAAY,EAAE,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;QACxE,CAAC;QACD,MAAM,OAAO,GAAc,EAAE,CAAC;QAC9B,MAAM,QAAQ,GAAiC,EAAE,CAAC;QAClD,KAAK,MAAM,YAAY,IAAI,OAAO,CAAC,aAAa,IAAI,EAAE,EAAE,CAAC;YACvD,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC;YACrC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;gBAAE,SAAS;YAChC,QAAQ,YAAY,CAAC,IAAI,EAAE,CAAC;gBAC1B,KAAK,cAAc;oBACjB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE;wBACxB,MAAM,EAAE,SAAS;wBACjB,KAAK,EAAE,MAAM,IAAI,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK;
|
|
1
|
+
{"version":3,"file":"remote_query_set.js","sourceRoot":"","sources":["../src/remote_query_set.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { RxFerrixError } from "./errors.js";
|
|
2
2
|
import type { ActionMessage, ActionResponseMessage, MutationMessage, MutationResponseMessage, RequestId } from "./protocol.js";
|
|
3
|
+
/**
|
|
4
|
+
* How long after its first send a mutation may still be sent again. The server recognizes a resent mutation (and
|
|
5
|
+
* answers with the recorded result instead of running it again) for 10 minutes (docs/PROTOCOL.md); later, a resend
|
|
6
|
+
* could run it twice. Kept below that, for clock and transit slack.
|
|
7
|
+
*/
|
|
8
|
+
export declare const RESEND_WINDOW_MS: number;
|
|
3
9
|
export interface SettledMutation {
|
|
4
10
|
requestId: RequestId;
|
|
5
11
|
settle: () => void;
|
|
@@ -21,8 +27,15 @@ export declare class RequestManager {
|
|
|
21
27
|
trackAction(message: ActionMessage): Promise<unknown>;
|
|
22
28
|
/** Messages that still have to go out on the current socket, in request order. */
|
|
23
29
|
unsent(): (MutationMessage | ActionMessage)[];
|
|
24
|
-
/** The message went out on the current socket. */
|
|
25
|
-
markSent(requestId: RequestId): void;
|
|
30
|
+
/** The message went out on the current socket, from `identity` (whom the socket is authenticated as). */
|
|
31
|
+
markSent(requestId: RequestId, identity?: string | null): void;
|
|
32
|
+
/**
|
|
33
|
+
* Mutations waiting to be sent again that can't be safely: sent more than {@link RESEND_WINDOW_MS} ago (the server
|
|
34
|
+
* no longer recognizes them, so they could run twice), or from another identity than now (the server never matches
|
|
35
|
+
* a resend across identities: it would run, or fail, as someone else). They are taken off and returned, rejected
|
|
36
|
+
* with {@link OUTCOME_UNKNOWN}: whether the first send ran isn't known.
|
|
37
|
+
*/
|
|
38
|
+
expireUnsafeResends(identity: string | null, now?: number): SettledMutation[];
|
|
26
39
|
/**
|
|
27
40
|
* Handles a MutationResponse. `observedTs` is the ts of the last applied Transition, or null when the client's view
|
|
28
41
|
* is not currently consistent with the server (right after a reconnect).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request_manager.d.ts","sourceRoot":"","sources":["../src/request_manager.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"request_manager.d.ts","sourceRoot":"","sources":["../src/request_manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,aAAa,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,KAAK,EACV,aAAa,EACb,qBAAqB,EACrB,eAAe,EACf,uBAAuB,EACvB,SAAS,EACV,MAAM,eAAe,CAAC;AAIvB;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,QAAa,CAAC;AAuB3C,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiC;IAE1D,iBAAiB,IAAI,SAAS;IAI9B,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC;IAezD,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC;IAarD,kFAAkF;IAClF,MAAM,IAAI,CAAC,eAAe,GAAG,aAAa,CAAC,EAAE;IAU7C,yGAAyG;IACzG,QAAQ,CAAC,SAAS,EAAE,SAAS,EAAE,QAAQ,GAAE,MAAM,GAAG,IAAW,GAAG,IAAI;IAOpE;;;;;OAKG;IACH,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,GAAE,MAAmB,GAAG,eAAe,EAAE;IAiBzF;;;;;;OAMG;IACH,kBAAkB,CAAC,QAAQ,EAAE,uBAAuB,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,eAAe,GAAG,IAAI;IAiBxG,8FAA8F;IAC9F,gBAAgB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI;IAYtE,2GAA2G;IAC3G,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,eAAe,EAAE;IAW9C;;;OAGG;IACH,cAAc,IAAI;QAAE,MAAM,EAAE,MAAM,IAAI,CAAC;QAAC,SAAS,EAAE,SAAS,CAAA;KAAE,EAAE;IAkBhE,0CAA0C;IAC1C,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAMrC,WAAW,IAAI,OAAO;IAItB,oBAAoB,IAAI,OAAO;IAK/B,MAAM,IAAI;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;IAYhF,SAAS,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS;CAGpD"}
|
package/dist/request_manager.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { CONNECTION_LOST, RxFerrixError } from "./errors.js";
|
|
1
|
+
import { CONNECTION_LOST, OUTCOME_UNKNOWN, RxFerrixError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* How long after its first send a mutation may still be sent again. The server recognizes a resent mutation (and
|
|
4
|
+
* answers with the recorded result instead of running it again) for 10 minutes (docs/PROTOCOL.md); later, a resend
|
|
5
|
+
* could run it twice. Kept below that, for clock and transit slack.
|
|
6
|
+
*/
|
|
7
|
+
export const RESEND_WINDOW_MS = 9 * 60_000;
|
|
2
8
|
/**
|
|
3
9
|
* Tracks in-flight mutations and actions.
|
|
4
10
|
*
|
|
@@ -22,6 +28,7 @@ export class RequestManager {
|
|
|
22
28
|
sent: false,
|
|
23
29
|
completed: null,
|
|
24
30
|
startedAt: Date.now(),
|
|
31
|
+
firstSent: null,
|
|
25
32
|
resolve,
|
|
26
33
|
reject,
|
|
27
34
|
});
|
|
@@ -51,11 +58,35 @@ export class RequestManager {
|
|
|
51
58
|
}
|
|
52
59
|
return out;
|
|
53
60
|
}
|
|
54
|
-
/** The message went out on the current socket. */
|
|
55
|
-
markSent(requestId) {
|
|
61
|
+
/** The message went out on the current socket, from `identity` (whom the socket is authenticated as). */
|
|
62
|
+
markSent(requestId, identity = null) {
|
|
56
63
|
const request = this.requests.get(requestId);
|
|
57
|
-
if (request)
|
|
58
|
-
|
|
64
|
+
if (!request)
|
|
65
|
+
return;
|
|
66
|
+
request.sent = true;
|
|
67
|
+
if (request.kind === "mutation" && !request.firstSent)
|
|
68
|
+
request.firstSent = { at: Date.now(), identity };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Mutations waiting to be sent again that can't be safely: sent more than {@link RESEND_WINDOW_MS} ago (the server
|
|
72
|
+
* no longer recognizes them, so they could run twice), or from another identity than now (the server never matches
|
|
73
|
+
* a resend across identities: it would run, or fail, as someone else). They are taken off and returned, rejected
|
|
74
|
+
* with {@link OUTCOME_UNKNOWN}: whether the first send ran isn't known.
|
|
75
|
+
*/
|
|
76
|
+
expireUnsafeResends(identity, now = Date.now()) {
|
|
77
|
+
const expired = [];
|
|
78
|
+
for (const [requestId, request] of this.requests) {
|
|
79
|
+
if (request.kind !== "mutation" || request.sent || request.completed || !request.firstSent)
|
|
80
|
+
continue;
|
|
81
|
+
const tooOld = now - request.firstSent.at > RESEND_WINDOW_MS;
|
|
82
|
+
if (!tooOld && request.firstSent.identity === identity)
|
|
83
|
+
continue;
|
|
84
|
+
this.requests.delete(requestId);
|
|
85
|
+
const why = tooOld ? "too long ago to be sent again safely" : "as another user than now";
|
|
86
|
+
const error = new RxFerrixError(`The connection was lost after mutation "${request.message.udfPath}" was sent, ${why}: it may or may not have run`, { code: OUTCOME_UNKNOWN });
|
|
87
|
+
expired.push({ requestId, settle: () => request.reject(error) });
|
|
88
|
+
}
|
|
89
|
+
return expired;
|
|
59
90
|
}
|
|
60
91
|
/**
|
|
61
92
|
* Handles a MutationResponse. `observedTs` is the ts of the last applied Transition, or null when the client's view
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request_manager.js","sourceRoot":"","sources":["../src/request_manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"request_manager.js","sourceRoot":"","sources":["../src/request_manager.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/auth.ts
CHANGED
|
@@ -171,6 +171,8 @@ interface StoredTokens {
|
|
|
171
171
|
|
|
172
172
|
/** Refresh access tokens that expire within this window instead of using them. */
|
|
173
173
|
const EXPIRY_LEEWAY_MS = 10_000;
|
|
174
|
+
/** A refresh request that hasn't answered by then is given up (a black-holed connection after a network switch). */
|
|
175
|
+
const REFRESH_TIMEOUT_MS = 10_000;
|
|
174
176
|
|
|
175
177
|
function defaultStorage(): AuthStorage {
|
|
176
178
|
try {
|
|
@@ -244,6 +246,9 @@ export class RxFerrixAuth {
|
|
|
244
246
|
private refreshing: Promise<string | null> | null = null;
|
|
245
247
|
private removeStorageListener: () => void = () => {};
|
|
246
248
|
|
|
249
|
+
/** Bumped whenever who is signed in may change (sign-in, sign-out, another tab): see `doRefresh`. */
|
|
250
|
+
private epoch = 0;
|
|
251
|
+
|
|
247
252
|
constructor(client: RxFerrixClient, options: RxFerrixAuthOptions = {}) {
|
|
248
253
|
this.client = client;
|
|
249
254
|
this.storage = options.storage ?? defaultStorage();
|
|
@@ -258,6 +263,7 @@ export class RxFerrixAuth {
|
|
|
258
263
|
// Holds the socket's subscriptions back until the stored session (if any) has been sent with Authenticate.
|
|
259
264
|
this.client.setAuth(this.fetchToken, this.onServerAuthChange);
|
|
260
265
|
if (options.syncTabs !== false) this.listenToOtherTabs();
|
|
266
|
+
client.onClose(() => this.dispose());
|
|
261
267
|
}
|
|
262
268
|
|
|
263
269
|
// ---------------------------------------------------------------------------------------------------------------
|
|
@@ -344,6 +350,7 @@ export class RxFerrixAuth {
|
|
|
344
350
|
/** Store fresh tokens and authenticate the socket with them. */
|
|
345
351
|
private async signedIn(tokens: AuthTokens): Promise<void> {
|
|
346
352
|
await this.loaded;
|
|
353
|
+
this.epoch++;
|
|
347
354
|
const t = toStored(tokens);
|
|
348
355
|
await this.saveTokens(t);
|
|
349
356
|
this.setState(this.pendingState(t));
|
|
@@ -395,6 +402,9 @@ export class RxFerrixAuth {
|
|
|
395
402
|
}
|
|
396
403
|
|
|
397
404
|
private async doRefresh(): Promise<string | null> {
|
|
405
|
+
// A sign-in, sign-out or another tab's change while the request is out makes its answer someone else's session:
|
|
406
|
+
// it must not be stored over theirs.
|
|
407
|
+
const epoch = this.epoch;
|
|
398
408
|
const before = this.tokens;
|
|
399
409
|
if (!before) return null;
|
|
400
410
|
const adopted = await this.adoptNewerStoredTokens(before);
|
|
@@ -403,16 +413,19 @@ export class RxFerrixAuth {
|
|
|
403
413
|
if (!t) return null;
|
|
404
414
|
let res: Response;
|
|
405
415
|
try {
|
|
416
|
+
const timeout = (AbortSignal as { timeout?: (ms: number) => AbortSignal }).timeout?.(REFRESH_TIMEOUT_MS);
|
|
406
417
|
res = await this.fetchImpl(`${this.baseUrl}/api/auth/refresh`, {
|
|
407
418
|
method: "POST",
|
|
408
419
|
headers: { "Content-Type": "application/json" },
|
|
409
420
|
body: JSON.stringify({ refreshToken: t.refreshToken }),
|
|
421
|
+
...(timeout ? { signal: timeout } : {}),
|
|
410
422
|
});
|
|
411
423
|
} catch (e) {
|
|
412
424
|
// Offline: keep the session; the next reconnect or AuthError tries again.
|
|
413
425
|
throw new RxFerrixError("Could not reach the server to refresh the session", { code: "network_error", cause: e });
|
|
414
426
|
}
|
|
415
427
|
const body = (await res.json().catch(() => null)) as unknown;
|
|
428
|
+
if (epoch !== this.epoch || this.tokens !== t) return this.tokens?.accessToken ?? null;
|
|
416
429
|
if (res.ok) {
|
|
417
430
|
const next = toStored(body as AuthTokens);
|
|
418
431
|
await this.saveTokens(next);
|
|
@@ -444,6 +457,7 @@ export class RxFerrixAuth {
|
|
|
444
457
|
}
|
|
445
458
|
|
|
446
459
|
private onOtherTabChange(next: StoredTokens | null): void {
|
|
460
|
+
this.epoch++;
|
|
447
461
|
const had = this.tokens;
|
|
448
462
|
this.tokens = next;
|
|
449
463
|
if (!next) {
|
|
@@ -621,6 +635,7 @@ export class RxFerrixAuth {
|
|
|
621
635
|
/** End the session on the server and forget the tokens (also in other tabs). */
|
|
622
636
|
async signOut(): Promise<void> {
|
|
623
637
|
await this.loaded;
|
|
638
|
+
this.epoch++;
|
|
624
639
|
const t = this.tokens;
|
|
625
640
|
await this.saveTokens(null);
|
|
626
641
|
this.client.clearAuth();
|
package/src/client.ts
CHANGED
|
@@ -119,6 +119,27 @@ function randomSessionId(): string {
|
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/** Who a token speaks for, as far as the server's mutation dedup goes: its issuer and `sub` (`null` for no token). A
|
|
123
|
+
* refreshed token keeps it; another user's doesn't. */
|
|
124
|
+
function tokenIdentity(token: string | null): string | null {
|
|
125
|
+
if (token === null) return null;
|
|
126
|
+
const parts = token.split(".");
|
|
127
|
+
try {
|
|
128
|
+
const b64 = (parts[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
|
|
129
|
+
const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
|
|
130
|
+
const payload = JSON.parse(typeof atob === "function" ? atob(padded) : "") as { sub?: unknown; iss?: unknown };
|
|
131
|
+
return `${String(payload.iss ?? "")}|${String(payload.sub ?? "")}`;
|
|
132
|
+
} catch {
|
|
133
|
+
return token;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Whether a JWT's `exp` has passed (a token without one never expires here). */
|
|
138
|
+
function isExpired(token: string): boolean {
|
|
139
|
+
const exp = jwtExpiry(token);
|
|
140
|
+
return exp !== null && exp * 1000 <= Date.now();
|
|
141
|
+
}
|
|
142
|
+
|
|
122
143
|
/** Seconds-since-epoch `exp` claim of a JWT, if the token is a decodable JWT. */
|
|
123
144
|
function jwtExpiry(token: string): number | null {
|
|
124
145
|
const parts = token.split(".");
|
|
@@ -215,6 +236,10 @@ export class RxFerrixClient {
|
|
|
215
236
|
private tokenFetchFailures = 0;
|
|
216
237
|
/** Rejecters of `query()` calls still waiting for their first result (settled on `close()`). */
|
|
217
238
|
private readonly oneShotQueries = new Set<(e: RxFerrixError) => void>();
|
|
239
|
+
/** Run on `close()` (`onClose`). */
|
|
240
|
+
private readonly closeHooks: (() => void)[] = [];
|
|
241
|
+
/** A token fetch failed while the token in hand is expired: sends wait for a token (or `clearAuth`). */
|
|
242
|
+
private awaitingToken = false;
|
|
218
243
|
|
|
219
244
|
private readonly removeUnloadListener: () => void;
|
|
220
245
|
|
|
@@ -293,7 +318,13 @@ export class RxFerrixClient {
|
|
|
293
318
|
finish(() => (state.status === "success" ? resolve(state.value) : reject(state.error)));
|
|
294
319
|
};
|
|
295
320
|
this.oneShotQueries.add(closed);
|
|
296
|
-
|
|
321
|
+
try {
|
|
322
|
+
unsubscribe = watch.onUpdate(check);
|
|
323
|
+
} catch (e) {
|
|
324
|
+
// E.g. `too_many_queries`: nothing will settle it later.
|
|
325
|
+
this.oneShotQueries.delete(closed);
|
|
326
|
+
throw e;
|
|
327
|
+
}
|
|
297
328
|
check();
|
|
298
329
|
});
|
|
299
330
|
}
|
|
@@ -418,9 +449,13 @@ export class RxFerrixClient {
|
|
|
418
449
|
|
|
419
450
|
private sendUnsentRequests(): void {
|
|
420
451
|
if (!this.ws.canSend()) return;
|
|
452
|
+
// Who the server takes these requests from: the identity of the token sent last (Authenticate goes out first).
|
|
453
|
+
const identity = tokenIdentity(this.serverToken);
|
|
454
|
+
const expired = this.requests.expireUnsafeResends(identity);
|
|
455
|
+
if (expired.length > 0) this.finishMutations(expired);
|
|
421
456
|
for (const message of this.requests.unsent()) {
|
|
422
457
|
if (!this.ws.send(message)) break;
|
|
423
|
-
this.requests.markSent(message.requestId);
|
|
458
|
+
this.requests.markSent(message.requestId, identity);
|
|
424
459
|
}
|
|
425
460
|
}
|
|
426
461
|
|
|
@@ -446,6 +481,7 @@ export class RxFerrixClient {
|
|
|
446
481
|
this.auth = null;
|
|
447
482
|
this.authGeneration++;
|
|
448
483
|
this.clearAuthRefreshTimer();
|
|
484
|
+
this.awaitingToken = false;
|
|
449
485
|
this.applyToken(null);
|
|
450
486
|
if (this.ws.isPaused) this.ws.resume();
|
|
451
487
|
this.setAuthenticated(false, previous?.onChange);
|
|
@@ -467,6 +503,13 @@ export class RxFerrixClient {
|
|
|
467
503
|
// have returned may well be valid, so the token in hand (if any) is kept and the fetch is retried with
|
|
468
504
|
// backoff instead of treating the user as signed out.
|
|
469
505
|
this.tokenFetchFailures++;
|
|
506
|
+
// An expired token in hand would only be rejected (and that AuthError would count as the server refusing the
|
|
507
|
+
// session, signing the user out): sends wait for a fresh one instead. (With no token yet, they go out
|
|
508
|
+
// anonymously meanwhile, so an app offline at page load isn't frozen.)
|
|
509
|
+
if (this.authToken !== null && isExpired(this.authToken)) {
|
|
510
|
+
this.awaitingToken = true;
|
|
511
|
+
if (!this.ws.isPaused) this.ws.pause();
|
|
512
|
+
}
|
|
470
513
|
const delay = Math.min(
|
|
471
514
|
TOKEN_FETCH_RETRY_MAX_MS,
|
|
472
515
|
TOKEN_FETCH_RETRY_BASE_MS * 2 ** Math.min(this.tokenFetchFailures - 1, 10),
|
|
@@ -477,6 +520,7 @@ export class RxFerrixClient {
|
|
|
477
520
|
}
|
|
478
521
|
if (generation !== this.authGeneration || this.auth !== auth || this.closed) return;
|
|
479
522
|
this.tokenFetchFailures = 0;
|
|
523
|
+
this.awaitingToken = false;
|
|
480
524
|
this.applyToken(token);
|
|
481
525
|
if (token === null) this.setAuthenticated(false, auth.onChange);
|
|
482
526
|
this.scheduleTokenRefresh(token);
|
|
@@ -484,7 +528,7 @@ export class RxFerrixClient {
|
|
|
484
528
|
this.tokenFetchesInFlight--;
|
|
485
529
|
// Whichever fetch finishes last lets sends through again: a superseded fetch (a newer `setAuth` or
|
|
486
530
|
// `clearAuth`) must not leave the socket paused for good.
|
|
487
|
-
if (this.tokenFetchesInFlight === 0 && this.ws.isPaused && !this.closed) this.ws.resume();
|
|
531
|
+
if (this.tokenFetchesInFlight === 0 && this.ws.isPaused && !this.closed && !this.awaitingToken) this.ws.resume();
|
|
488
532
|
}
|
|
489
533
|
}
|
|
490
534
|
|
|
@@ -566,6 +610,10 @@ export class RxFerrixClient {
|
|
|
566
610
|
|
|
567
611
|
/** A new socket is open and `Connect` was sent: rebuild the server's state from scratch. */
|
|
568
612
|
private onSocketOpen(): void {
|
|
613
|
+
// An access token that expired meanwhile (a laptop waking up) would be rejected: get a fresh one first.
|
|
614
|
+
if (this.auth && this.authToken !== null && isExpired(this.authToken) && this.tokenFetchesInFlight === 0) {
|
|
615
|
+
void this.fetchAndApplyToken(true, true);
|
|
616
|
+
}
|
|
569
617
|
this.remoteQueries.resetVersion();
|
|
570
618
|
this.localQueries.resetForNewSocket();
|
|
571
619
|
this.identityVersion = 0;
|
|
@@ -768,10 +816,23 @@ export class RxFerrixClient {
|
|
|
768
816
|
|
|
769
817
|
// ---------------------------------------------------------------------------------------------------------------
|
|
770
818
|
|
|
819
|
+
/** Calls `hook` when the client is closed (e.g. so that the `RxFerrixAuth` of `getAuth` stops listening to other tabs). */
|
|
820
|
+
onClose(hook: () => void): void {
|
|
821
|
+
if (this.closed) hook();
|
|
822
|
+
else this.closeHooks.push(hook);
|
|
823
|
+
}
|
|
824
|
+
|
|
771
825
|
/** Closes the socket for good. Pending mutations and actions are rejected. */
|
|
772
826
|
close(): void {
|
|
773
827
|
if (this.closed) return;
|
|
774
828
|
this.closed = true;
|
|
829
|
+
for (const hook of this.closeHooks.splice(0)) {
|
|
830
|
+
try {
|
|
831
|
+
hook();
|
|
832
|
+
} catch (e) {
|
|
833
|
+
this.logger.error("onClose hook threw", e);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
775
836
|
this.authGeneration++;
|
|
776
837
|
this.clearAuthRefreshTimer();
|
|
777
838
|
this.ws.stop();
|
package/src/errors.ts
CHANGED
|
@@ -34,3 +34,5 @@ export class RxFerrixError extends Error {
|
|
|
34
34
|
/** Code used for client-side failures (lost connection, client closed). */
|
|
35
35
|
export const CONNECTION_LOST = "ConnectionLost";
|
|
36
36
|
export const CLIENT_CLOSED = "ClientClosed";
|
|
37
|
+
/** A mutation sent before a connection was lost couldn't be sent again safely: it may or may not have run. */
|
|
38
|
+
export const OUTCOME_UNKNOWN = "OutcomeUnknown";
|
package/src/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ export {
|
|
|
22
22
|
type ConnectionState,
|
|
23
23
|
type QueryWatch,
|
|
24
24
|
} from "./client.js";
|
|
25
|
-
export { RxFerrixError, CONNECTION_LOST, CLIENT_CLOSED } from "./errors.js";
|
|
25
|
+
export { RxFerrixError, CONNECTION_LOST, CLIENT_CLOSED, OUTCOME_UNKNOWN } from "./errors.js";
|
|
26
26
|
export { applyDiff, DiffMismatch } from "./diff.js";
|
|
27
27
|
export {
|
|
28
28
|
RxFerrixAuth,
|
package/src/protocol.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type QueryId = number;
|
|
|
6
6
|
export type RequestId = number;
|
|
7
7
|
export type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };
|
|
8
8
|
|
|
9
|
-
export const CLIENT_VERSION = "0.
|
|
9
|
+
export const CLIENT_VERSION = "0.10.0";
|
|
10
10
|
|
|
11
11
|
export interface StateVersion {
|
|
12
12
|
querySet: number;
|
package/src/remote_query_set.ts
CHANGED
|
@@ -16,6 +16,20 @@ export class TransitionVersionMismatch extends Error {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Freezes `value` and everything in it not frozen yet, and returns it. Query results are frozen: they are the base the
|
|
21
|
+
* server's next diff applies to, and they are shared with every caller (`useQuery`, `getQuery` in optimistic updates),
|
|
22
|
+
* so changing one in place (`results.sort()`, `store.getQuery(q).push(x)`) would silently corrupt every later update.
|
|
23
|
+
* Freezing makes that a `TypeError` instead. A diff reuses the unchanged parts of the previous result (frozen already),
|
|
24
|
+
* so only what it built is walked. Typed arrays (`Bytes`) can't be frozen and are left as they are.
|
|
25
|
+
*/
|
|
26
|
+
export function deepFreeze<T>(value: T): T {
|
|
27
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value) || ArrayBuffer.isView(value)) return value;
|
|
28
|
+
Object.freeze(value);
|
|
29
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
export interface TransitionResult {
|
|
20
34
|
/** Queries whose server result changed (updated, failed or removed). */
|
|
21
35
|
changed: QueryId[];
|
|
@@ -66,7 +80,7 @@ export class RemoteQuerySet {
|
|
|
66
80
|
case "QueryUpdated":
|
|
67
81
|
this.results.set(queryId, {
|
|
68
82
|
status: "success",
|
|
69
|
-
value: "diff" in modification ? diffed.get(modification) : modification.value,
|
|
83
|
+
value: deepFreeze("diff" in modification ? diffed.get(modification) : modification.value),
|
|
70
84
|
});
|
|
71
85
|
break;
|
|
72
86
|
case "QueryFailed":
|
package/src/request_manager.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CONNECTION_LOST, RxFerrixError } from "./errors.js";
|
|
1
|
+
import { CONNECTION_LOST, OUTCOME_UNKNOWN, RxFerrixError } from "./errors.js";
|
|
2
2
|
import type {
|
|
3
3
|
ActionMessage,
|
|
4
4
|
ActionResponseMessage,
|
|
@@ -9,6 +9,13 @@ import type {
|
|
|
9
9
|
|
|
10
10
|
type Settle = { resolve: (value: unknown) => void; reject: (error: unknown) => void };
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* How long after its first send a mutation may still be sent again. The server recognizes a resent mutation (and
|
|
14
|
+
* answers with the recorded result instead of running it again) for 10 minutes (docs/PROTOCOL.md); later, a resend
|
|
15
|
+
* could run it twice. Kept below that, for clock and transit slack.
|
|
16
|
+
*/
|
|
17
|
+
export const RESEND_WINDOW_MS = 9 * 60_000;
|
|
18
|
+
|
|
12
19
|
type MutationRequest = Settle & {
|
|
13
20
|
kind: "mutation";
|
|
14
21
|
message: MutationMessage;
|
|
@@ -17,6 +24,8 @@ type MutationRequest = Settle & {
|
|
|
17
24
|
/** Set once a successful MutationResponse arrived: waiting for a Transition with ts >= this. */
|
|
18
25
|
completed: { ts: number; result: unknown } | null;
|
|
19
26
|
startedAt: number;
|
|
27
|
+
/** When it first went out, and as whom (`identity` of the client then): a resend must match both. */
|
|
28
|
+
firstSent: { at: number; identity: string | null } | null;
|
|
20
29
|
};
|
|
21
30
|
|
|
22
31
|
type ActionRequest = Settle & {
|
|
@@ -58,6 +67,7 @@ export class RequestManager {
|
|
|
58
67
|
sent: false,
|
|
59
68
|
completed: null,
|
|
60
69
|
startedAt: Date.now(),
|
|
70
|
+
firstSent: null,
|
|
61
71
|
resolve,
|
|
62
72
|
reject,
|
|
63
73
|
});
|
|
@@ -88,10 +98,35 @@ export class RequestManager {
|
|
|
88
98
|
return out;
|
|
89
99
|
}
|
|
90
100
|
|
|
91
|
-
/** The message went out on the current socket. */
|
|
92
|
-
markSent(requestId: RequestId): void {
|
|
101
|
+
/** The message went out on the current socket, from `identity` (whom the socket is authenticated as). */
|
|
102
|
+
markSent(requestId: RequestId, identity: string | null = null): void {
|
|
93
103
|
const request = this.requests.get(requestId);
|
|
94
|
-
if (request)
|
|
104
|
+
if (!request) return;
|
|
105
|
+
request.sent = true;
|
|
106
|
+
if (request.kind === "mutation" && !request.firstSent) request.firstSent = { at: Date.now(), identity };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Mutations waiting to be sent again that can't be safely: sent more than {@link RESEND_WINDOW_MS} ago (the server
|
|
111
|
+
* no longer recognizes them, so they could run twice), or from another identity than now (the server never matches
|
|
112
|
+
* a resend across identities: it would run, or fail, as someone else). They are taken off and returned, rejected
|
|
113
|
+
* with {@link OUTCOME_UNKNOWN}: whether the first send ran isn't known.
|
|
114
|
+
*/
|
|
115
|
+
expireUnsafeResends(identity: string | null, now: number = Date.now()): SettledMutation[] {
|
|
116
|
+
const expired: SettledMutation[] = [];
|
|
117
|
+
for (const [requestId, request] of this.requests) {
|
|
118
|
+
if (request.kind !== "mutation" || request.sent || request.completed || !request.firstSent) continue;
|
|
119
|
+
const tooOld = now - request.firstSent.at > RESEND_WINDOW_MS;
|
|
120
|
+
if (!tooOld && request.firstSent.identity === identity) continue;
|
|
121
|
+
this.requests.delete(requestId);
|
|
122
|
+
const why = tooOld ? "too long ago to be sent again safely" : "as another user than now";
|
|
123
|
+
const error = new RxFerrixError(
|
|
124
|
+
`The connection was lost after mutation "${request.message.udfPath}" was sent, ${why}: it may or may not have run`,
|
|
125
|
+
{ code: OUTCOME_UNKNOWN },
|
|
126
|
+
);
|
|
127
|
+
expired.push({ requestId, settle: () => request.reject(error) });
|
|
128
|
+
}
|
|
129
|
+
return expired;
|
|
95
130
|
}
|
|
96
131
|
|
|
97
132
|
/**
|