@peabody-soft/mbs-model-ts 1.5.0 → 1.7.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.
Files changed (52) hide show
  1. package/dist/Address.js +8 -11
  2. package/dist/Attachment.js +7 -8
  3. package/dist/AttachmentBaseUrl.js +10 -10
  4. package/dist/AuthTokens.js +12 -7
  5. package/dist/Business.js +19 -19
  6. package/dist/BusinessIdentifier.js +2 -2
  7. package/dist/BusinessLink.js +16 -16
  8. package/dist/BusinessProfile.js +4 -4
  9. package/dist/CartEditor.js +137 -166
  10. package/dist/Catalog.js +19 -25
  11. package/dist/Constants.js +10 -10
  12. package/dist/ContainerNames.js +10 -11
  13. package/dist/Entity.js +11 -9
  14. package/dist/Enums.js +2 -2
  15. package/dist/Exceptions.js +2 -2
  16. package/dist/GeneralSettings.js +23 -21
  17. package/dist/HttpStatusCodes.js +10 -11
  18. package/dist/Ids.js +24 -30
  19. package/dist/Item.js +6 -7
  20. package/dist/LatLong.js +3 -3
  21. package/dist/LiveData.js +6 -6
  22. package/dist/Measures.js +2 -2
  23. package/dist/Message.js +6 -6
  24. package/dist/MockDatabase.js +2 -2
  25. package/dist/Model.js +294 -365
  26. package/dist/Order.js +20 -17
  27. package/dist/Package.js +7 -8
  28. package/dist/PackageIdentifier.js +2 -2
  29. package/dist/PaymentMethodDetail.js +4 -4
  30. package/dist/Photo.js +6 -7
  31. package/dist/Price.js +8 -9
  32. package/dist/PriceList.js +2 -2
  33. package/dist/Product.js +17 -22
  34. package/dist/ProductCategory.js +3 -4
  35. package/dist/Profile.js +26 -27
  36. package/dist/ProtoMap.d.ts +3 -0
  37. package/dist/ProtoMap.js +288 -294
  38. package/dist/Routes.js +3 -3
  39. package/dist/ServiceExceptions.js +11 -12
  40. package/dist/ServiceProxy.js +19 -23
  41. package/dist/ShoppingList.js +112 -130
  42. package/dist/SyncCache.d.ts +3 -0
  43. package/dist/SyncCache.js +92 -75
  44. package/dist/SyncKeys.js +8 -8
  45. package/dist/SyncState.js +5 -5
  46. package/dist/Tax.js +16 -21
  47. package/dist/TaxHelper.js +22 -30
  48. package/dist/TaxType.js +8 -9
  49. package/dist/UserProfile.js +4 -4
  50. package/dist/WorkingDays.js +15 -15
  51. package/dist/index.js +9 -9
  52. package/package.json +1 -1
package/dist/ProtoMap.js CHANGED
@@ -62,7 +62,8 @@ import Tax from "./Tax";
62
62
  import UserProfile from "./UserProfile";
63
63
  import WorkingDays, { WorkingHours, WorkingHoursRange } from "./WorkingDays";
64
64
  // #region A response and the blobs it is cached as
