@peabody-soft/mbs-model-ts 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Address.js +8 -11
- package/dist/Attachment.js +7 -8
- package/dist/AttachmentBaseUrl.js +10 -10
- package/dist/AuthTokens.js +12 -7
- package/dist/Business.js +19 -19
- package/dist/BusinessIdentifier.js +2 -2
- package/dist/BusinessLink.js +16 -16
- package/dist/BusinessProfile.js +4 -4
- package/dist/CartEditor.js +137 -166
- package/dist/Catalog.js +19 -25
- package/dist/Constants.js +10 -10
- package/dist/ContainerNames.js +10 -11
- package/dist/Entity.js +11 -9
- package/dist/Enums.js +2 -2
- package/dist/Exceptions.js +2 -2
- package/dist/GeneralSettings.js +22 -21
- package/dist/HttpStatusCodes.js +10 -11
- package/dist/Ids.js +24 -30
- package/dist/Item.js +6 -7
- package/dist/LatLong.js +3 -3
- package/dist/LiveData.js +6 -6
- package/dist/Measures.js +2 -2
- package/dist/Message.js +6 -6
- package/dist/MockDatabase.js +2 -2
- package/dist/Model.js +292 -359
- package/dist/Order.js +20 -17
- package/dist/Package.js +7 -8
- package/dist/PackageIdentifier.js +2 -2
- package/dist/PaymentMethodDetail.js +4 -4
- package/dist/Photo.js +6 -7
- package/dist/Price.js +8 -9
- package/dist/PriceList.js +2 -2
- package/dist/Product.js +17 -22
- package/dist/ProductCategory.js +3 -4
- package/dist/Profile.js +26 -27
- package/dist/ProtoMap.d.ts +3 -0
- package/dist/ProtoMap.js +288 -294
- package/dist/Routes.js +3 -3
- package/dist/ServiceExceptions.js +11 -12
- package/dist/ServiceProxy.js +19 -23
- package/dist/ShoppingList.js +112 -130
- package/dist/SyncCache.d.ts +3 -0
- package/dist/SyncCache.js +92 -75
- package/dist/SyncKeys.js +8 -8
- package/dist/SyncState.js +5 -5
- package/dist/Tax.js +16 -21
- package/dist/TaxHelper.js +22 -30
- package/dist/TaxType.js +8 -9
- package/dist/UserProfile.js +4 -4
- package/dist/WorkingDays.js +15 -15
- package/dist/index.js +9 -9
- 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).
|
|
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
|
|
89
|
-
//
|
|
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
|
-
//
|
|
101
|
-
//
|
|
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
|
|
106
|
-
//
|
|
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
|
|
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,
|
|
121
|
-
//
|
|
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
|
|
127
|
-
//
|
|
128
|
-
//
|
|
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.
|
|
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
|
|
141
|
-
//
|
|
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
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
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
|
|
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
|
|
230
|
-
//
|
|
231
|
-
export function
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
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
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
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
|
|
276
|
-
// the one before
|
|
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
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
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
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
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
|
|
317
|
-
//
|
|
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
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
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
|
|
334
|
-
//
|
|
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
|
|
339
|
-
// to one and has no sync in.
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
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.
|
|
358
|
-
//
|
|
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
|
|
370
|
-
//
|
|
371
|
-
//
|
|
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
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
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
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
//
|
|
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
|
|
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.
|
|
409
|
-
// tombstone
|
|
410
|
-
//
|
|
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
|
|
418
|
-
//
|
|
419
|
-
//
|
|
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
|
|
428
|
-
//
|
|
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
|
|
436
|
-
//
|
|
437
|
-
//
|
|
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
|
|
445
|
-
// configuration that carries them, unless it
|
|
446
|
-
//
|
|
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
|
|
459
|
-
//
|
|
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,
|
|
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
|
|
478
|
-
//
|
|
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
|
|
483
|
-
// null
|
|
484
|
-
//
|
|
485
|
-
//
|
|
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
|
|
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
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
//
|
|
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
|
-
//
|
|
530
|
-
//
|
|
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.
|
|
564
|
-
//
|
|
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
|
|
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
|
|
585
|
-
//
|
|
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
|
|
604
|
-
//
|
|
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
|
|
614
|
-
//
|
|
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
|
|
621
|
-
//
|
|
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
|
-
//
|
|
641
|
-
//
|
|
642
|
-
//
|
|
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
|
|
651
|
-
//
|
|
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
|
|
654
|
-
//
|
|
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
|
-
//
|
|
678
|
-
//
|
|
679
|
-
//
|
|
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
|
|
691
|
-
//
|
|
692
|
-
//
|
|
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
|
|
713
|
-
//
|
|
714
|
-
//
|
|
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,
|
|
740
|
-
//
|
|
741
|
-
//
|
|
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
|
|
754
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
764
|
-
//
|
|
765
|
-
// read from
|
|
766
|
-
// is the emptied one, and the older one's list is not shown
|
|
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
|
|
772
|
-
//
|
|
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
|
-
//
|
|
776
|
-
//
|
|
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
|
|
817
|
-
//
|
|
818
|
-
//
|
|
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
|
-
//
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
//
|
|
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
|
|
855
|
-
//
|
|
856
|
-
//
|
|
857
|
-
//
|
|
858
|
-
//
|
|
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
|
|
876
|
-
//
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
//
|
|
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
|
-
//
|
|
905
|
-
//
|
|
906
|
-
//
|
|
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
|
|
912
|
-
// replaces a caller's own with the
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
//
|
|
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
|
|
923
|
-
//
|
|
924
|
-
//
|
|
925
|
-
//
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
//
|
|
929
|
-
//
|
|
930
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
|
969
|
-
//
|
|
970
|
-
//
|
|
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
|
|
988
|
-
//
|
|
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
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1016
|
-
//
|
|
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
|
|
1028
|
-
//
|
|
1029
|
-
//
|
|
1030
|
-
// decides between
|
|
1031
|
-
//
|
|
1032
|
-
//
|
|
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
|
|
1039
|
-
//
|
|
1040
|
-
//
|
|
1041
|
-
//
|
|
1042
|
-
//
|
|
1043
|
-
//
|
|
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
|
|
1058
|
-
//
|
|
1059
|
-
//
|
|
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 (
|
|
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
|
|
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
|
|
1123
|
-
//
|
|
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
|
|
1134
|
-
//
|
|
1135
|
-
//
|
|
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
|
|
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
|
|
1162
|
-
//
|
|
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
|
|
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
|
|
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.
|
|
1270
|
-
//
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
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
|
|
1281
|
-
//
|
|
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)
|