@rocicorp/zero 0.22.2025071100 → 0.22.2025071101

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,6 +1,6 @@
1
1
  import {
2
2
  clientToServer
3
- } from "./chunk-ZRSXCDPE.js";
3
+ } from "./chunk-5A6QECBY.js";
4
4
  import {
5
5
  AbstractQuery,
6
6
  ExpressionBuilder,
@@ -337,4 +337,4 @@ export {
337
337
  querify,
338
338
  namedQuery
339
339
  };
340
- //# sourceMappingURL=chunk-ZVMEFHVU.js.map
340
+ //# sourceMappingURL=chunk-4FQJO7OM.js.map
@@ -5916,7 +5916,8 @@ function encodeSecProtocols(initConnectionMessage, authToken) {
5916
5916
  initConnectionMessage,
5917
5917
  authToken
5918
5918
  };
5919
- return encodeURIComponent(btoa(JSON.stringify(protocols)));
5919
+ const bytes = new TextEncoder().encode(JSON.stringify(protocols));
5920
+ return encodeURIComponent(btoa(String.fromCharCode(...bytes)));
5920
5921
  }
5921
5922
 
5922
5923
  // ../zero-protocol/src/error.ts
@@ -8537,7 +8538,7 @@ function makeMessage(message, context, logLevel) {
8537
8538
  }
8538
8539
 
8539
8540
  // ../zero-client/src/client/version.ts
8540
- var version2 = "0.22.2025071100";
8541
+ var version2 = "0.22.2025071101";
8541
8542
 
8542
8543
  // ../zero-client/src/client/log-options.ts