65
- // Every entity of a response, in declaration order (parents before children). A tombstone is included, flagged.
65
+ // Every entity of a response, in declaration order (parents before children). Tombstones are
66
+ // included, flagged.
66
67
  export function getBusinessResponseEntities(response) {
67
68
  const result = [];
68
69
  if (response.business !== undefined)
@@ -85,8 +86,8 @@ export function getBusinessResponseEntities(response) {
85
86
  result.push(toItemWireEntity(price.item, proto_Price.toBinary(price)));
86
87
  return result;
87
88
  }
88
- // Every entity of a profile sync out response: the caller, their links, and the businesses those links name.
89
- // The three have separate cursors, so unlike a business response the order between the arrays does not matter.
89
+ // Every entity of a profile response: the caller, their links, and the businesses those name. The
90
+ // three have separate cursors, so unlike a business response the array order does not matter.
90
91
  export function getProfileResponseEntities(response) {
91
92
  const result = [];
92
93
  if (response.user !== undefined)
@@ -97,13 +98,12 @@ export function getProfileResponseEntities(response) {
97
98
  result.push(toWireEntity(business.entity, proto_Business.toBinary(business)));
98
99
  return result;
99
100
  }
100
- // What a business response is cached as: one blob per live entity, the ids its tombstones named,
101
- // and the attachment base url in a blob of its own, since it belongs to the business rather than to
102
- // any entity of it.
101
+ // One blob per entity, tombstones included and as sent, plus the attachment base url in a blob of its
102
+ // own (it belongs to the business, not to an entity).
103
103
  export function businessResponseToCacheWrite(response) {
104
104
  const write = toCacheWrite(getBusinessResponseEntities(response));
105
- // Its eTag is its expiry, which is exactly its cursor: a client sends back the expiry it holds
106
- // and is sent a later url only when one exists
105
+ // Its eTag is its expiry, which is also its cursor: a client sends back the expiry it holds and
106
+ // gets a later url only if one exists.
107
107
  const attachmentBaseUrl = response.attachmentBaseUrl;
108
108
  if (attachmentBaseUrl !== undefined)
109
109
  write.blobs.push({
@@ -113,36 +113,32 @@ export function businessResponseToCacheWrite(response) {
113
113
  });
114
114
  return write;
115
115
  }
116
- // The same for a profile response, which carries nothing but entities.
116
+ // The same for a profile response, which holds only entities.
117
117
  export function profileResponseToCacheWrite(response) {
118
118
  return toCacheWrite(getProfileResponseEntities(response));
119
119
  }
120
- // The expiry of the attachment base url a response carried, and zero when it carried none, which
121
- // is exactly what the next request sends back.
122
- // Here rather than at a call site, because reading a field of a response is this file's job.
120
+ // The expiry of the attachment base url a response carried, or zero when none. The next request
121
+ // sends it back.
123
122
  export function getBusinessResponseAttachmentBaseUrlExpiry(response) {
124
123
  return response?.attachmentBaseUrl?.expiry ?? 0n;
125
124
  }
126
- // One order as its own blob, for a caller holding a message rather than a whole response. The
127
- // payload is the only thing that says which a message is, so one carrying the other is refused
128
- // here rather than stored where a read would never find it.
129
- // heldETag is what the container holds that order at, and null when it holds none - see messageToBlob.
125
+ // One order as its own blob, for a caller holding a message rather than a response. Only the payload
126
+ // says what a message is, so a non-order is refused here instead of stored where a read would not
127
+ // find it. heldETag is the eTag the container holds the order at, or null - see messageToBlob.
130
128
  export function orderToBlob(order, heldETag) {
131
129
  if (order.payload.oneofKind !== "salesOrder")
132
130
  throw new InvalidArgumentException("That message carries no sales order, so it is not an order");
133
131
  return messageToBlob(order, heldETag);
134
132
  }
135
- // Whether a message carries party preferences. Here rather than at a call site, because reading a
136
- // field of a message is this file's job.
133
+ // Whether a message carries party preferences.
137
134
  export function isPartyPreferencesMessage(message) {
138
135
  return message.payload.oneofKind === "partyPreferences";
139
136
  }
140
- // When a message was last written: the field to compare two copies of one thing on when they are
141
- // held at different ids, and so have no version to be compared by.
137
+ // When a message was last written. It compares two copies of one thing held at different ids, which
138
+ // have no shared version.
142
139
  export function getMessageUpdatedAt(message) {
143
140
  return getEntity(message.entity).updatedAt;
144
141
  }
145
- // Here rather than at a call site, because reading a field of a message is this file's job.
146
142
  export function getMessageId(message) {
147
143
  return getEntity(message.entity).id;
148
144
  }
@@ -152,10 +148,9 @@ export function partyPreferencesToBlob(partyPreferences, heldETag) {
152
148
  throw new InvalidArgumentException("That message carries no party preferences");
153
149
  return messageToBlob(partyPreferences, heldETag);
154
150
  }
155
- // The response the blobs of a business container make up: everything cached for that business, in
156
- // the shape the call that brought it returned, and null when they hold nothing of it yet.
157
- // The entity type in each blob id sorts them, so no stored type tag is needed; one this library
158
- // does not know is skipped rather than refused, so a newer server cannot break an older client.
151
+ // Rebuilds the response a business container's blobs make up, as the call returned it. Null when they
152
+ // hold nothing yet. The entity type in each blob id sorts them, so no type tag is stored. An unknown
153
+ // type is skipped, so a newer server cannot break an older client.
159
154
  export function businessResponseFromBlobs(blobs) {
160
155
  const response = proto_BusinessSyncOutResponse.create({});
161
156
  let hasContent = false;
@@ -202,7 +197,7 @@ export function businessResponseFromBlobs(blobs) {
202
197
  }
203
198
  return hasContent ? response : null;
204
199
  }
205
- // The same for a profile container, whose three entity types are siblings rather than a hierarchy.
200
+ // The same for a profile container, whose three entity types are siblings.
206
201
  export function profileResponseFromBlobs(blobs) {
207
202
  const response = proto_ProfileSyncOutResponse.create({});
208
203
  let hasContent = false;
@@ -226,28 +221,66 @@ export function profileResponseFromBlobs(blobs) {
226
221
  }
227
222
  return hasContent ? response : null;
228
223
  }
229
- // The orders a container holds. The payload a message carries is what says whether it is an
230
- // order or a cart - Message.messageType is not read, because here the payload is typed.
231
- export function orderMessagesFromBlobs(blobs) {
232
- const result = [];
233
- for (const message of messagesFromBlobs(blobs))
234
- if (message.payload.oneofKind === "salesOrder")
235
- result.push(message);
236
- return result;
224
+ // The tombstones a business container holds, as a response, each as stored (and so as sent). Null
225
+ // when it holds none. The attachment base url belongs to no entity, so it is left out.
226
+ export function deletedBusinessResponseFromBlobs(blobs) {
227
+ const held = businessResponseFromBlobs(blobs);
228
+ if (held === null)
229
+ return null;
230
+ const deleted = proto_BusinessSyncOutResponse.create({
231
+ business: held.business !== undefined && isDeletedEntity(getEntity(held.business.entity))
232
+ ? held.business
233
+ : undefined,
234
+ attachments: held.attachments.filter((attachment) => isDeletedEntity(getEntity(attachment.entity))),
235
+ configurations: held.configurations.filter((configuration) => isDeletedEntity(getEntity(configuration.entity))),
236
+ messages: held.messages.filter((message) => isDeletedEntity(getEntity(message.entity))),
237
+ productCategories: held.productCategories.filter((category) => isDeletedItem(category.item)),
238
+ products: held.products.filter((product) => isDeletedItem(product.item)),
239
+ packages: held.packages.filter((pkg) => isDeletedItem(pkg.item)),
240
+ priceLists: held.priceLists.filter((priceList) => isDeletedItem(priceList.item)),
241
+ prices: held.prices.filter((price) => isDeletedItem(price.item)),
242
+ });
243
+ return businessResponseEntityCount(deleted) === 0 ? null : deleted;
237
244
  }
238
- // The party preferences message among messages already in hand, or null when none of them is
239
- // one. A customer has one per business, so two means two callers' messages are in one container -
240
- // the most recently updated then wins for want of anything better: nothing in a message says
241
- // which caller it was sent to - see THE MESSAGES OF A BUSINESS ARE NOT PUBLIC.
242
- // Split out of partyPreferencesMessageFromBlobs so a caller already holding a response's messages
243
- // - such as Model.businessSyncIn folding the anonymous container in - can settle its cart without
244
- // asking the store for the same blobs again. buildBusiness takes the cart it shows from here too, so
245
- // that the cart shown, the cart amended and the cart carried over a sign in are always one message.
245
+ // The same for a profile container.
246
+ export function deletedProfileResponseFromBlobs(blobs) {
247
+ const held = profileResponseFromBlobs(blobs);
248
+ if (held === null)
249
+ return null;
250
+ const deleted = proto_ProfileSyncOutResponse.create({
251
+ user: held.user !== undefined && isDeletedEntity(getEntity(held.user.entity)) ? held.user : undefined,
252
+ businessLinks: held.businessLinks.filter((businessLink) => isDeletedEntity(getEntity(businessLink.entity))),
253
+ businesses: held.businesses.filter((business) => isDeletedEntity(getEntity(business.entity))),
254
+ });
255
+ return profileResponseEntityCount(deleted) === 0 ? null : deleted;
256
+ }
257
+ // The open orders a container holds, which the business has not completed. The payload says whether
258
+ // a message is an order or a cart. Message.messageType is not read, since the payload is typed.
259
+ export function orderMessagesFromBlobs(blobs) {
260
+ return ordersAmong(messagesFromBlobs(blobs, false));
261
+ }
262
+ // The orders the business has completed, by fulfilling or cancelling. The merchant app completes an
263
+ // order by deleting its message, so these are tombstones, but a server sends them whole because a
264
+ // customer is shown the order afterwards. A tombstone with no order (left by an older server that
265
+ // sent only the header) is not one.
266
+ export function deletedOrderMessagesFromBlobs(blobs) {
267
+ return ordersAmong(messagesFromBlobs(blobs, true));
268
+ }
269
+ function ordersAmong(messages) {
270
+ return messages.filter((message) => message.payload.oneofKind === "salesOrder");
271
+ }
272
+ // The party preferences message among these messages, or null. A customer has one per business, so
273
+ // two means two callers' messages share a container. The greater update number wins for want of
274
+ // anything better, since nothing in a message says which caller it was sent to. See THE MESSAGES OF
275
+ // A BUSINESS ARE NOT PUBLIC.
276
+ // Split from partyPreferencesMessageFromBlobs so a caller already holding the messages (such as
277
+ // Model.businessSyncIn folding the anonymous container in) need not read the blobs again.
278
+ // buildBusiness uses it too, so the cart shown, amended and carried over a sign in is one message.
246
279
  export function partyPreferencesMessageFromMessages(messages) {
247
280
  let result = null;
248
281
  for (const message of messages) {
249
282
  if (isDeletedEntity(getEntity(message.entity)))
250
- continue; // A tombstone is not a cart, whatever it carries
283
+ continue; // A tombstone is not a cart
251
284
  if (message.payload.oneofKind !== "partyPreferences")
252
285
  continue;
253
286
  if (result === null ||
@@ -263,17 +296,17 @@ export function partyPreferencesMessageFromBlobs(blobs) {
263
296
  }
264
297
  // #endregion
265
298
  // #region Requests and credentials
266
- // The request that asks for everything changed since these cursors, about the business it is given: a bigint
267
- // is the entity id of the Business, which a caller that has read it before has, and a string is its
268
- // url friendly businessId, which is all that one reading it for the first time has.
269
- // attachmentBaseUrlExpiry is not a cursor, just the expiry of the url already held, sent back unchanged (zero when none is held).
299
+ // The request for everything changed since these cursors. A bigint business is the Business entity
300
+ // id, which a caller that has read it before has. A string is its url friendly businessId, which is
301
+ // all a first read has. attachmentBaseUrlExpiry is not a cursor but the expiry of the url already
302
+ // held, sent back unchanged (zero when none).
270
303
  export function buildBusinessSyncOutRequest(business, syncInState, attachmentBaseUrlExpiry) {
271
304
  return createBusinessSyncOutRequest(typeof business === "bigint"
272
305
  ? { oneofKind: "businessEntityId", businessEntityId: business }
273
306
  : { oneofKind: "businessId", businessId: business }, syncInState, attachmentBaseUrlExpiry);
274
307
  }
275
- // Takes the business as the request holds it, so that the request after a page can name it exactly as
276
- // the one before did, whichever way that was
308
+ // Takes the business as the request holds it, so the request after a page names it the same way as
309
+ // the one before.
277
310
  function createBusinessSyncOutRequest(business, syncInState, attachmentBaseUrlExpiry) {
278
311
  return proto_BusinessSyncOutRequest.create({
279
312
  business: business,
@@ -285,11 +318,10 @@ function createBusinessSyncOutRequest(business, syncInState, attachmentBaseUrlEx
285
318
  itemUpdateNumber: syncInState.getUpdateNumber(proto_Constants.ITEM_ENTITY_TYPE_ID),
286
319
  });
287
320
  }
288
- // How far a container has been read, as the blobs that keep it: one for each key that a page has
289
- // moved, at an id of its own - see Ids.getSyncCursorBlobId. The cursor is the eTag and there is
290
- // nothing else to keep, so there are no bytes. A cursor is therefore only ever raised: the store
291
- // refuses a blob that is not the greater, so a cursor is not lowered by a page that is stale or sent
292
- // again, nor by another tab that wrote at the same time.
321
+ // How far a container has been read, as one blob per moved key at its own id - see
322
+ // Ids.getSyncCursorBlobId. The cursor is the eTag, so there are no bytes. A cursor only rises: the
323
+ // store refuses a blob that is not greater, so a stale or repeated page, or another tab writing at
324
+ // the same time, cannot lower it.
293
325
  export function cursorsToBlobs(cursors, movedKeys) {
294
326
  const blobs = [];
295
327
  for (const key of movedKeys)
@@ -300,10 +332,10 @@ export function cursorsToBlobs(cursors, movedKeys) {
300
332
  });
301
333
  return blobs;
302
334
  }
303
- // The cursors a container holds, from the blobs it holds at the cursor ids of keys, in the order of
304
- // keys and null where it holds none: a state at zero for one that was never read, or was filled by a
305
- // version that did not keep them, which asks for everything again and is sent it again, a cost in
306
- // bandwidth and no more: the store keeps a blob only when its eTag is greater.
335
+ // The cursors a container holds, from the blobs at the cursor ids of keys (in key order, null where
336
+ // none). A container never read, or filled by a version that kept no cursors, gets zeros and asks for
337
+ // everything again. That costs bandwidth only, since the store keeps a blob only when its eTag is
338
+ // greater.
307
339
  export function cursorsFromBlobs(keys, blobs) {
308
340
  const cursors = new SyncState(keys);
309
341
  for (const [index, key] of keys.entries()) {
@@ -313,35 +345,31 @@ export function cursorsFromBlobs(keys, blobs) {
313
345
  }
314
346
  return cursors;
315
347
  }
316
- // Raises these cursors to what a page of a business read brought, tombstones included, and answers
317
- // the keys that moved. The only thing that moves them: what this client pushed does not.
348
+ // Raises the cursors to what a page of a business read brought, tombstones included, and answers the
349
+ // keys that moved. Only a read moves them. What this client pushed does not.
318
350
  export function raiseBusinessCursors(cursors, response) {
319
351
  return raiseCursorsFrom(cursors, getBusinessResponseEntities(response));
320
352
  }
321
- // The request that pushes back what this customer has changed. Messages are the only entity type a
322
- // storefront may push, and one request is one batch: an order and the cart it emptied go together,
323
- // so that a customer can never be left holding an order and a cart still full of the same items.
324
- // messageUpdateNumber is how far the reads of this customer's container have got with messages, and
325
- // not what a push has since stored: the response is this push's own messages, under the update
326
- // numbers the server assigns them, and nothing else.
353
+ // The request that pushes what this customer changed. A storefront may push only messages, and one
354
+ // request is one batch, so an order and the cart it emptied go together and a customer is never left
355
+ // holding the order and a still full cart. messageUpdateNumber is how far reads of this container
356
+ // have got with messages - see SyncCache.readMessageUpdateNumber.
327
357
  export function buildBusinessSyncInRequest(messageUpdateNumber, messages) {
328
358
  return proto_BusinessSyncInRequest.create({
329
359
  messageUpdateNumber: messageUpdateNumber,
330
360
  messages: [...messages],
331
361
  });
332
362
  }
333
- // The messages a sync in response carried - this push's own, now bearing the update numbers the
334
- // server assigned them, and no others. Nothing is to be made of those numbers: they are not how far a read has got.
363
+ // The messages a sync in response carried: this push's own, with the update numbers the server
364
+ // assigned. Those numbers are not how far a read has got.
335
365
  export function getSyncInResponseMessages(response) {
336
366
  return response.messages;
337
367
  }
338
- // The same, for the profile of the caller - names no business, since a profile sync is not scoped
339
- // to one and has no sync in.
340
- // joinBusinessId is the exception, and it is not a scope but an instruction: make the caller a
341
- // customer of that business, by entity id, while answering. Constants.ENTITY_ID_NULL asks for no
342
- // join, which is what every ordinary read carries, and it is the default for that reason. A join
343
- // belongs to one call and not to a read, so nextProfileSyncOutRequest leaves it off the pages that
344
- // follow - see there.
368
+ // The request for everything changed in the caller's profile since these cursors. It names no
369
+ // business, since a profile sync is not scoped to one and has no sync in. joinBusinessId tells the
370
+ // server to make the caller a customer of that business, by entity id, while answering.
371
+ // Constants.ENTITY_ID_NULL asks for no join and is the default, since an ordinary read joins
372
+ // nothing. A join belongs to one call, so nextProfileSyncOutRequest leaves it off later pages.
345
373
  export function buildProfileSyncOutRequest(syncInState, joinBusinessId = BigInt(proto_Constants.ENTITY_ID_NULL)) {
346
374
  return proto_ProfileSyncOutRequest.create({
347
375
  userUpdateNumber: syncInState.getUpdateNumber(proto_Constants.USER_ENTITY_TYPE_ID),
@@ -354,9 +382,8 @@ export function buildProfileSyncOutRequest(syncInState, joinBusinessId = BigInt(
354
382
  export function raiseProfileCursors(cursors, response) {
355
383
  return raiseCursorsFrom(cursors, getProfileResponseEntities(response));
356
384
  }
357
- // The user a sign up asks the server to create, as CreateUserRequest.user. Built from the domain
358
- // object rather than carrying one of its own: this user has no entity yet - the server assigns id,
359
- // createdAt and updatedAt on creation - so none of EntityHeader goes out here.
385
+ // The user a sign up asks the server to create, as CreateUserRequest.user. It has no entity yet (the
386
+ // server assigns id, createdAt and updatedAt), so none of EntityHeader goes out.
360
387
  export function buildUser(userProfile) {
361
388
  return proto_User.create({
362
389
  firstName: userProfile.firstName,
@@ -366,22 +393,21 @@ export function buildUser(userProfile) {
366
393
  preferredLanguageCode: stringToProto(userProfile.preferredLanguageCode),
367
394
  });
368
395
  }
369
- // The user Service.updateUser asks the server to overwrite this customer's profile with - the same
370
- // fields as buildUser, plus the one field that call matches the caller by: its own entity id.
371
- // Nothing else of EntityHeader goes out, because nothing else of it is read on that call.
396
+ // The user Service.updateUser overwrites this customer's profile with: the fields of buildUser plus
397
+ // the entity id, which the call matches the caller by. The rest of EntityHeader is not read on that
398
+ // call, so it is not sent.
372
399
  export function buildUpdateUser(userProfile) {
373
400
  return proto_User.create({
374
401
  ...buildUser(userProfile),
375
402
  entity: proto_Entity.create({ id: userProfile.id }),
376
403
  });
377
404
  }
378
- // The two halves of what a join must be answered with, each asked separately so that a caller
379
- // reporting a server that answered without one can say which was missing - see
380
- // Model.requireJoinedBusiness. Both take the id rather than the request, because both of the
381
- // requests that carry a joinBusinessId need them.
382
- // The membership itself: a live BusinessLink naming that business. A tombstoned one is not a
383
- // membership - see OBJECT MODEL - so it does not count, and neither does a link to any other
384
- // business. Its linkToken is the only copy of that credential a profile response carries.
405
+ // The two things a join must be answered with, checked separately so a caller can report which one a
406
+ // server left out - see Model.requireJoinedBusiness. They take the id, since both requests that
407
+ // carry a joinBusinessId need them.
408
+ // The membership itself: a live BusinessLink naming that business. A tombstoned link does not count
409
+ // (see OBJECT MODEL), nor does a link to another business. Its linkToken is the only copy of that
410
+ // credential a profile response carries.
385
411
  export function hasJoinedBusinessLink(joinBusinessId, response) {
386
412
  if (joinBusinessId === BigInt(proto_Constants.ENTITY_ID_NULL))
387
413
  return false;
@@ -391,59 +417,57 @@ export function hasJoinedBusinessLink(joinBusinessId, response) {
391
417
  return true;
392
418
  return false;
393
419
  }
394
- // The business that link names, matched on Business.entity.id - which is what
395
- // BusinessLink.businessId is, and not Business.businessId, the url friendly string. A caller needs
396
- // it as well as the link, because the string is what every later request and container name for
397
- // that business is built out of, and the link carries only the id.
420
+ // The business the link names, matched on Business.entity.id, which BusinessLink.businessId holds
421
+ // (Business.businessId is the url friendly string). A caller needs the business as well as the link,
422
+ // because every later request and container name is built from that string and the link has only
423
+ // the id.
398
424
  export function hasJoinedBusiness(joinBusinessId, response) {
399
425
  if (joinBusinessId === BigInt(proto_Constants.ENTITY_ID_NULL))
400
426
  return false;
401
- // A tombstone of the business is not the business, which is the whole of what a join is answered with it for
427
+ // A tombstone is not the business, which is what the join is answered with
402
428
  for (const business of response.businesses)
403
429
  if (getEntity(business.entity).id === joinBusinessId &&
404
430
  !isDeletedEntity(getEntity(business.entity)))
405
431
  return true;
406
432
  return false;
407
433
  }
408
- // The country of the business a response holds, ISO 3166-1 numeric, or undefined when it holds none. A
409
- // tombstone is not a business and has no country: it is only its header, so the zero it carries must
410
- // not be taken for one - a cart's taxes are raw codes, which mean something only within a country.
434
+ // The country of the business a response holds, ISO 3166-1 numeric, or undefined when it holds none.
435
+ // A tombstone has no country, and a bare header carries zero, which must not be taken for one: a
436
+ // cart's taxes are raw codes that mean something only within a country.
411
437
  export function getBusinessResponseCountryCode(response) {
412
438
  const business = response?.business;
413
439
  if (business === undefined || isDeletedEntity(getEntity(business.entity)))
414
440
  return undefined;
415
441
  return business.countryCode;
416
442
  }
417
- // The entity id of the business a response holds, or null when it holds none - which is a response
418
- // with no business in it, or one with a tombstone, a deleted business being no business to name. Once
419
- // a business has been read, this is what every later request names it by: its entity id is stable
420
- // and a url friendly id is only how it was first found.
443
+ // The entity id of the business a response holds, or null when it holds none or only a tombstone.
444
+ // Once a business has been read, later requests name it by this id: it is stable, and the url
445
+ // friendly id is only how it was first found.
421
446
  export function getBusinessResponseBusinessEntityId(response) {
422
447
  const entity = response?.business?.entity;
423
448
  if (entity === undefined || isDeletedEntity(entity))
424
449
  return null;
425
450
  return entity.id;
426
451
  }
427
- // The url friendly id of the business a response holds - the one that a business is found by, which is not its entity id
428
- // - or null when it holds none, or only a tombstone, which is only a header and has no business id to tell.
452
+ // The url friendly id of the business a response holds (the one a business is found by, not its
453
+ // entity id), or null when it holds none or only a tombstone.
429
454
  export function getBusinessResponseBusinessId(response) {
430
455
  const business = response?.business;
431
456
  if (business === undefined || isDeletedEntity(getEntity(business.entity)))
432
457
  return null;
433
458
  return business.businessId;
434
459
  }
435
- // Whether the general settings that a response holds say that the business has turned online shopping on. Settings that
436
- // are not there - the business has made none, or has deleted them, or this has not read them yet - say that it has not, as
437
- // settings that say so do. That is what a storefront is told by a business that has not turned it on: the server sends it
438
- // whatever it says, and a client decides, from what it holds once it has read everything it was sent.
460
+ // Whether the general settings a response holds turn online shopping on. Missing settings (never
461
+ // made, deleted, or not read yet) count as off. The server sends a business's data whether or not
462
+ // shopping is on, so the client decides from what it holds once it has read everything.
439
463
  export function isBusinessResponseOnlineShoppingOn(response) {
440
464
  if (response === null)
441
465
  return false;
442
466
  return getGeneralSettingsOf(response)?.onlineShoppingEnabled ?? false;
443
467
  }
444
- // The general settings that a response holds, as the wire carries them, or undefined when it holds none: the one public
445
- // configuration that carries them, unless it has been deleted, which is only its header. What is read of the settings - the
446
- // business that is built from them, and whether it is open - is read from here, so that the two cannot be about different ones.
468
+ // The general settings a response holds, as on the wire, or undefined when none: the one public
469
+ // configuration that carries them, unless it is deleted. The business built from them and whether
470
+ // shopping is on both read from here, so they cannot be about different settings.
447
471
  function getGeneralSettingsOf(response) {
448
472
  let settings;
449
473
  for (const configuration of response.configurations) {
@@ -455,8 +479,8 @@ function getGeneralSettingsOf(response) {
455
479
  }
456
480
  return settings;
457
481
  }
458
- // The credentials of one call, as the wire carries them. An absent token is left off the message
459
- // rather than sent empty, because the protocol tells the two apart.
482
+ // The credentials of one call, as on the wire. An absent token is left off the message, since the
483
+ // protocol tells absent from empty.
460
484
  export function buildAuthTokens(authTokens) {
461
485
  const result = proto_AuthTokens.create({
462
486
  authenticationMethod: authTokens.authenticationMethod,
@@ -470,21 +494,19 @@ export function buildAuthTokens(authTokens) {
470
494
  result.linkToken = linkToken;
471
495
  return result;
472
496
  }
473
- // What a server issued while answering a call, in the terms this library holds credentials in
497
+ // What a server issued while answering a call, as this library's AuthTokens
474
498
  export function authTokensFromProto(authTokens) {
475
499
  return new AuthTokens(authTokens.authenticationMethod, authTokens.authenticationToken, stringFromProto(authTokens.userToken), stringFromProto(authTokens.linkToken));
476
500
  }
477
- // The authorization header of a request, encoded. Lives here because this is the only file that
478
- // writes a proto field; a transport puts these bytes in the header however it wants.
501
+ // The encoded authorization header of a request. It lives here because this file writes the proto
502
+ // fields. A transport puts the bytes in the header.
479
503
  export function buildAuthTokensBytes(authTokens) {
480
504
  return proto_AuthTokens.toBinary(authTokens);
481
505
  }
482
- // How a container keeps the credentials it earned: the two tokens this server issued, with a
483
- // null eTag because a reissued token is not a later version of an earlier one and greater-wins
484
- // would refuse it.
485
- // The authentication token is left out however it was passed in: it belongs to the identity
486
- // provider, so a stored copy could only be staler than the one held in memory and would outlive
487
- // the sign out that should have ended it.
506
+ // How a container keeps the credentials it earned: the two tokens this server issued. The eTag is
507
+ // null because a reissued token is not a later version of the old one, and greater-wins would refuse
508
+ // it. The authentication token is never stored: it belongs to the identity provider, so a copy would
509
+ // be staler than the one in memory and would outlive the sign out.
488
510
  export function authTokensToBlob(authTokens) {
489
511
  const stored = proto_AuthTokens.create({
490
512
  authenticationMethod: authTokens.authenticationMethod,
@@ -506,8 +528,7 @@ export function authTokensFromBytes(bytes) {
506
528
  }
507
529
  // #endregion
508
530
  // #region Paging
509
- // What the server issued while answering, which every response of this protocol carries in the same
510
- // field - the two sync outs and a sync in alike.
531
+ // What the server issued while answering. All three response types carry it in the same field.
511
532
  export function getResponseAuthTokens(response) {
512
533
  return response.authTokens;
513
534
  }
@@ -521,13 +542,13 @@ export function refreshAuthTokens(authTokens, issued) {
521
542
  result.linkToken = issued.linkToken;
522
543
  return result;
523
544
  }
524
- // The request that asks for the page after this response, or null when this response was the last.
525
- // A response carrying a full page has been cut short, so the cursors are raised from what arrived
526
- // and the call is repeated. A full page that raised no cursor at all can only come from a server
527
- // breaking the ascending order it promises; the loop is unbounded, so it stops rather than trust it.
545
+ // The request for the page after this response, or null when it was the last. A full page was cut
546
+ // short, so the cursors are raised from what arrived and the call repeats. A full page that raised no
547
+ // cursor means the server broke the ascending order it promises. The loop is unbounded, so it stops
548
+ // there.
528
549
  export function nextBusinessSyncOutRequest(request, response) {
529
- // Anything short of a full page is the server saying it has sent everything it had. Counted
530
- // before anything is looked at, so the ordinary answer costs no more than reading nine lengths.
550
+ // A short page means the server sent everything. Counting first keeps the ordinary case to nine
551
+ // length reads.
531
552
  if (businessResponseEntityCount(response) < proto_Constants.SYNC_OUT_LIMIT)
532
553
  return null;
533
554
  const cursors = businessSyncOutCursors(request);
@@ -560,8 +581,8 @@ export function nextBusinessSyncOutRequest(request, response) {
560
581
  const renewedExpiry = getBusinessResponseAttachmentBaseUrlExpiry(response);
561
582
  return createBusinessSyncOutRequest(request.business, cursors, renewedExpiry === 0n ? request.attachmentBaseUrlExpiry : renewedExpiry);
562
583
  }
563
- // The same for a profile read. Any joinBusinessId the first request carried is deliberately left
564
- // off the ones that follow: a join is done once, by the call that asked for it.
584
+ // The same for a profile read. A joinBusinessId on the first request is left off the later ones,
585
+ // since the call that asked for the join does it once.
565
586
  export function nextProfileSyncOutRequest(request, response) {
566
587
  if (profileResponseEntityCount(response) < proto_Constants.SYNC_OUT_LIMIT)
567
588
  return null;
@@ -575,15 +596,13 @@ export function nextProfileSyncOutRequest(request, response) {
575
596
  for (const business of response.businesses)
576
597
  if (raiseCursor(cursors, business.entity))
577
598
  haveCursorsRisen = true;
578
- // A full page that raised nothing can only come from a server breaking the ascending order it
579
- // promises; the loop is unbounded, so it stops rather than trust it
599
+ // A full page that raised no cursor ends the loop, as in nextBusinessSyncOutRequest
580
600
  if (!haveCursorsRisen)
581
601
  return null;
582
602
  return buildProfileSyncOutRequest(cursors);
583
603
  }
584
- // How many entities a response carried, without decoding or encoding any of them - a paging loop
585
- // asks this of every page, and getBusinessResponseEntities answers the same question only by encoding each
586
- // entity for the cache on the way past.
604
+ // How many entities a response carried, without encoding any. A paging loop asks this of every page,
605
+ // and getBusinessResponseEntities would encode each entity to answer it.
587
606
  function businessResponseEntityCount(response) {
588
607
  return ((response.business === undefined ? 0 : 1) +
589
608
  response.attachments.length +
@@ -600,9 +619,8 @@ function profileResponseEntityCount(response) {
600
619
  response.businessLinks.length +
601
620
  response.businesses.length);
602
621
  }
603
- // Raises the cursors the entities of one page advance, and answers the keys that moved - one for
604
- // all the entities of a table, however many of them raised it. A tombstone counts exactly as a value
605
- // does, as it does for raiseCursor.
622
+ // Raises the cursors the entities of one page advance and answers the keys that moved, one per table
623
+ // however many entities raised it. A tombstone counts like any entity, as in raiseCursor.
606
624
  function raiseCursorsFrom(cursors, entities) {
607
625
  const movedKeys = new Set();
608
626
  for (const entity of entities)
@@ -610,15 +628,15 @@ function raiseCursorsFrom(cursors, entities) {
610
628
  movedKeys.add(SyncKeys.keyOf(entity.entityTypeId));
611
629
  return movedKeys;
612
630
  }
613
- // Raises the cursor one entity advances, and answers whether it moved. A tombstone counts exactly
614
- // as a value does - skipping one would have the server send it again for ever.
631
+ // Raises the cursor one entity advances and answers whether it moved. A tombstone counts, or the
632
+ // server would send it again for ever.
615
633
  function raiseCursor(cursors, entity) {
616
634
  if (entity === undefined)
617
635
  return false;
618
636
  return cursors.raiseUpdateNumber(Ids.getEntityTypeId(entity.id), entity.updateNumber);
619
637
  }
620
- // The cursors a request is carrying, as a state that can be raised - the inverse of the two builders
621
- // above, and how a paging loop carries what it has already been sent into the call after it.
638
+ // The cursors a request carries, as a state that can be raised: the inverse of the request builders.
639
+ // A paging loop uses it to carry them into the next call.
622
640
  function businessSyncOutCursors(request) {
623
641
  const cursors = new SyncState(SyncKeys.BUSINESS_SYNC_IN);
624
642
  cursors.raiseUpdateNumber(proto_Constants.BUSINESS_ENTITY_TYPE_ID, request.businessUpdateNumber);
@@ -637,21 +655,20 @@ function profileSyncOutCursors(request) {
637
655
  }
638
656
  // #endregion
639
657
  // #region A response as an object graph
640
- // The whole of one business as an object graph, regrouping the flat wire entities by owner id so
641
- // each class can be constructed with its children.
642
- // An entity whose parent is missing is dropped, since a paged read can hold a child whose parent
643
- // has yet to arrive; null when the response carries no Business at all.
658
+ // One business as an object graph, regrouping the flat wire entities by owner id so each class is
659
+ // built with its children. An entity whose parent is missing is dropped, since a paged read can hold
660
+ // a child before its parent arrives. Null when the response has no live Business.
644
661
  export function buildBusiness(response) {
645
662
  const businessProto = response.business;
646
663
  if (businessProto === undefined || isDeletedEntity(getEntity(businessProto.entity)))
647
664
  return null;
648
665
  const business = businessFromProto(businessProto);
649
666
  const updatableBusiness = business;
650
- // A tax travels as its raw code, which means something only within the business's country - so
651
- // every tax below is read against this one
667
+ // A tax travels as a raw code that means something only within the business's country, so every tax
668
+ // below is read against this one
652
669
  const countryCode = businessProto.countryCode;
653
- // Resolved once here and handed to every Photo built below, since a photo takes the base url in
654
- // effect when it is built rather than looking one up later - see the comment on Photo's constructor.
670
+ // Resolved once and handed to every Photo built below, since a Photo takes the base url when it is
671
+ // built - see Photo's constructor.
655
672
  const attachmentBaseUrlProto = response.attachmentBaseUrl;
656
673
  const attachmentBaseUrl = attachmentBaseUrlProto === undefined ? null : attachmentBaseUrlFromProto(attachmentBaseUrlProto);
657
674
  updatableBusiness.attachmentBaseUrl = attachmentBaseUrl;
@@ -673,10 +690,10 @@ export function buildBusiness(response) {
673
690
  updatableBusiness.settings = generalSettingsFromProto(generalSettings, countryCode);
674
691
  // The photos of the store itself
675
692
  updatableBusiness.photos = takePhotos(attachmentsByOwner, business.id, attachmentBaseUrl);
676
- // Prices, by the package each applies to. Only the MeraBills Online list is this storefront's -
677
- // a business prices the same package in several lists, and a price from any other one is not
678
- // what it sells at here, so it is passed over rather than allowed to win a package. Within that
679
- // one list a package has one price; if a second arrives anyway, the first is kept.
693
+ // Prices, by the package each applies to. Only the MeraBills Online list is this storefront's. A
694
+ // business prices a package in several lists, and a price from another list is not what it sells at
695
+ // here, so it is skipped. A package has one price in that list; if a second arrives, the first is
696
+ // kept.
680
697
  const pricesByPackage = new Map();
681
698
  for (const priceProto of response.prices) {
682
699
  if (isDeletedItem(priceProto.item))
@@ -687,10 +704,9 @@ export function buildBusiness(response) {
687
704
  if (!pricesByPackage.has(price.packageId))
688
705
  pricesByPackage.set(price.packageId, price);
689
706
  }
690
- // Packages, by the product each belongs to, built with their photos and their price. A package
691
- // that is not buyable - disabled, unpriced, or priced with a price that is itself disabled - is
692
- // left out here rather than handed to the UI unbuyable: a product-level filter does not, by
693
- // itself, guarantee every package of an otherwise visible product is itself enabled.
707
+ // Packages, by the product each belongs to, built with their photos and price. A package that is not
708
+ // buyable (disabled, unpriced, or priced with a disabled price) is left out. A product-level filter
709
+ // alone would not guarantee that every package of a visible product is enabled.
694
710
  const packagesByProduct = new Map();
695
711
  for (const packageProto of response.packages) {
696
712
  if (isDeletedItem(packageProto.item))
@@ -709,10 +725,9 @@ export function buildBusiness(response) {
709
725
  else
710
726
  siblings.push(pkg);
711
727
  }
712
- // Products, built with their photos and their buyable packages, and parented to the business
713
- // (not their category, which only groups them). A product that is not buyable - disabled, sold
714
- // only offline (the server sends those, flagged), or left with no buyable package by the filtering
715
- // above - is left out for the same reason.
728
+ // Products, built with their photos and buyable packages, and parented to the business (their
729
+ // category only groups them). A product that is not buyable is left out: disabled, sold only offline
730
+ // (the server sends those, flagged), or left with no buyable package by the filtering above.
716
731
  const products = [];
717
732
  for (const productProto of response.products) {
718
733
  if (isDeletedItem(productProto.item))
@@ -736,9 +751,9 @@ export function buildBusiness(response) {
736
751
  UpdatableEntity.setParent(productCategory, business);
737
752
  productCategories.push(productCategory);
738
753
  }
739
- // The one list a storefront sells from is MeraBills Online, named rather than taken to be
740
- // whichever list arrived first: the same sync can carry others, and the catalog has to answer
741
- // with the list the prices above came from or a cart would name one they did not.
754
+ // The one list a storefront sells from is MeraBills Online, picked by id since the same sync can
755
+ // carry others. The catalog must answer with the list the prices above came from, or a cart would
756
+ // name one they are not in.
742
757
  let priceList = null;
743
758
  for (const priceListProto of response.priceLists) {
744
759
  if (isDeletedItem(priceListProto.item))
@@ -750,30 +765,30 @@ export function buildBusiness(response) {
750
765
  break;
751
766
  }
752
767
  updatableBusiness.catalog = new Catalog(productCategories, products, priceList);
753
- // The orders of this customer, and their party preferences: the cart, and the fulfillment mode
754
- // to default their next order to. A message of any other payload type is dropped.
768
+ // The orders of this customer, and their party preferences (the cart and the fulfillment mode to
769
+ // default the next order to). A message of any other payload type is dropped. The business completes
770
+ // an order by deleting its message, and the server sends that message whole, so the orders are the
771
+ // open and the completed ones, told apart by Order.isDeleted. A deleted message with no order (from
772
+ // an older server that sent only the header) is not an order.
755
773
  const orders = [];
756
774
  for (const message of response.messages) {
757
- if (isDeletedEntity(getEntity(message.entity)))
758
- continue;
759
- // The party preferences are one message out of however many there are, so they are settled below
775
+ // Party preferences are one message out of possibly several, so they are settled below
760
776
  if (message.payload.oneofKind === "salesOrder")
761
777
  orders.push(orderFromProto(message, message.payload.salesOrder, countryCode));
762
778
  }
763
- // One customer has one party preferences message, so two means two callers' messages are in one container. The one that is
764
- // taken is the one that partyPreferencesMessageFromMessages picks, which is what a cart that is amended or sent again is
765
- // read from as well: a rule of its own here would show one cart and amend another. It is the newer of the two even when it
766
- // is the emptied one, and the older one's list is not shown for want of it.
779
+ // A customer has one party preferences message, so two means two callers' messages share a
780
+ // container. Take the one partyPreferencesMessageFromMessages picks, which is also what an amended or
781
+ // resent cart is read from. A rule of its own here would show one cart and amend another. The newer
782
+ // one wins even when it is the emptied one, and the older one's list is not shown.
767
783
  const partyPreferences = partyPreferencesMessageFromMessages(response.messages);
768
784
  let cart = null;
769
785
  let defaultFulfillmentMode = proto_FulfillmentMode.DELIVERY;
770
786
  if (partyPreferences !== null && partyPreferences.payload.oneofKind === "partyPreferences") {
771
- // An emptied cart is a message whose payload carries no shopping list, which is what
772
- // placing an order leaves behind - so it answers null and not an empty list, the same as
773
- // never having had a cart at all
787
+ // An emptied cart is a message whose payload has no shopping list, which is what placing an order
788
+ // leaves. It answers null, the same as never having had a cart.
774
789
  cart = cartShoppingListFromProto(partyPreferences.payload.partyPreferences, countryCode);
775
- // Whatever this same message carries, DELIVERY only ever coming from the fallback above -
776
- // not from a message that happens to hold UNSPECIFIED_FULFILLMENT_MODE itself
790
+ // Taken as the message has it. DELIVERY comes only from the fallback above, even when the message
791
+ // holds UNSPECIFIED_FULFILLMENT_MODE.
777
792
  defaultFulfillmentMode = partyPreferences.payload.partyPreferences.fulfillmentMode;
778
793
  }
779
794
  for (const order of orders)
@@ -813,11 +828,9 @@ export function buildOrder(message, countryCode) {
813
828
  return null;
814
829
  return orderFromProto(message, message.payload.salesOrder, countryCode);
815
830
  }
816
- // The shopping list a cart message carries, empty rather than absent when the cart has been
817
- // emptied. Unlike Business.cart (see the party preferences case of buildBusiness, which answers
818
- // null so "emptied" and "never had one" look the same to a customer), a caller here - createOrder,
819
- // the visitor cart carried over on sign in - works with an actual list either way.
820
- // countryCode is that of the business the cart is for, as for buildOrder.
831
+ // The shopping list a cart message carries, empty when the cart was emptied. Business.cart answers
832
+ // null for that (see buildBusiness), but the callers here, createOrder and the visitor cart carried
833
+ // over on sign in, need an actual list. countryCode is the cart's business, as for buildOrder.
821
834
  export function buildShoppingList(message, countryCode) {
822
835
  if (message.payload.oneofKind !== "partyPreferences")
823
836
  throw new InvalidArgumentException("That message carries no party preferences, so it is not a cart");
@@ -826,37 +839,29 @@ export function buildShoppingList(message, countryCode) {
826
839
  // #endregion
827
840
  // #region What a customer pushes
828
841
  //
829
- // The only direction of this file that encodes rather than decodes, and the only entity type a
830
- // storefront may write: a Message, carrying either the cart or an order placed from it. Two rules
831
- // hold for both, and they are what make a pushed message storable here as well as on the server.
832
- // The first is that the id is the client's: a sync in answers with an update number and no id, so a
833
- // message the server named could never be found here again. The second is that updatedAt is the
834
- // client's too and the server keeps it unchanged - so the copy stored here carries the same eTag as
835
- // the copy a later read would bring back, and neither can look stale to the other.
836
- // The party preferences of this customer as a message to push, carrying the cart and (optionally)
837
- // the fulfillment mode both. An existing message is amended rather than rebuilt, because its
838
- // payload can carry fields neither param here speaks to, and losing them would be a storefront
839
- // overwriting data it cannot even read.
840
- // Its id is used in preference to the derived one for the same reason: that is where the server
841
- // already holds this customer's party preferences, and it is only worth deriving an id when there
842
- // is none to amend.
843
- // A list holding nothing is pushed as no list at all, the way Java serializeShoppingList answers
844
- // null below one item - so emptying a cart and never having had one are one state on the wire, and
845
- // placing an order is what puts a cart into it.
846
- // fulfillmentMode is null to leave whatever is already there alone - the ordinary case, since a
847
- // storefront otherwise treats it as the merchant app's own data - and a value only where createOrder
848
- // means to set it, to the mode of the order that just emptied this cart.
842
+ // A storefront writes one entity type, a Message carrying the cart or an order placed from it. Two
843
+ // rules hold for both, so a pushed message is storable here as well as on the server. The id is the
844
+ // client's: a sync in answers with an update number and no id, so a message the server named could
845
+ // not be found here again. updatedAt is the client's too and the server keeps it, so the copy stored
846
+ // here has the same eTag as the one a later read brings back.
847
+ // This customer's party preferences as a message to push, carrying the cart and optionally the
848
+ // fulfillment mode. An existing message is amended, since its payload can carry fields a storefront
849
+ // cannot read, and dropping them would overwrite that data. Its id is kept, as the server holds the
850
+ // preferences under it. The derived id is used only when there is nothing to amend.
851
+ // A list with no items is pushed as no list, as Java serializeShoppingList answers null below one
852
+ // item. Emptying a cart and never having had one are one state on the wire.
853
+ // fulfillmentMode null leaves the existing value alone, the ordinary case: a storefront treats it as
854
+ // the merchant app's data. createOrder sets it to the mode of the order that just emptied the cart.
849
855
  export function buildPartyPreferencesMessage(messageId, existing, senderId, shoppingList, fulfillmentMode, now) {
850
856
  const existingEntity = existing === null ? null : getEntity(existing.entity);
851
857
  const preferences = existing !== null && existing.payload.oneofKind === "partyPreferences"
852
858
  ? existing.payload.partyPreferences
853
859
  : proto_PartyPreferencesPayload.create();
854
- // Later than the copy it replaces, whatever this client's clock reads. An amendment written here
855
- // without the server is stored at the update number of the copy it amends, where this field is
856
- // what decides - so a client whose clock sits behind the time on the cart it is amending, which
857
- // Constants.MAX_CLOCK_SKEW says is a thing that happens, would have its own store refuse the
858
- // edit, leaving the customer looking at a cart that is not the one it holds. One millisecond past
859
- // the copy being replaced is enough, and the server keeps whatever is sent, so both sides still agree.
860
+ // Later than the copy it replaces, whatever this client's clock reads. An amendment written without
861
+ // the server is stored at the update number of the copy it amends, so updatedAt decides. A client
862
+ // clock behind that copy (Constants.MAX_CLOCK_SKEW says it happens) would have its own store refuse
863
+ // the edit and leave the customer looking at a stale cart. One millisecond past the copy is enough,
864
+ // and the server keeps what is sent, so both sides agree.
860
865
  const updatedAt = existingEntity === null || existingEntity.updatedAt < now ? now : existingEntity.updatedAt + 1n;
861
866
  const partyPreferences = proto_PartyPreferencesPayload.create({
862
867
  ...preferences,
@@ -872,13 +877,12 @@ export function buildPartyPreferencesMessage(messageId, existing, senderId, shop
872
877
  payload: { oneofKind: "partyPreferences", partyPreferences: partyPreferences },
873
878
  });
874
879
  }
875
- // One order as a message to push. Its shopping list goes out final whatever was passed in, which is
876
- // what Java SalesOrderViewModel.saveChanges does before creating one: an order is what a customer
877
- // has finished choosing, and only a final list tracks a later change as a change rather than
878
- // silently altering what was ordered.
879
- // The two totals are filled in here from the list, and are what a fulfilled order is left with once
880
- // the business has cleared the list itself - so they are written off the items actually being sent
881
- // rather than off the counts the list carries, which a caller could have set to anything.
880
+ // One order as a message to push. Its shopping list goes out final whatever the list says, as Java
881
+ // SalesOrderViewModel.saveChanges does before creating an order: the customer has finished choosing,
882
+ // and only a final list tracks a later change as a change instead of silently altering what was
883
+ // ordered. The two totals are all that remains of an order completed by an older merchant app, which
884
+ // moved the list out of the order. They are filled from the list being sent, not from the counts it
885
+ // carries, which a caller could set to anything.
882
886
  export function buildOrderMessage(orderId, senderId, shoppingList, fulfillmentMode, deliveryAddressId, deliveryAddress, note, creationDate) {
883
887
  return proto_Message.create({
884
888
  entity: pushedEntity(orderId, creationDate, creationDate),
@@ -901,36 +905,31 @@ export function buildOrderMessage(orderId, senderId, shoppingList, fulfillmentMo
901
905
  },
902
906
  });
903
907
  }
904
- // A sync in response's messages as the cache write they make, tombstones included - the same as any
905
- // other entity array this library reads off the wire. This is how a push's own messages end up
906
- // stored under the update numbers the server just assigned them, with no separate stamping step:
907
- // the response already carries them as full entities, not as a bare number to reapply by hand.
908
+ // The messages of a sync in response as a cache write, tombstones included like any other entity
909
+ // array. This stores a push's own messages under the update numbers the server just assigned, with no
910
+ // separate stamping step, since the response carries full entities.
908
911
  export function messagesToCacheWrite(messages) {
909
912
  return toCacheWrite(messages.map((message) => toWireEntity(message.entity, proto_Message.toBinary(message))));
910
913
  }
911
- // The Entity of something this client is pushing. The update number goes out as zero: the protocol
912
- // replaces a caller's own with the server's and answers what it assigned, so there is nothing to
913
- // send and nothing to guess. It is also what a message not pushed yet is left holding in the store,
914
- // where messageToBlob versions it from the copy it amends. updateInfo goes out as zero too: it
915
- // would be the id of the store that made the change, and this client has no store of its own that
916
- // the server knows. It needs none: a read of this business sends these messages back, as the
917
- // server leaves nobody's changes out of a storefront's read, and the eTag of the copy stored here
918
- // is the one the server kept, so, that changes nothing it holds.
914
+ // The Entity of something this client is pushing. The update number goes out as zero: the server
915
+ // replaces a caller's own with the one it assigns, so there is nothing to send. A message not pushed
916
+ // yet is stored holding zero, and messageToBlob versions it from the copy it amends. updateInfo is
917
+ // zero too, as it would be the id of the store that made the change and the server knows no store of
918
+ // this client. It needs none: a read sends these messages back (the server leaves no one's changes
919
+ // out of a storefront's read), and the eTag stored here is the one the server kept, so the read
920
+ // changes nothing.
919
921
  function pushedEntity(id, createdAt, updatedAt) {
920
922
  return proto_Entity.create({ id: id, createdAt: createdAt, updatedAt: updatedAt });
921
923
  }
922
- // Whether shoppingList differs from what stored already holds, so updateCart can check before
923
- // building or pushing a cart message rather than after - a caller may call it on every event it
924
- // cares about and only a cart the server has not got costs a request.
925
- // Compares raw lists rather than built messages, because a message's updatedAt always differs from
926
- // call to call; sound because the server echoes a pushed list back with the items it was sent, so what
927
- // is stored is what was last sent - but not in the order it was sent in: the server keeps the items of a
928
- // list by package id and sends them in that order, and the order of the list is each item's
929
- // shoppingListOrder, which is compared with the rest of the item. So the items of both lists are put
930
- // in package id order before they are compared, or a cart that was not picked in package id order would
931
- // differ from its own echo and be pushed again, which re-stamps its updatedAt for every other device.
932
- // Also true whenever stored has never been pushed (updateNumber 0, left by updateCartLocalOnly),
933
- // regardless of content, so an unsent local edit still goes out.
924
+ // Whether shoppingList differs from what stored holds, so updateCart can skip a push. Compares raw
925
+ // lists, since a message's updatedAt differs on every call. That is sound because the server echoes
926
+ // a pushed list back with the items it was sent, so stored is what was last sent.
927
+ // The server echoes the items in package id order, and the list's own order is each item's
928
+ // shoppingListOrder, compared with the item. So both lists are put in package id order first, or a
929
+ // cart not picked in that order would differ from its echo and be pushed again, re-stamping
930
+ // updatedAt for every other device.
931
+ // True also when stored was never pushed (updateNumber 0, left by updateCartLocalOnly), so an
932
+ // unsent local edit still goes out.
934
933
  export function hasShoppingListChanged(stored, shoppingList) {
935
934
  if (stored === null)
936
935
  return pushedShoppingList(shoppingList, false) !== undefined;
@@ -949,15 +948,14 @@ function inPackageIdOrder(list) {
949
948
  deletedItems: [...list.deletedItems].sort(byPackageId),
950
949
  });
951
950
  }
952
- // The list inside a cart message, or absent for one carrying no list - which is an emptied cart
951
+ // The list inside a cart message, or absent for an emptied cart
953
952
  function getCartShoppingList(message) {
954
953
  return message.payload.oneofKind === "partyPreferences"
955
954
  ? message.payload.partyPreferences.shoppingList
956
955
  : undefined;
957
956
  }
958
- // A shopping list on its way out, or absent when it holds nothing - see
959
- // buildPartyPreferencesMessage. Emptiness is judged by the items being sent and not by the counts
960
- // the list carries, for the reason the counts below are.
957
+ // A shopping list on its way out, or absent when it holds nothing - see buildPartyPreferencesMessage.
958
+ // Emptiness is judged by the items sent, not the counts, for the reason given at shoppingListToProto.
961
959
  function pushedShoppingList(shoppingList, isFinal) {
962
960
  if (shoppingList === null)
963
961
  return undefined;
@@ -965,10 +963,9 @@ function pushedShoppingList(shoppingList, isFinal) {
965
963
  return undefined;
966
964
  return shoppingListToProto(shoppingList, isFinal);
967
965
  }
968
- // The counts are written off the arrays rather than copied from the list. A read takes them from the
969
- // wire because the protocol sends a count even where it does not send the items, but a push always
970
- // sends the items - so the arrays are what is true here, and taking the counts from them is what
971
- // makes it impossible to push a list whose count disagrees with its contents.
966
+ // The counts come from the arrays, not the list. A read takes them from the wire because the
967
+ // protocol sends a count even where it omits the items, but a push always sends the items. Taking the
968
+ // counts from the arrays means a pushed list cannot disagree with its contents.
972
969
  function shoppingListToProto(shoppingList, isFinal) {
973
970
  return proto_ShoppingList.create({
974
971
  isFinal: isFinal,
@@ -984,9 +981,8 @@ function shoppingListToProto(shoppingList, isFinal) {
984
981
  deletedItems: shoppingList.deletedItems.map(shoppingListItemToProto),
985
982
  });
986
983
  }
987
- // The mirror of shoppingListItemFromProto: everything the model holds unscaled goes back scaled, and
988
- // nothing derived is sent - amount, totalTax and finalPrice are computed from these fields, here and
989
- // by the server both.
984
+ // The mirror of shoppingListItemFromProto. Unscaled model values go back scaled, and nothing derived
985
+ // is sent: amount, totalTax and finalPrice are computed from these fields on both sides.
990
986
  function shoppingListItemToProto(item) {
991
987
  return proto_ShoppingListItem.create({
992
988
  packageId: item.packageId,
@@ -1008,12 +1004,12 @@ function taxToProto(tax) {
1008
1004
  }
1009
1005
  // #endregion
1010
1006
  // #region Entity and item bookkeeping
1011
- // One pass over the entities of a response, each becoming a blob at its own id, which is what makes
1012
- // one read of a container enough to sort it by entity type. Every blob is versioned by entityETag, a
1013
- // tombstone's too: a delete is a write of its own, so a tombstone carries a greater update number
1014
- // than any copy of what it deleted and replaces it, and an older copy of it, sent again, is the
1015
- // lesser of the two and is refused. A later write of the same id is the greater again, and is kept.
1016
- // Mutable arrays, so that businessResponseToCacheWrite can add the attachment base url to the blobs.
1007
+ // One pass over a response's entities, each becoming a blob at its own id, so one read of a container
1008
+ // is enough to sort it by entity type. Every blob is versioned by entityETag, tombstones too. A
1009
+ // delete is a write of its own, so a tombstone has a greater update number than any copy of what it
1010
+ // deleted and replaces it. An older copy sent again is lesser and refused, and a later write of the
1011
+ // same id is greater and kept. The arrays are mutable so that businessResponseToCacheWrite can add
1012
+ // the attachment base url.
1017
1013
  function toCacheWrite(entities) {
1018
1014
  const blobs = [];
1019
1015
  for (const entity of entities)
@@ -1024,23 +1020,23 @@ function toCacheWrite(entities) {
1024
1020
  });
1025
1021
  return { blobs: blobs, deletedIds: [] };
1026
1022
  }
1027
- // What a store versions the blob of an entity by: its update number, and below it its updatedAt, in
1028
- // one bigint so that the greater wins as the store says. The server assigns an update number to every
1029
- // write, deletes included, so it rises with every change to an entity whatever any clock reads, and
1030
- // decides between two copies that are not the same version. updatedAt decides between those that
1031
- // are: only a copy this client wrote itself, at the update number of the one it amended, is another
1032
- // of the same number. Wider than 64 bits, which a bigint holds exactly and a store must compare as one.
1023
+ // What a store versions an entity's blob by: the update number, with updatedAt below it, in one
1024
+ // bigint so the greater wins as the store says. The server assigns an update number to every write,
1025
+ // deletes included, so it rises with every change whatever any clock reads, and decides between
1026
+ // different versions. updatedAt decides between copies of one update number. Only a copy this client
1027
+ // wrote itself, at the update number of the one it amended, shares one. The value is wider than 64
1028
+ // bits, which a bigint holds exactly and a store must compare as one.
1033
1029
  export function entityETag(updateNumber, updatedAt) {
1034
1030
  if (updateNumber < 0n || updatedAt < 0n)
1035
1031
  throw new InvalidArgumentException("An update number and an updatedAt are never negative");
1036
1032
  return (updateNumber << UPDATED_AT_BITS) | updatedAt;
1037
1033
  }
1038
- // A message this client wrote, which has no update number of its own until a server assigns one, and
1039
- // has none in the store as long as it is not pushed (see pushedEntity). It is versioned at the update
1040
- // number of the copy it amends, which is the one heldETag carries, and so it is the later copy of that
1041
- // version by its updatedAt: a copy of the version it amends, sent again by the server, is not, and a
1042
- // later version of the server's, with a greater update number, replaces it. One that amends nothing
1043
- // is at zero. A message that has an update number of its own is versioned by it, as any entity is.
1034
+ // A message this client wrote has no update number until a server assigns one, and has none in the
1035
+ // store while unpushed (see pushedEntity). It is versioned at the update number of the copy it
1036
+ // amends, which heldETag carries, so by its updatedAt it is the later copy of that version. The
1037
+ // server sending the amended copy again does not replace it, but a later server version with a
1038
+ // greater update number does. A message that amends nothing is at zero. A message with an update
1039
+ // number of its own is versioned by it, as any entity is.
1044
1040
  function messageToBlob(message, heldETag) {
1045
1041
  const entity = getEntity(message.entity);
1046
1042
  const updateNumber = entity.updateNumber !== 0n
@@ -1054,10 +1050,10 @@ function messageToBlob(message, heldETag) {
1054
1050
  bytes: proto_Message.toBinary(message),
1055
1051
  };
1056
1052
  }
1057
- // The messages a container holds that are messages: a tombstone is held, and is not one. It is left out
1058
- // whatever it carries, and not by what is found in it - a payload says what a message is, and a
1059
- // tombstone sent by a server that left the payload on it would otherwise be read as a cart or an order.
1060
- function messagesFromBlobs(blobs) {
1053
+ // The messages a container holds that are deleted (isDeleted) or live. Only the update number says
1054
+ // which, never what a message carries. A deleted message is a completed order when it carries one,
1055
+ // and is never a customer's cart. Each as stored.
1056
+ function messagesFromBlobs(blobs, isDeleted = false) {
1061
1057
  const result = [];
1062
1058
  for (const blob of blobs) {
1063
1059
  if (blob.id < 0n)
@@ -1065,7 +1061,7 @@ function messagesFromBlobs(blobs) {
1065
1061
  if (Ids.getEntityTypeId(blob.id) !== proto_Constants.MESSAGE_ENTITY_TYPE_ID)
1066
1062
  continue;
1067
1063
  const message = proto_Message.fromBinary(blob.bytes);
1068
- if (!isDeletedEntity(getEntity(message.entity)))
1064
+ if (isDeletedEntity(getEntity(message.entity)) === isDeleted)
1069
1065
  result.push(message);
1070
1066
  }
1071
1067
  return result;
@@ -1115,12 +1111,12 @@ function getItemId(item) {
1115
1111
  }
1116
1112
  // #endregion
1117
1113
  // #region Values
1118
- // An absent optional string is null and not empty, because Java tells the two apart
1114
+ // An absent optional string is null, because Java tells absent from empty
1119
1115
  function stringFromProto(value) {
1120
1116
  return value === undefined ? null : value;
1121
1117
  }
1122
- // The way back. An empty string goes out absent as well: the two mean the same thing to a caller
1123
- // that had nothing to say, and sending one of them would have the field come back as the other.
1118
+ // The way back. An empty string goes out absent too: the two mean the same, and sending one could
1119
+ // bring the field back as the other.
1124
1120
  function stringToProto(value) {
1125
1121
  return value === null || value.length === 0 ? undefined : value;
1126
1122
  }
@@ -1130,9 +1126,9 @@ function attachmentBaseUrlFromProto(urlAndToken) {
1130
1126
  function addressFromProto(address) {
1131
1127
  return new Address(address.id, address.addressType, stringFromProto(address.unitNumber), stringFromProto(address.community), stringFromProto(address.line2), stringFromProto(address.city), stringFromProto(address.state), stringFromProto(address.pinCode), address.countryCode);
1132
1128
  }
1133
- // The way back, for a delivery address a customer sends out directly rather than by id - see
1134
- // buildOrderMessage. id is sent as whatever Address carries, though it means nothing outside the
1135
- // list it was read from and this address belongs to none.
1129
+ // The way back, for a delivery address sent directly instead of by id - see buildOrderMessage. id is
1130
+ // whatever Address carries, though it means nothing outside the list it was read from, and this
1131
+ // address is in none.
1136
1132
  function addressToProto(address) {
1137
1133
  return proto_Address.create({
1138
1134
  id: address.id,
@@ -1146,7 +1142,7 @@ function addressToProto(address) {
1146
1142
  countryCode: address.countryCode,
1147
1143
  });
1148
1144
  }
1149
- // The way back, for the addresses of a user being sent to the server rather than read from it.
1145
+ // The way back, for the addresses of a user sent to the server.
1150
1146
  function addressListToProto(addressList) {
1151
1147
  return proto_AddressList.create({
1152
1148
  defaultAddressId: addressList.defaultAddressId,
@@ -1158,8 +1154,8 @@ function addressListFromProto(addressList) {
1158
1154
  return AddressList.EMPTY;
1159
1155
  return new AddressList(addressList.defaultAddressId, Object.freeze(addressList.addresses.map(addressFromProto)));
1160
1156
  }
1161
- // The wire carries a tax's raw code, meaningful only within a country - that of the business the tax
1162
- // is on, which every caller therefore has to name
1157
+ // The wire carries a tax's raw code, meaningful only within a country, so every caller names that of
1158
+ // the business the tax is on.
1163
1159
  function taxFromProto(tax, countryCode) {
1164
1160
  return Tax.fromScaledValues(countryCode, tax.taxCode, tax.rate);
1165
1161
  }
@@ -1193,11 +1189,11 @@ function workingHoursRangeFromProto(range) {
1193
1189
  function workingHoursFromProto(workingHours) {
1194
1190
  return new WorkingHours(Object.freeze(workingHours.ranges.map(workingHoursRangeFromProto)));
1195
1191
  }
1196
- // An absent WorkingDays means the business works every day, all day - the Java default, not "no day at all"
1192
+ // An absent WorkingDays means the business works every day, all day (the Java default)
1197
1193
  function workingDaysFromProto(workingDays) {
1198
1194
  if (workingDays === undefined)
1199
1195
  return WorkingDays.ALL_DAYS;
1200
- // Not an optional field, so an unspecified zone arrives as an empty string rather than absent
1196
+ // Not an optional field, so an unspecified zone arrives as an empty string
1201
1197
  return new WorkingDays(Object.freeze([...workingDays.days]), workingDays.timeZone.length === 0 ? null : workingDays.timeZone, Object.freeze(workingDays.workingHours.map(workingHoursFromProto)));
1202
1198
  }
1203
1199
  function shoppingListItemFromProto(item, countryCode) {
@@ -1266,20 +1262,18 @@ function orderFromProto(message, salesOrder, countryCode) {
1266
1262
  const entity = getEntity(message.entity);
1267
1263
  return Order.fromScaledValues(toEntityHeader(entity), message.senderId, message.status, stringFromProto(message.senderName), stringFromProto(message.senderPhone), salesOrder.status, salesOrder.fulfillmentMode, shoppingListFromProto(salesOrder.shoppingList, countryCode), salesOrder.shoppingListTotal, salesOrder.shoppingListCount, salesOrder.deliveryAddressId, salesOrder.deliveryAddress === undefined ? null : addressFromProto(salesOrder.deliveryAddress), salesOrder.creationDate, stringFromProto(salesOrder.note), salesOrder.paymentReceived);
1268
1264
  }
1269
- // The one part of a cart message a customer is shown. Absent rather than empty when the cart holds
1270
- // nothing, so that "emptied by placing an order" and "never had one" are the same answer - which is
1271
- // also what makes createOrder's emptied cart indistinguishable from a cart that was never created.
1272
- // Everything else the message carries is the server's: who sent it, how far the business has got
1273
- // with reading it, and the fulfillment mode Java keeps there, which belongs to an order here.
1265
+ // The one part of a cart message a customer is shown. Null when the cart holds nothing, so a cart
1266
+ // emptied by placing an order and one never created give the same answer. The rest of the message is
1267
+ // the server's or read elsewhere: who sent it, how far the business has read it, and the fulfillment
1268
+ // mode, which buildBusiness reads as defaultFulfillmentMode.
1274
1269
  function cartShoppingListFromProto(partyPreferences, countryCode) {
1275
1270
  const shoppingList = partyPreferences.shoppingList;
1276
1271
  return shoppingList === undefined ? null : shoppingListFromProto(shoppingList, countryCode);
1277
1272
  }
1278
1273
  // #endregion
1279
1274
  // #region Photos
1280
- // The photos of one entity, in no particular order - a UI orders these for display. Each is built
1281
- // with the base url resolved for this response, since a Photo takes it at construction rather than
1282
- // looking it up later.
1275
+ // The photos of one entity, in no particular order (a UI orders them). Each is built with the
1276
+ // response's base url, since a Photo takes it at construction.
1283
1277
  function takePhotos(attachmentsByOwner, owningEntityId, attachmentBaseUrl) {
1284
1278
  const attachments = attachmentsByOwner.get(owningEntityId);
1285
1279
  if (attachments === undefined)