@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.
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
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.8.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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AAyItC,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"}
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"}
@@ -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;qBAC9E,CAAC,CAAC;oBACH,MAAM;gBACR,KAAK,aAAa;oBAChB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE;wBACxB,MAAM,EAAE,OAAO;wBACf,KAAK,EAAE,aAAa,CAAC,UAAU,CAAC,YAAY,CAAC,YAAY,EAAE,YAAY,CAAC,SAAS,CAAC;qBACnF,CAAC,CAAC;oBACH,MAAM;gBACR,KAAK,cAAc;oBACjB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;oBAC7B,MAAM;gBACR;oBACE,SAAS;YACb,CAAC;YACD,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACtB,IAAI,UAAU,IAAI,YAAY,IAAI,YAAY,CAAC,QAAQ,IAAI,YAAY,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC5F,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC3D,CAAC;QACH,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;QAC1C,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAC/B,CAAC;IAED,0EAA0E;IAC1E,MAAM,CAAC,OAAgB;QACrB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;IAED;;;OAGG;IACH,YAAY;QACV,IAAI,CAAC,QAAQ,GAAG,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;IACtD,CAAC;CACF"}
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,EAAmB,aAAa,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,KAAK,EACV,aAAa,EACb,qBAAqB,EACrB,eAAe,EACf,uBAAuB,EACvB,SAAS,EACV,MAAM,eAAe,CAAC;AAuBvB,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;IAczD,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC;IAarD,kFAAkF;IAClF,MAAM,IAAI,CAAC,eAAe,GAAG,aAAa,CAAC,EAAE;IAU7C,kDAAkD;IAClD,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAKpC;;;;;;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"}
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"}
@@ -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
- request.sent = true;
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":"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"}
1
+ {"version":3,"file":"request_manager.js","sourceRoot":"","sources":["../src/request_manager.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rxferrix/client",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Framework-agnostic reactive sync client for RxFerrix",
5
5
  "license": "MIT",
6
6
  "homepage": "https://genesysflow.github.io/rxferrix/",
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
- unsubscribe = watch.onUpdate(check);
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.8.0";
9
+ export const CLIENT_VERSION = "0.10.0";
10
10
 
11
11
  export interface StateVersion {
12
12
  querySet: number;
@@ -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":
@@ -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) request.sent = true;
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
  /**