8543
8544
  var LevelFilterLogSink = class {
@@ -8833,9 +8834,7 @@ var State = class {
8833
8834
 
8834
8835
  // ../zero-client/src/client/mutation-tracker.ts
8835
8836
  import { resolver as resolver8 } from "@rocicorp/resolver";
8836
- var transientPushErrorTypes = [
8837
- "zeroPusher",
8838
- "http",
8837
+ var completeFailureTypes = [
8839
8838
  // These should never actually be received as they cause the websocket
8840
8839
  // connection to be closed.
8841
8840
  "unsupportedPushVersion",
@@ -8848,14 +8847,20 @@ function nextEphemeralID() {
8848
8847
  var MutationTracker = class {
8849
8848
  #outstandingMutations;
8850
8849
  #ephemeralIDsByMutationID;
8851
- #allMutationsConfirmedListeners;
8850
+ #allMutationsAppliedListeners;
8852
8851
  #lc;
8852
+ #limboMutations;
8853
8853
  #clientID;
8854
+ #largestOutstandingMutationID;
8855
+ #currentMutationID;
8854
8856
  constructor(lc) {
8855
8857
  this.#lc = lc.withContext("MutationTracker");
8856
8858
  this.#outstandingMutations = /* @__PURE__ */ new Map();
8857
8859
  this.#ephemeralIDsByMutationID = /* @__PURE__ */ new Map();
8858
- this.#allMutationsConfirmedListeners = /* @__PURE__ */ new Set();
8860
+ this.#allMutationsAppliedListeners = /* @__PURE__ */ new Set();
8861
+ this.#limboMutations = /* @__PURE__ */ new Set();
8862
+ this.#largestOutstandingMutationID = 0;
8863
+ this.#currentMutationID = 0;
8859
8864
  }
8860
8865
  set clientID(clientID) {
8861
8866
  this.#clientID = clientID;
@@ -8873,6 +8878,10 @@ var MutationTracker = class {
8873
8878
  if (entry) {
8874
8879
  entry.mutationID = mutationID;
8875
8880
  this.#ephemeralIDsByMutationID.set(mutationID, id);
8881
+ this.#largestOutstandingMutationID = Math.max(
8882
+ this.#largestOutstandingMutationID,
8883
+ mutationID
8884
+ );
8876
8885
  }
8877
8886
  }
8878
8887
  /**
@@ -8904,7 +8913,7 @@ var MutationTracker = class {
8904
8913
  * to those mutations have been lost.
8905
8914
  *
8906
8915
  * An example case: the API server responds while the connection
8907
- * is down. Those responses are dropped.
8916
+ * is down. Those responses are lost.
8908
8917
  *
8909
8918
  * Mutations whose LMID is > the lastMutationID are not resolved
8910
8919
  * since they will be retried by the client, giving us another chance
@@ -8913,9 +8922,6 @@ var MutationTracker = class {
8913
8922
  * The only way to ensure that all API server responses are
8914
8923
  * received would be to have the API server write them
8915
8924
  * to the DB while writing the LMID.
8916
- *
8917
- * This would have the downside of not being able to provide responses to a
8918
- * mutation with data gathered after the transaction.
8919
8925
  */
8920
8926
  onConnected(lastMutationID) {
8921
8927
  for (const [id, entry] of this.#outstandingMutations) {
@@ -8928,12 +8934,78 @@ var MutationTracker = class {
8928
8934
  break;
8929
8935
  }
8930
8936
  }
8937
+ for (const [id, entry] of this.#outstandingMutations) {
8938
+ if (entry.mutationID && entry.mutationID > lastMutationID) {
8939
+ this.#limboMutations.add(id);
8940
+ }
8941
+ }
8942
+ this.lmidAdvanced(lastMutationID);
8943
+ }
8944
+ /**
8945
+ * lmid advance will:
8946
+ * 1. notify "allMutationsApplied" listeners if the lastMutationID
8947
+ * is greater than or equal to the largest outstanding mutation ID.
8948
+ * 2. resolve all limbo mutations whose mutation ID is less than or equal to
8949
+ * the lastMutationID.
8950
+ *
8951
+ * We only resolve "limbo mutations" since we want to give the mutation
8952
+ * responses a chance to be received. `poke` and `pushResponse` are non transactional
8953
+ * so they race.
8954
+ *
8955
+ * E.g., a `push` may call the api server which:
8956
+ * writes to PG, replicates to zero-cache, then pokes the lmid down before the
8957
+ * push response is sent.
8958
+ *
8959
+ * The only fix for this would be to have mutation responses be written to the database
8960
+ * and sent down through the poke protocol.
8961
+ *
8962
+ * The artifact the user sees is that promise resolutions for mutations is not transactional
8963
+ * with the update to synced data.
8964
+ *
8965
+ * It was a mistake to not just write the mutation responses to the database
8966
+ * in the first place as this route ends up being more complicated
8967
+ * and less reliable.
8968
+ */
8969
+ lmidAdvanced(lastMutationID) {
8970
+ assert(
8971
+ lastMutationID >= this.#currentMutationID,
8972
+ "lmid must be greater than or equal to current lmid"
8973
+ );
8974
+ if (lastMutationID === this.#currentMutationID) {
8975
+ return;
8976
+ }
8977
+ try {
8978
+ this.#currentMutationID = lastMutationID;
8979
+ this.#resolveLimboMutations(lastMutationID);
8980
+ } finally {
8981
+ if (lastMutationID >= this.#largestOutstandingMutationID) {
8982
+ this.#notifyAllMutationsAppliedListeners();
8983
+ }
8984
+ }
8931
8985
  }
8932
8986
  get size() {
8933
8987
  return this.#outstandingMutations.size;
8934
8988
  }
8989
+ /**
8990
+ * Push errors fall into two categories:
8991
+ * - Those where we know the mutations were not applied
8992
+ * - Those where we do not know the state of the mutations.
8993
+ *
8994
+ * The first category includes errors like "unsupportedPushVersion"
8995
+ * and "unsupportedSchemaVersion". The mutations were never applied in those cases.
8996
+ *
8997
+ * The second category includes errors like "http" errors. E.g., a 500 on the user's
8998
+ * API server or a network error.
8999
+ *
9000
+ * The mutations may have been applied by the API server but we never
9001
+ * received the response.
9002
+ *
9003
+ * In this latter case, we must mark the mutations as being in limbo.
9004
+ * This allows us to resolve them when we receive the next
9005
+ * lmid bump if their lmids are lesser.
9006
+ */
8935
9007
  #processPushError(error) {
8936
- if (transientPushErrorTypes.includes(error.error)) {
9008
+ if (completeFailureTypes.includes(error.error)) {
8937
9009
  return;
8938
9010
  }
8939
9011
  const mids = error.mutationIDs;
@@ -8941,7 +9013,30 @@ var MutationTracker = class {
8941
9013
  return;
8942
9014
  }
8943
9015
  for (const mid of mids) {
8944
- this.#processMutationError(mid, error);
9016
+ const ephemeralID = this.#ephemeralIDsByMutationID.get(mid.id);
9017
+ if (ephemeralID) {
9018
+ if (mid.id <= this.#currentMutationID) {
9019
+ const entry = this.#outstandingMutations.get(ephemeralID);
9020
+ if (entry) {
9021
+ this.#settleMutation(ephemeralID, entry, "resolve", emptyObject);
9022
+ }
9023
+ continue;
9024
+ }
9025
+ this.#limboMutations.add(ephemeralID);
9026
+ }
9027
+ }
9028
+ }
9029
+ #resolveLimboMutations(lastMutationID) {
9030
+ for (const id of this.#limboMutations) {
9031
+ const entry = this.#outstandingMutations.get(id);
9032
+ if (!entry || !entry.mutationID) {
9033
+ this.#limboMutations.delete(id);
9034
+ continue;
9035
+ }
9036
+ if (entry.mutationID <= lastMutationID) {
9037
+ this.#limboMutations.delete(id);
9038
+ this.#settleMutation(id, entry, "resolve", emptyObject);
9039
+ }
8945
9040
  }
8946
9041
  }
8947
9042
  #processPushOk(ok) {
@@ -8994,19 +9089,27 @@ var MutationTracker = class {
8994
9089
  entry.resolver.reject(result);
8995
9090
  break;
8996
9091
  }
8997
- const removed = this.#outstandingMutations.delete(ephemeralID);
9092
+ this.#outstandingMutations.delete(ephemeralID);
8998
9093
  if (entry.mutationID) {
8999
9094
  this.#ephemeralIDsByMutationID.delete(entry.mutationID);
9000
9095
  }
9001
- if (removed && this.#outstandingMutations.size === 0) {
9002
- this.#notifyAllMutationsConfirmedListeners();
9003
- }
9096
+ this.#limboMutations.delete(ephemeralID);
9004
9097
  }
9005
- onAllMutationsConfirmed(listener) {
9006
- this.#allMutationsConfirmedListeners.add(listener);
9098
+ /**
9099
+ * Be notified when all mutations have been included in the server snapshot.
9100
+ *
9101
+ * The query manager will not de-register queries from the server until there
9102
+ * are no pending mutations.
9103
+ *
9104
+ * The reason is that a mutation may need to be rebased. We do not want
9105
+ * data that was available the first time it was run to not be available
9106
+ * on a rebase.
9107
+ */
9108
+ onAllMutationsApplied(listener) {
9109
+ this.#allMutationsAppliedListeners.add(listener);
9007
9110
  }
9008
- #notifyAllMutationsConfirmedListeners() {
9009
- for (const listener of this.#allMutationsConfirmedListeners) {
9111
+ #notifyAllMutationsAppliedListeners() {
9112
+ for (const listener of this.#allMutationsAppliedListeners) {
9010
9113
  listener();
9011
9114
  }
9012
9115
  }
@@ -9039,7 +9142,7 @@ var QueryManager = class {
9039
9142
  this.#send = send2;
9040
9143
  this.#mutationTracker = mutationTracker;
9041
9144
  this.#queryChangeThrottleMs = queryChangeThrottleMs;
9042
- this.#mutationTracker.onAllMutationsConfirmed(() => {
9145
+ this.#mutationTracker.onAllMutationsApplied(() => {
9043
9146
  if (this.#pendingRemovals.length === 0) {
9044
9147
  return;
9045
9148
  }
@@ -10735,6 +10838,7 @@ var Zero = class _Zero {
10735
10838
  #handlePokeEnd(_lc, pokeMessage) {
10736
10839
  this.#abortPingTimeout();
10737
10840
  this.#pokeHandler.handlePokeEnd(pokeMessage[1]);
10841
+ this.#mutationTracker.lmidAdvanced(this.#lastMutationIDReceived);
10738
10842
  }
10739
10843
  #onPokeError() {
10740
10844
  const lc = this.#lc;
@@ -11241,4 +11345,4 @@ export {
11241
11345
  update_needed_reason_type_enum_exports,
11242
11346
  Zero
11243
11347
  };
11244
- //# sourceMappingURL=chunk-ZRSXCDPE.js.map
11348
+ //# sourceMappingURL=chunk-5A6QECBY.js.map