@peabody-soft/mbs-model-ts 1.2.0 → 1.4.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/Business.d.ts +5 -0
- package/dist/Business.js +13 -1
- package/dist/Model.d.ts +1 -1
- package/dist/Model.js +119 -90
- package/dist/ProtoMap.d.ts +8 -5
- package/dist/ProtoMap.js +103 -61
- package/dist/SyncCache.d.ts +3 -3
- package/dist/SyncCache.js +29 -27
- package/dist/WorkingDays.d.ts +3 -2
- package/dist/WorkingDays.js +16 -12
- package/dist/generated/WorkingDays.d.ts +9 -1
- package/dist/generated/WorkingDays.js +12 -4
- package/package.json +2 -2
package/dist/Business.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FulfillmentMode as proto_FulfillmentMode } from "./generated/FulfillmentMode";
|
|
1
2
|
import AttachmentBaseUrl from "./AttachmentBaseUrl";
|
|
2
3
|
import CartEditor from "./CartEditor";
|
|
3
4
|
import Catalog from "./Catalog";
|
|
@@ -22,6 +23,8 @@ export default class Business extends Entity implements UpdatableBusiness {
|
|
|
22
23
|
private set orders(value);
|
|
23
24
|
get cart(): ShoppingList | null;
|
|
24
25
|
private set cart(value);
|
|
26
|
+
get defaultFulfillmentMode(): proto_FulfillmentMode;
|
|
27
|
+
private set defaultFulfillmentMode(value);
|
|
25
28
|
get catalog(): Catalog;
|
|
26
29
|
private set catalog(value);
|
|
27
30
|
createCartEditor(): CartEditor;
|
|
@@ -31,6 +34,7 @@ export default class Business extends Entity implements UpdatableBusiness {
|
|
|
31
34
|
private _photos;
|
|
32
35
|
private _orders;
|
|
33
36
|
private _cart;
|
|
37
|
+
private _defaultFulfillmentMode;
|
|
34
38
|
private _catalog;
|
|
35
39
|
private _attachmentBaseUrl;
|
|
36
40
|
}
|
|
@@ -39,6 +43,7 @@ export interface UpdatableBusiness {
|
|
|
39
43
|
set photos(value: ReadonlyArray<Photo>);
|
|
40
44
|
set orders(value: ReadonlyArray<Order>);
|
|
41
45
|
set cart(value: ShoppingList | null);
|
|
46
|
+
set defaultFulfillmentMode(value: proto_FulfillmentMode);
|
|
42
47
|
set catalog(value: Catalog);
|
|
43
48
|
set attachmentBaseUrl(value: AttachmentBaseUrl | null);
|
|
44
49
|
}
|
package/dist/Business.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FulfillmentMode as proto_FulfillmentMode } from "./generated/FulfillmentMode";
|
|
1
2
|
import CartEditor from "./CartEditor";
|
|
2
3
|
import Catalog from "./Catalog";
|
|
3
4
|
import Entity from "./Entity";
|
|
@@ -15,6 +16,7 @@ export default class Business extends Entity {
|
|
|
15
16
|
this._photos = Object.freeze([]);
|
|
16
17
|
this._orders = Object.freeze([]);
|
|
17
18
|
this._cart = null;
|
|
19
|
+
this._defaultFulfillmentMode = proto_FulfillmentMode.DELIVERY;
|
|
18
20
|
this._catalog = Catalog.EMPTY;
|
|
19
21
|
this._attachmentBaseUrl = null;
|
|
20
22
|
}
|
|
@@ -56,13 +58,23 @@ export default class Business extends Entity {
|
|
|
56
58
|
// cart, and null again once they have ordered what was in it - the message the cart travels in
|
|
57
59
|
// outlives being emptied, but nothing of it is a customer's business, so what is exposed is the
|
|
58
60
|
// list alone. The fulfillment mode is not part of it: that is chosen when an order is placed, and
|
|
59
|
-
// is a field of the Order.
|
|
61
|
+
// is a field of the Order - see defaultFulfillmentMode for the one other place it is read.
|
|
60
62
|
get cart() {
|
|
61
63
|
return this._cart;
|
|
62
64
|
}
|
|
63
65
|
set cart(value) {
|
|
64
66
|
this._cart = value;
|
|
65
67
|
}
|
|
68
|
+
// What to preselect for this customer's next order: whatever the party preferences message
|
|
69
|
+
// already carries, which createOrder stamps with the mode an order was just placed under - so
|
|
70
|
+
// this is that order's own choice, not necessarily today's cart's. DELIVERY when there has never
|
|
71
|
+
// been a party preferences message to read one from.
|
|
72
|
+
get defaultFulfillmentMode() {
|
|
73
|
+
return this._defaultFulfillmentMode;
|
|
74
|
+
}
|
|
75
|
+
set defaultFulfillmentMode(value) {
|
|
76
|
+
this._defaultFulfillmentMode = value;
|
|
77
|
+
}
|
|
66
78
|
// Catalog.EMPTY until the catalog of this business arrives
|
|
67
79
|
get catalog() {
|
|
68
80
|
return this._catalog;
|
package/dist/Model.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export default class Model {
|
|
|
24
24
|
createOrder(orderId: bigint, fulfillmentMode: proto_FulfillmentMode, deliveryAddressId: bigint, deliveryAddress: Address | null, note: string | null): Promise<void>;
|
|
25
25
|
private beginSignIn;
|
|
26
26
|
private pushBusinessMessages;
|
|
27
|
-
private
|
|
27
|
+
private storeCartInCache;
|
|
28
28
|
private requireBusinessBeingBrowsed;
|
|
29
29
|
private requireCustomerOfBusiness;
|
|
30
30
|
private static requireDeliveryAddress;
|
package/dist/Model.js
CHANGED
|
@@ -8,6 +8,7 @@ import Ids from "./Ids";
|
|
|
8
8
|
import LiveData from "./LiveData";
|
|
9
9
|
import * as ProtoMap from "./ProtoMap";
|
|
10
10
|
import { InternalServerException } from "./ServiceExceptions";
|
|
11
|
+
import ShoppingList from "./ShoppingList";
|
|
11
12
|
import SyncCache from "./SyncCache";
|
|
12
13
|
// The root of the object model, and the only thing a UI has to be given. Call initialize once,
|
|
13
14
|
// then always fetch the current instance via getInstance.
|
|
@@ -42,12 +43,25 @@ class Model {
|
|
|
42
43
|
// Signs in with the given credentials and syncs this subject's profile in - for a subject the
|
|
43
44
|
// server already has a profile for; createUser is what a subject with none yet uses instead. Also
|
|
44
45
|
// reads in the business being browsed, if any, now signed in as this customer.
|
|
46
|
+
//
|
|
47
|
+
// Idempotent on failure: beginSignIn's own "already signed in" guard aside, anything that throws
|
|
48
|
+
// after it - a bad credential, a refused push while reading in the business being browsed -
|
|
49
|
+
// discards this instance via signOut rather than leaving it half signed in. profileSyncIn
|
|
50
|
+
// publishes a cached profile before it ever reaches the network, so without this, a retry after
|
|
51
|
+
// such a failure could find _profile already set and trip that same guard despite never having
|
|
52
|
+
// actually signed in.
|
|
45
53
|
async signIn(authenticationMethod, subjectId, authenticationToken) {
|
|
46
54
|
this.beginSignIn(authenticationMethod, subjectId, authenticationToken);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
try {
|
|
56
|
+
const authTokens = await this.requireProfileAuthTokens();
|
|
57
|
+
await this.profileSyncIn(ProtoMap.buildProfileSyncOutRequestFromCache(await Model.getCache().readProfile(this.requireProfileContainerName())), authTokens);
|
|
58
|
+
if (this._businessId !== null)
|
|
59
|
+
await this.businessSyncIn();
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
await this.signOut();
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
51
65
|
}
|
|
52
66
|
// Signs up with the given credentials, creating userToCreate as this subject's profile - for a
|
|
53
67
|
// subject the server has no profile for yet; signIn is what an already known subject uses instead.
|
|
@@ -75,11 +89,16 @@ class Model {
|
|
|
75
89
|
async signOut() {
|
|
76
90
|
Model._instance = null;
|
|
77
91
|
}
|
|
78
|
-
// Names the business to browse and reads it in, publishing null first so a
|
|
79
|
-
//
|
|
92
|
+
// Names the business to browse and reads it in, publishing null first so a Business being
|
|
93
|
+
// replaced is never shown as the one now being browsed - but only when it IS being replaced.
|
|
94
|
+
// This is also how a caller re-reads the shop it is already on, and nulling unconditionally
|
|
95
|
+
// would blank the UI for the length of the read and undo syncBusinessContainer's own
|
|
96
|
+
// cache-first publish two lines later.
|
|
80
97
|
async setBusinessId(businessId) {
|
|
98
|
+
const isDifferentBusiness = this._businessId !== businessId;
|
|
81
99
|
this._businessId = businessId;
|
|
82
|
-
|
|
100
|
+
if (isDifferentBusiness)
|
|
101
|
+
this.publishBusiness(null);
|
|
83
102
|
await this.businessSyncIn();
|
|
84
103
|
}
|
|
85
104
|
// #region What a customer writes
|
|
@@ -92,57 +111,50 @@ class Model {
|
|
|
92
111
|
// of the difference.
|
|
93
112
|
// Both need this customer to be a member of the business being browsed - three separate things
|
|
94
113
|
// to be missing, so three separate failures; see requireCustomerOfBusiness.
|
|
95
|
-
// Creates or replaces this customer's cart,
|
|
96
|
-
// empties the cart -
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
// once - without tracking what it last sent, and what it checks against is the store's own copy
|
|
105
|
-
// rather than a caller's idea of one. It is not a substitute for reading the business, though: a
|
|
106
|
-
// push answers with whatever else of this customer's changed, and a call that does not go out
|
|
107
|
-
// brings none of that back.
|
|
114
|
+
// Creates or replaces this customer's cart, on the server and in the store both. An empty list
|
|
115
|
+
// empties the cart - the same state as never having had one.
|
|
116
|
+
// Signed out, this is just updateCartLocalOnly - there is no server cart yet for a visitor the
|
|
117
|
+
// server has not heard of. Signed in, it costs a request, so an editor should write every change
|
|
118
|
+
// with updateCartLocalOnly and call this on its own schedule (debounced, on blur, a timer).
|
|
119
|
+
// Safe to call often: hasShoppingListChanged checks against the store's own copy rather than
|
|
120
|
+
// whatever the caller last sent, so a call that matches what is already there returns without a
|
|
121
|
+
// request. Not a substitute for reading the business, though - a push answers only with whatever
|
|
122
|
+
// else of this customer's changed, and a call that does not go out brings none of that back.
|
|
108
123
|
async updateCart(shoppingList) {
|
|
109
124
|
if (this._profile.value === null)
|
|
110
125
|
return this.updateCartLocalOnly(shoppingList);
|
|
111
126
|
const target = this.requireCustomerOfBusiness();
|
|
112
|
-
const stored = await Model.getCache().
|
|
113
|
-
|
|
114
|
-
if (!ProtoMap.isCartWorthPushing(stored, cartMessage))
|
|
127
|
+
const stored = await Model.getCache().readPartyPreferences(target.containerName);
|
|
128
|
+
if (!ProtoMap.hasShoppingListChanged(stored, shoppingList))
|
|
115
129
|
return;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// The store keeps whichever copy has the greater eTag, and buildCartMessage dates an amendment
|
|
130
|
-
// past the copy it replaces, so a cart stored here cannot be undone by a sync in bringing the
|
|
131
|
-
// server's older one back.
|
|
130
|
+
const partyPreferencesMessage = ProtoMap.buildPartyPreferencesMessage(Ids.getPartyPreferencesMessageId(target.businessLink.id), stored, target.senderId, shoppingList, null, BigInt(Date.now()));
|
|
131
|
+
await this.pushBusinessMessages(target, [partyPreferencesMessage]);
|
|
132
|
+
}
|
|
133
|
+
// The same, stored locally and not sent. Call this on every edit - a request per tap of a
|
|
134
|
+
// quantity stepper is not worth making - then call updateCart on your own schedule (debounced,
|
|
135
|
+
// on blur, before signing in) to actually send it.
|
|
136
|
+
// Nothing is lost by leaving it unsent: it survives a closed tab, the next updateCart sends it,
|
|
137
|
+
// and createOrder reads straight from the store. All a caller gives up is that the server, and so
|
|
138
|
+
// this customer's other devices, hear about the change late.
|
|
139
|
+
// It also cannot be undone by a stale sync in bringing an older server copy back: the store keeps
|
|
140
|
+
// whichever copy has the later eTag, and buildPartyPreferencesMessage always dates an amendment
|
|
141
|
+
// past what it replaces.
|
|
132
142
|
async updateCartLocalOnly(shoppingList) {
|
|
133
|
-
// No membership to derive a
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
// a User, and the first push after signing in names the real one.
|
|
143
|
+
// No membership to derive a message id from while signed out, so one is drawn instead - once,
|
|
144
|
+
// and only once: buildPartyPreferencesMessage keeps amending the same entity by reusing its id,
|
|
145
|
+
// so a second edit cannot leave two behind. The sender is ENTITY_ID_NULL for want of a User;
|
|
146
|
+
// the first push after signing in names the real one.
|
|
138
147
|
if (this._profile.value === null)
|
|
139
|
-
return this.
|
|
148
|
+
return this.storeCartInCache(ContainerNames.forBusiness(this.requireBusinessBeingBrowsed()), Ids.getNewId(proto_Constants.MESSAGE_ENTITY_TYPE_ID), BigInt(proto_Constants.ENTITY_ID_NULL), shoppingList);
|
|
140
149
|
const target = this.requireCustomerOfBusiness();
|
|
141
|
-
return this.
|
|
150
|
+
return this.storeCartInCache(target.containerName, Ids.getPartyPreferencesMessageId(target.businessLink.id), target.senderId, shoppingList);
|
|
142
151
|
}
|
|
143
152
|
// Places an order for what is in the cart, and empties it - the list is used up becoming the
|
|
144
153
|
// order, as Java SalesOrderViewModel.saveChanges does on MODE_CREATE. Takes no shopping list
|
|
145
154
|
// itself for that reason: the cart is the order.
|
|
155
|
+
// The emptied party preferences message is also stamped with this fulfillmentMode, so it becomes
|
|
156
|
+
// this order's rather than whatever the cart last held - see Business.defaultFulfillmentMode,
|
|
157
|
+
// which is what reads it back.
|
|
146
158
|
// orderId is the caller's to mint, with Ids.getNewId(Constants.MESSAGE_ENTITY_TYPE_ID) - not
|
|
147
159
|
// minted here - so that a caller holds it before the call is made: to find the order once placed
|
|
148
160
|
// without a server-assigned id to wait for, and to retry a call a NetworkConnectionException left
|
|
@@ -154,9 +166,11 @@ class Model {
|
|
|
154
166
|
const target = this.requireCustomerOfBusiness();
|
|
155
167
|
// Read from the store, not the published Business, so the order reflects the cart as the
|
|
156
168
|
// server last confirmed it, not a value some observer may still hold
|
|
157
|
-
const
|
|
158
|
-
const shoppingList =
|
|
159
|
-
|
|
169
|
+
const partyPreferencesMessage = await Model.getCache().readPartyPreferences(target.containerName);
|
|
170
|
+
const shoppingList = partyPreferencesMessage === null
|
|
171
|
+
? ShoppingList.EMPTY
|
|
172
|
+
: ProtoMap.buildShoppingList(partyPreferencesMessage);
|
|
173
|
+
if (shoppingList.existingItems.length === 0)
|
|
160
174
|
throw new IllegalOperationException("The cart holds nothing, so there is no order to place");
|
|
161
175
|
Model.requireDeliveryAddress(target.addresses, fulfillmentMode, deliveryAddressId, deliveryAddress);
|
|
162
176
|
// One creation time for both, and it is what the order was placed at
|
|
@@ -165,7 +179,10 @@ class Model {
|
|
|
165
179
|
// but a cart still full, if the second one failed
|
|
166
180
|
await this.pushBusinessMessages(target, [
|
|
167
181
|
ProtoMap.buildOrderMessage(orderId, target.senderId, shoppingList, fulfillmentMode, deliveryAddressId, deliveryAddress, note, now),
|
|
168
|
-
|
|
182
|
+
// fulfillmentMode is the order's, not left as whatever the cart held: placing an order is
|
|
183
|
+
// this customer's own choice of how it is fulfilled, freshly made, so it is what
|
|
184
|
+
// defaultFulfillmentMode should offer next time rather than a possibly stale earlier one.
|
|
185
|
+
ProtoMap.buildPartyPreferencesMessage(Ids.getPartyPreferencesMessageId(target.businessLink.id), partyPreferencesMessage, target.senderId, null, fulfillmentMode, now),
|
|
169
186
|
]);
|
|
170
187
|
}
|
|
171
188
|
// #endregion
|
|
@@ -198,16 +215,13 @@ class Model {
|
|
|
198
215
|
if (changeCount !== 0)
|
|
199
216
|
this.publishBusiness(await cache.readBusiness(target.containerName));
|
|
200
217
|
}
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
// across a sign in: the container named for a new membership is seeded from that one - see
|
|
207
|
-
// syncCustomerBusiness.
|
|
208
|
-
async storeCart(containerName, cartMessageId, senderId, shoppingList) {
|
|
218
|
+
// Amends whatever cart containerName already holds and writes the result back to the cache -
|
|
219
|
+
// nothing is pushed to the server. updateCartLocalOnly's two callers differ only in which
|
|
220
|
+
// container, which id, and which sender: a visitor's has a freshly drawn id and no sender, a
|
|
221
|
+
// customer's has the id their membership derives and their own sender id.
|
|
222
|
+
async storeCartInCache(containerName, messageId, senderId, shoppingList) {
|
|
209
223
|
const cache = Model.getCache();
|
|
210
|
-
const changeCount = await cache.
|
|
224
|
+
const changeCount = await cache.writePartyPreferences(containerName, ProtoMap.buildPartyPreferencesMessage(messageId, await cache.readPartyPreferences(containerName), senderId, shoppingList, null, BigInt(Date.now())));
|
|
211
225
|
// Published on what the store took, the same rule every read and push keeps
|
|
212
226
|
if (changeCount !== 0)
|
|
213
227
|
this.publishBusiness(await cache.readBusiness(containerName));
|
|
@@ -279,23 +293,39 @@ class Model {
|
|
|
279
293
|
const link = await this.requireBusinessLink(businessId, profile);
|
|
280
294
|
const cache = Model.getCache();
|
|
281
295
|
const containerName = Model.getBusinessContainerName(link);
|
|
282
|
-
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
+
const anonymousName = ContainerNames.forBusiness(businessId);
|
|
297
|
+
// What this device read while nobody was signed in, folded into the container it is read
|
|
298
|
+
// under now - the same rule the server's own pages take: writeBusiness keeps the later of two
|
|
299
|
+
// copies per entity by updatedAt. Unconditional, since a returning customer's own container
|
|
300
|
+
// can easily be the staler of the two.
|
|
301
|
+
// Not the cart, which is settled below: the two containers hold it under different ids, so a
|
|
302
|
+
// merge keyed on entity id would file the visitor's as a second cart instead of replacing one.
|
|
303
|
+
const anonymous = await cache.readBusiness(anonymousName);
|
|
304
|
+
// Out of the messages just read, rather than a second read of the same container -
|
|
305
|
+
// partyPreferencesMessageFromMessages picks the same one readPartyPreferences(anonymousName)
|
|
306
|
+
// would.
|
|
307
|
+
const anonymousPartyPreferences = anonymous === null ? null : ProtoMap.partyPreferencesMessageFromMessages(anonymous.messages);
|
|
308
|
+
if (anonymous !== null) {
|
|
309
|
+
anonymous.messages = anonymous.messages.filter((message) => !ProtoMap.isPartyPreferencesMessage(message));
|
|
310
|
+
await cache.writeBusiness(containerName, anonymous);
|
|
296
311
|
}
|
|
297
312
|
const authTokens = await this.requireBusinessAuthTokens(containerName, link);
|
|
298
313
|
await this.syncBusinessContainer(containerName, businessId, ProtoMap.buildAuthTokens(authTokens));
|
|
314
|
+
// THE CART A VISITOR BUILT BEFORE SIGNING IN. Signing in changes which container is being
|
|
315
|
+
// browsed, so without this it is simply left behind - and a customer has to build it again
|
|
316
|
+
// having just proved who they are.
|
|
317
|
+
if (anonymousPartyPreferences === null)
|
|
318
|
+
return;
|
|
319
|
+
// Read after the sync, so this is weighed against what the server just settled rather than a
|
|
320
|
+
// local copy about to be replaced. Last written wins, by updatedAt.
|
|
321
|
+
const userPartyPreferences = await cache.readPartyPreferences(containerName);
|
|
322
|
+
if (userPartyPreferences === null ||
|
|
323
|
+
ProtoMap.getMessageUpdatedAt(anonymousPartyPreferences) >
|
|
324
|
+
ProtoMap.getMessageUpdatedAt(userPartyPreferences))
|
|
325
|
+
await this.updateCart(ProtoMap.buildShoppingList(anonymousPartyPreferences));
|
|
326
|
+
// Dropped either way, carried over or beaten by the customer's own: the anonymous container is
|
|
327
|
+
// what the next visitor on this device reads, and a settled cart has no business staying there.
|
|
328
|
+
await cache.deletePartyPreferences(anonymousName);
|
|
299
329
|
}
|
|
300
330
|
// The plain businessId-named container: an anonymous read, before signing in or joining, or to
|
|
301
331
|
// learn a business's entity id while joining - see readJoinBusinessId.
|
|
@@ -395,15 +425,14 @@ class Model {
|
|
|
395
425
|
return ContainerNames.forBusiness(Ids.toStorageHexString(link.id));
|
|
396
426
|
}
|
|
397
427
|
// The credentials that sign a call about a customer's own business container. The authentication
|
|
398
|
-
// token always comes from memory,
|
|
399
|
-
//
|
|
400
|
-
// The user token and link token are read independently
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
// inconsistency, thrown rather than sent unsigned.
|
|
428
|
+
// token always comes from memory, not a cache read - same as buildProfileAuthTokens, since a
|
|
429
|
+
// stored AuthTokens never persists it.
|
|
430
|
+
// The user token and link token are read independently, not as one object: storeIssuedTokens
|
|
431
|
+
// overwrites the whole blob per call, so trusting only that would lose whichever token the last
|
|
432
|
+
// call didn't reissue. Each falls back to its own source instead - the profile container's user
|
|
433
|
+
// token, this link's own linkToken - which is exactly what a fresh membership lacks and what the
|
|
434
|
+
// write below now gives this container to reissue over.
|
|
435
|
+
// A link with no linkToken at all is a server inconsistency: thrown rather than sent unsigned.
|
|
407
436
|
async requireBusinessAuthTokens(containerName, link) {
|
|
408
437
|
const authenticationMethod = this._authenticationMethod;
|
|
409
438
|
const authenticationToken = this._authenticationToken;
|
|
@@ -484,19 +513,19 @@ class Model {
|
|
|
484
513
|
// #endregion
|
|
485
514
|
// #region Driving one call
|
|
486
515
|
// A join that was asked for but not answered. Shared by both calls that can carry a
|
|
487
|
-
// joinBusinessId, since
|
|
516
|
+
// joinBusinessId, since a join answers the same two things regardless of which one asked: the
|
|
488
517
|
// BusinessLink (the membership) and the Business it names. Both are needed to reach a business -
|
|
489
|
-
// the link for its token, the Business for the url-friendly id everything else is built from -
|
|
490
|
-
// either one missing leaves the caller told it
|
|
518
|
+
// the link for its token, the Business for the url-friendly id everything else is built from -
|
|
519
|
+
// so either one missing leaves the caller told it is a customer of something it cannot reach.
|
|
491
520
|
//
|
|
492
521
|
// A ServiceException, not one of Exceptions.ts: nothing the caller passed was wrong - the server
|
|
493
|
-
// answered 200 and left out what was asked for - so a caller can retry or carry on with what
|
|
522
|
+
// answered 200 and left out what was asked for - so a caller can retry or carry on with what is
|
|
494
523
|
// cached. 500 is the closest code, and the one that says retrying is worth it.
|
|
495
524
|
//
|
|
496
|
-
// Only a joinBusinessId a server could act on is checked (Java Business.isValidBusinessId's
|
|
497
|
-
// which Constants.ENTITY_ID_NULL also fails, so "no join asked" needs no case of its own).
|
|
498
|
-
// that
|
|
499
|
-
// it (see THE MOCKS), and sending one is the caller's bug, not the server's.
|
|
525
|
+
// Only a joinBusinessId a server could act on is checked (Java Business.isValidBusinessId's
|
|
526
|
+
// test, which Constants.ENTITY_ID_NULL also fails, so "no join asked" needs no case of its own).
|
|
527
|
+
// An id that is not a business entity id is not a failure here: a server ignores it rather than
|
|
528
|
+
// refusing it (see THE MOCKS), and sending one is the caller's bug, not the server's.
|
|
500
529
|
static requireJoinedBusiness(joinBusinessId, hasBusinessLink, hasBusiness) {
|
|
501
530
|
if (Ids.getEntityTypeId(joinBusinessId) !== proto_Constants.BUSINESS_ENTITY_TYPE_ID)
|
|
502
531
|
return;
|
package/dist/ProtoMap.d.ts
CHANGED
|
@@ -35,11 +35,14 @@ export declare function businessResponseToCacheWrite(response: proto_BusinessSyn
|
|
|
35
35
|
export declare function profileResponseToCacheWrite(response: proto_ProfileSyncOutResponse): CacheWrite;
|
|
36
36
|
export declare function getBusinessResponseAttachmentBaseUrlExpiry(response: proto_BusinessSyncOutResponse | null): bigint;
|
|
37
37
|
export declare function orderToBlob(order: proto_Message): StoredBlob;
|
|
38
|
-
export declare function
|
|
38
|
+
export declare function isPartyPreferencesMessage(message: proto_Message): boolean;
|
|
39
|
+
export declare function getMessageUpdatedAt(message: proto_Message): bigint;
|
|
40
|
+
export declare function partyPreferencesToBlob(partyPreferences: proto_Message): StoredBlob;
|
|
39
41
|
export declare function businessResponseFromBlobs(blobs: ReadonlyArray<StoredBlob>): proto_BusinessSyncOutResponse | null;
|
|
40
42
|
export declare function profileResponseFromBlobs(blobs: ReadonlyArray<StoredBlob>): proto_ProfileSyncOutResponse | null;
|
|
41
43
|
export declare function orderMessagesFromBlobs(blobs: ReadonlyArray<StoredBlob>): ReadonlyArray<proto_Message>;
|
|
42
|
-
export declare function
|
|
44
|
+
export declare function partyPreferencesMessageFromMessages(messages: ReadonlyArray<proto_Message>): proto_Message | null;
|
|
45
|
+
export declare function partyPreferencesMessageFromBlobs(blobs: ReadonlyArray<StoredBlob>): proto_Message | null;
|
|
43
46
|
export declare function buildBusinessSyncOutRequest(businessId: string, syncInState: SyncState, attachmentBaseUrlExpiry: bigint): proto_BusinessSyncOutRequest;
|
|
44
47
|
export declare function buildBusinessSyncOutRequestFromCache(businessId: string, cached: proto_BusinessSyncOutResponse | null): proto_BusinessSyncOutRequest;
|
|
45
48
|
export declare function buildBusinessSyncInRequest(messageUpdateNumber: bigint, messages: ReadonlyArray<proto_Message>): proto_BusinessSyncInRequest;
|
|
@@ -63,9 +66,9 @@ export declare function nextProfileSyncOutRequest(request: proto_ProfileSyncOutR
|
|
|
63
66
|
export declare function buildBusiness(response: proto_BusinessSyncOutResponse): Business | null;
|
|
64
67
|
export declare function buildProfile(profileId: string, response: proto_ProfileSyncOutResponse): Profile | null;
|
|
65
68
|
export declare function buildOrder(message: proto_Message): Order | null;
|
|
66
|
-
export declare function
|
|
67
|
-
export declare function
|
|
69
|
+
export declare function buildShoppingList(message: proto_Message): ShoppingList;
|
|
70
|
+
export declare function buildPartyPreferencesMessage(messageId: bigint, existing: proto_Message | null, senderId: bigint, shoppingList: ShoppingList | null, fulfillmentMode: proto_FulfillmentMode | null, now: bigint): proto_Message;
|
|
68
71
|
export declare function buildOrderMessage(orderId: bigint, senderId: bigint, shoppingList: ShoppingList, fulfillmentMode: proto_FulfillmentMode, deliveryAddressId: bigint, deliveryAddress: Address | null, note: string | null, creationDate: bigint): proto_Message;
|
|
69
72
|
export declare function messagesToCacheWrite(messages: ReadonlyArray<proto_Message>): CacheWrite;
|
|
70
73
|
export declare function highestCachedMessageUpdateNumber(blobs: ReadonlyArray<StoredBlob>): bigint;
|
|
71
|
-
export declare function
|
|
74
|
+
export declare function hasShoppingListChanged(stored: proto_Message | null, shoppingList: ShoppingList): boolean;
|
package/dist/ProtoMap.js
CHANGED
|
@@ -10,6 +10,7 @@ import { BusinessSyncOutResponse as proto_BusinessSyncOutResponse } from "./gene
|
|
|
10
10
|
import { Configuration as proto_Configuration } from "./generated/Configuration";
|
|
11
11
|
import { Constants as proto_Constants } from "./generated/Constants";
|
|
12
12
|
import { Entity as proto_Entity } from "./generated/Entity";
|
|
13
|
+
import { FulfillmentMode as proto_FulfillmentMode } from "./generated/FulfillmentMode";
|
|
13
14
|
import { Message as proto_Message } from "./generated/Message";
|
|
14
15
|
import { MessageType as proto_MessageType } from "./generated/MessageType";
|
|
15
16
|
import { OrderStatus as proto_OrderStatus } from "./generated/OrderStatus";
|
|
@@ -101,7 +102,8 @@ export function getProfileResponseEntities(response) {
|
|
|
101
102
|
// any entity of it.
|
|
102
103
|
export function businessResponseToCacheWrite(response) {
|
|
103
104
|
const write = toCacheWrite(getBusinessResponseEntities(response));
|
|
104
|
-
// Its eTag is its expiry, which is exactly its cursor: a client sends back the expiry it holds
|
|
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
107
|
const attachmentBaseUrl = response.attachmentBaseUrl;
|
|
106
108
|
if (attachmentBaseUrl !== undefined)
|
|
107
109
|
write.blobs.push({
|
|
@@ -115,24 +117,39 @@ export function businessResponseToCacheWrite(response) {
|
|
|
115
117
|
export function profileResponseToCacheWrite(response) {
|
|
116
118
|
return toCacheWrite(getProfileResponseEntities(response));
|
|
117
119
|
}
|
|
118
|
-
// The expiry of the attachment base url a response carried, and zero when it carried none, which
|
|
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.
|
|
119
122
|
// Here rather than at a call site, because reading a field of a response is this file's job.
|
|
120
123
|
export function getBusinessResponseAttachmentBaseUrlExpiry(response) {
|
|
121
124
|
return response?.attachmentBaseUrl?.expiry ?? 0n;
|
|
122
125
|
}
|
|
123
|
-
// One order as its own blob, for a caller holding a message rather than a whole response. The
|
|
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.
|
|
124
129
|
export function orderToBlob(order) {
|
|
125
130
|
if (order.payload.oneofKind !== "salesOrder")
|
|
126
131
|
throw new InvalidArgumentException("That message carries no sales order, so it is not an order");
|
|
127
132
|
return messageToBlob(order);
|
|
128
133
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
//
|
|
135
|
-
//
|
|
134
|
+
// Whether a message carries party preferences. Here rather than at a call site, because reading a
|
|
135
|
+
// field of a message is this file's job.
|
|
136
|
+
export function isPartyPreferencesMessage(message) {
|
|
137
|
+
return message.payload.oneofKind === "partyPreferences";
|
|
138
|
+
}
|
|
139
|
+
// When a message was last written, which is what the store versions a blob by and so the field
|
|
140
|
+
// to compare two copies of one thing on.
|
|
141
|
+
export function getMessageUpdatedAt(message) {
|
|
142
|
+
return getEntity(message.entity).updatedAt;
|
|
143
|
+
}
|
|
144
|
+
export function partyPreferencesToBlob(partyPreferences) {
|
|
145
|
+
if (partyPreferences.payload.oneofKind !== "partyPreferences")
|
|
146
|
+
throw new InvalidArgumentException("That message carries no party preferences");
|
|
147
|
+
return messageToBlob(partyPreferences);
|
|
148
|
+
}
|
|
149
|
+
// The response the blobs of a business container make up: everything cached for that business, in
|
|
150
|
+
// the shape the call that brought it returned, and null when they hold nothing of it yet.
|
|
151
|
+
// The entity type in each blob id sorts them, so no stored type tag is needed; one this library
|
|
152
|
+
// does not know is skipped rather than refused, so a newer server cannot break an older client.
|
|
136
153
|
export function businessResponseFromBlobs(blobs) {
|
|
137
154
|
const response = proto_BusinessSyncOutResponse.create({});
|
|
138
155
|
let hasContent = false;
|
|
@@ -203,7 +220,8 @@ export function profileResponseFromBlobs(blobs) {
|
|
|
203
220
|
}
|
|
204
221
|
return hasContent ? response : null;
|
|
205
222
|
}
|
|
206
|
-
// The orders a container holds. The payload a message carries is what says whether it is an
|
|
223
|
+
// The orders a container holds. The payload a message carries is what says whether it is an
|
|
224
|
+
// order or a cart - Message.messageType is not read, because here the payload is typed.
|
|
207
225
|
export function orderMessagesFromBlobs(blobs) {
|
|
208
226
|
const result = [];
|
|
209
227
|
for (const message of messagesFromBlobs(blobs))
|
|
@@ -211,11 +229,16 @@ export function orderMessagesFromBlobs(blobs) {
|
|
|
211
229
|
result.push(message);
|
|
212
230
|
return result;
|
|
213
231
|
}
|
|
214
|
-
// The
|
|
215
|
-
//
|
|
216
|
-
|
|
232
|
+
// The party preferences message among messages already in hand, or null when none of them is
|
|
233
|
+
// one. A customer has one per business, so two means two callers' messages are in one container -
|
|
234
|
+
// the most recently updated then wins for want of anything better: nothing in a message says
|
|
235
|
+
// which caller it was sent to - see THE MESSAGES OF A BUSINESS ARE NOT PUBLIC.
|
|
236
|
+
// Split out of partyPreferencesMessageFromBlobs so a caller already holding a response's messages
|
|
237
|
+
// - such as Model.businessSyncIn folding the anonymous container in - can settle its cart without
|
|
238
|
+
// asking the store for the same blobs again.
|
|
239
|
+
export function partyPreferencesMessageFromMessages(messages) {
|
|
217
240
|
let result = null;
|
|
218
|
-
for (const message of
|
|
241
|
+
for (const message of messages) {
|
|
219
242
|
if (message.payload.oneofKind !== "partyPreferences")
|
|
220
243
|
continue;
|
|
221
244
|
if (result === null ||
|
|
@@ -224,6 +247,11 @@ export function cartMessageFromBlobs(blobs) {
|
|
|
224
247
|
}
|
|
225
248
|
return result;
|
|
226
249
|
}
|
|
250
|
+
// The party preferences message a container holds, or null when the customer has not put
|
|
251
|
+
// anything in a cart yet.
|
|
252
|
+
export function partyPreferencesMessageFromBlobs(blobs) {
|
|
253
|
+
return partyPreferencesMessageFromMessages(messagesFromBlobs(blobs));
|
|
254
|
+
}
|
|
227
255
|
// #endregion
|
|
228
256
|
// #region Requests and credentials
|
|
229
257
|
// The request that asks for everything changed since these cursors.
|
|
@@ -363,8 +391,12 @@ export function authTokensFromProto(authTokens) {
|
|
|
363
391
|
export function buildAuthTokensBytes(authTokens) {
|
|
364
392
|
return proto_AuthTokens.toBinary(authTokens);
|
|
365
393
|
}
|
|
366
|
-
// How a container keeps the credentials it earned: the two tokens this server issued, with a
|
|
367
|
-
//
|
|
394
|
+
// How a container keeps the credentials it earned: the two tokens this server issued, with a
|
|
395
|
+
// null eTag because a reissued token is not a later version of an earlier one and greater-wins
|
|
396
|
+
// would refuse it.
|
|
397
|
+
// The authentication token is left out however it was passed in: it belongs to the identity
|
|
398
|
+
// provider, so a stored copy could only be staler than the one held in memory and would outlive
|
|
399
|
+
// the sign out that should have ended it.
|
|
368
400
|
export function authTokensToBlob(authTokens) {
|
|
369
401
|
const stored = proto_AuthTokens.create({
|
|
370
402
|
authenticationMethod: authTokens.authenticationMethod,
|
|
@@ -507,8 +539,10 @@ function profileSyncOutCursors(request) {
|
|
|
507
539
|
}
|
|
508
540
|
// #endregion
|
|
509
541
|
// #region A response as an object graph
|
|
510
|
-
// The whole of one business as an object graph, regrouping the flat wire entities by owner id so
|
|
511
|
-
//
|
|
542
|
+
// The whole of one business as an object graph, regrouping the flat wire entities by owner id so
|
|
543
|
+
// each class can be constructed with its children.
|
|
544
|
+
// An entity whose parent is missing is dropped, since a paged read can hold a child whose parent
|
|
545
|
+
// has yet to arrive; null when the response carries no Business at all.
|
|
512
546
|
export function buildBusiness(response) {
|
|
513
547
|
const businessProto = response.business;
|
|
514
548
|
if (businessProto === undefined || isDeletedEntity(getEntity(businessProto.entity)))
|
|
@@ -618,10 +652,12 @@ export function buildBusiness(response) {
|
|
|
618
652
|
break;
|
|
619
653
|
}
|
|
620
654
|
updatableBusiness.catalog = new Catalog(productCategories, products, priceList);
|
|
621
|
-
// The orders of this customer, and their
|
|
655
|
+
// The orders of this customer, and their party preferences: the cart, and the fulfillment mode
|
|
656
|
+
// to default their next order to. A message of any other payload type is dropped.
|
|
622
657
|
const orders = [];
|
|
623
658
|
let cart = null;
|
|
624
659
|
let cartUpdateNumber = 0n;
|
|
660
|
+
let defaultFulfillmentMode = proto_FulfillmentMode.DELIVERY;
|
|
625
661
|
for (const message of response.messages) {
|
|
626
662
|
if (isDeletedEntity(getEntity(message.entity)))
|
|
627
663
|
continue;
|
|
@@ -631,8 +667,8 @@ export function buildBusiness(response) {
|
|
|
631
667
|
break;
|
|
632
668
|
}
|
|
633
669
|
case "partyPreferences": {
|
|
634
|
-
// One customer has one
|
|
635
|
-
//
|
|
670
|
+
// One customer has one party preferences message, so two means two callers' messages are
|
|
671
|
+
// in one container - see partyPreferencesMessageFromBlobs, which breaks the tie the same way
|
|
636
672
|
const updateNumber = getEntity(message.entity).updateNumber;
|
|
637
673
|
if (cart !== null && updateNumber <= cartUpdateNumber)
|
|
638
674
|
break;
|
|
@@ -641,6 +677,9 @@ export function buildBusiness(response) {
|
|
|
641
677
|
// never having had a cart at all
|
|
642
678
|
cart = cartShoppingListFromProto(message.payload.partyPreferences);
|
|
643
679
|
cartUpdateNumber = updateNumber;
|
|
680
|
+
// Whatever this same message carries, DELIVERY only ever coming from the fallback above -
|
|
681
|
+
// not from a message that happens to hold UNSPECIFIED_FULFILLMENT_MODE itself
|
|
682
|
+
defaultFulfillmentMode = message.payload.partyPreferences.fulfillmentMode;
|
|
644
683
|
break;
|
|
645
684
|
}
|
|
646
685
|
// No default: MONEY_TRANSFER has no payload in this protocol, and OTHER is not ours
|
|
@@ -650,6 +689,7 @@ export function buildBusiness(response) {
|
|
|
650
689
|
UpdatableEntity.setParent(order, business);
|
|
651
690
|
updatableBusiness.orders = Object.freeze(orders);
|
|
652
691
|
updatableBusiness.cart = cart;
|
|
692
|
+
updatableBusiness.defaultFulfillmentMode = defaultFulfillmentMode;
|
|
653
693
|
return business;
|
|
654
694
|
}
|
|
655
695
|
// One profile as an object graph: the caller, their links, and the businesses those links name.
|
|
@@ -681,13 +721,14 @@ export function buildOrder(message) {
|
|
|
681
721
|
return null;
|
|
682
722
|
return orderFromProto(message, message.payload.salesOrder);
|
|
683
723
|
}
|
|
684
|
-
// The
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
|
|
724
|
+
// The shopping list a cart message carries, empty rather than absent when the cart has been
|
|
725
|
+
// emptied. Unlike Business.cart (see the party preferences case of buildBusiness, which answers
|
|
726
|
+
// null so "emptied" and "never had one" look the same to a customer), a caller here - createOrder,
|
|
727
|
+
// the visitor cart carried over on sign in - works with an actual list either way.
|
|
728
|
+
export function buildShoppingList(message) {
|
|
688
729
|
if (message.payload.oneofKind !== "partyPreferences")
|
|
689
|
-
|
|
690
|
-
return cartShoppingListFromProto(message.payload.partyPreferences);
|
|
730
|
+
throw new InvalidArgumentException("That message carries no party preferences, so it is not a cart");
|
|
731
|
+
return cartShoppingListFromProto(message.payload.partyPreferences) ?? ShoppingList.EMPTY;
|
|
691
732
|
}
|
|
692
733
|
// #endregion
|
|
693
734
|
// #region What a customer pushes
|
|
@@ -699,16 +740,20 @@ export function buildCart(message) {
|
|
|
699
740
|
// message the server named could never be found here again. The second is that updatedAt is the
|
|
700
741
|
// client's too and the server keeps it unchanged - so the copy stored here carries the same eTag as
|
|
701
742
|
// the copy a later read would bring back, and neither can look stale to the other.
|
|
702
|
-
// The
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
//
|
|
743
|
+
// The party preferences of this customer as a message to push, carrying the cart and (optionally)
|
|
744
|
+
// the fulfillment mode both. An existing message is amended rather than rebuilt, because its
|
|
745
|
+
// payload can carry fields neither param here speaks to, and losing them would be a storefront
|
|
746
|
+
// overwriting data it cannot even read.
|
|
706
747
|
// Its id is used in preference to the derived one for the same reason: that is where the server
|
|
707
|
-
// already holds this
|
|
748
|
+
// already holds this customer's party preferences, and it is only worth deriving an id when there
|
|
749
|
+
// is none to amend.
|
|
708
750
|
// A list holding nothing is pushed as no list at all, the way Java serializeShoppingList answers
|
|
709
751
|
// null below one item - so emptying a cart and never having had one are one state on the wire, and
|
|
710
752
|
// placing an order is what puts a cart into it.
|
|
711
|
-
|
|
753
|
+
// fulfillmentMode is null to leave whatever is already there alone - the ordinary case, since a
|
|
754
|
+
// storefront otherwise treats it as the merchant app's own data - and a value only where createOrder
|
|
755
|
+
// means to set it, to the mode of the order that just emptied this cart.
|
|
756
|
+
export function buildPartyPreferencesMessage(messageId, existing, senderId, shoppingList, fulfillmentMode, now) {
|
|
712
757
|
const existingEntity = existing === null ? null : getEntity(existing.entity);
|
|
713
758
|
const preferences = existing !== null && existing.payload.oneofKind === "partyPreferences"
|
|
714
759
|
? existing.payload.partyPreferences
|
|
@@ -720,18 +765,18 @@ export function buildCartMessage(cartMessageId, existing, senderId, shoppingList
|
|
|
720
765
|
// the two disagreeing about the cart the customer is looking at. One millisecond past the copy
|
|
721
766
|
// being replaced is enough, and the server keeps whatever is sent, so both sides still agree.
|
|
722
767
|
const updatedAt = existingEntity === null || existingEntity.updatedAt < now ? now : existingEntity.updatedAt + 1n;
|
|
768
|
+
const partyPreferences = proto_PartyPreferencesPayload.create({
|
|
769
|
+
...preferences,
|
|
770
|
+
shoppingList: pushedShoppingList(shoppingList, false),
|
|
771
|
+
});
|
|
772
|
+
if (fulfillmentMode !== null)
|
|
773
|
+
partyPreferences.fulfillmentMode = fulfillmentMode;
|
|
723
774
|
return proto_Message.create({
|
|
724
|
-
entity: pushedEntity(existingEntity?.id ??
|
|
775
|
+
entity: pushedEntity(existingEntity?.id ?? messageId, existingEntity?.createdAt ?? updatedAt, updatedAt),
|
|
725
776
|
senderId: senderId,
|
|
726
777
|
messageType: proto_MessageType.PARTY_PREFERENCES,
|
|
727
778
|
status: proto_TaskStatus.NOT_YET_PROCESSED,
|
|
728
|
-
payload: {
|
|
729
|
-
oneofKind: "partyPreferences",
|
|
730
|
-
partyPreferences: proto_PartyPreferencesPayload.create({
|
|
731
|
-
...preferences,
|
|
732
|
-
shoppingList: pushedShoppingList(shoppingList, false),
|
|
733
|
-
}),
|
|
734
|
-
},
|
|
779
|
+
payload: { oneofKind: "partyPreferences", partyPreferences: partyPreferences },
|
|
735
780
|
});
|
|
736
781
|
}
|
|
737
782
|
// One order as a message to push. Its shopping list goes out final whatever was passed in, which is
|
|
@@ -792,24 +837,20 @@ export function highestCachedMessageUpdateNumber(blobs) {
|
|
|
792
837
|
function pushedEntity(id, createdAt, updatedAt) {
|
|
793
838
|
return proto_Entity.create({ id: id, createdAt: createdAt, updatedAt: updatedAt });
|
|
794
839
|
}
|
|
795
|
-
// Whether
|
|
796
|
-
//
|
|
797
|
-
//
|
|
798
|
-
//
|
|
799
|
-
//
|
|
800
|
-
//
|
|
801
|
-
//
|
|
802
|
-
//
|
|
803
|
-
|
|
804
|
-
// out; a server that normalised the list would make every call differ and push every time, which
|
|
805
|
-
// is what this library did before and so is no worse.
|
|
806
|
-
export function isCartWorthPushing(stored, cartMessage) {
|
|
807
|
-
// Nothing stored and nothing to send: an emptied cart and one never created are one state
|
|
840
|
+
// Whether shoppingList differs from what stored already holds, so updateCart can check before
|
|
841
|
+
// building or pushing a cart message rather than after - a caller may call it on every event it
|
|
842
|
+
// cares about and only a cart the server has not got costs a request.
|
|
843
|
+
// Compares raw lists rather than built messages, because a message's updatedAt always differs from
|
|
844
|
+
// call to call; sound because the server echoes a pushed list back unchanged, so what is stored is
|
|
845
|
+
// exactly what was last sent.
|
|
846
|
+
// Also true whenever stored has never been pushed (updateNumber 0, left by updateCartLocalOnly),
|
|
847
|
+
// regardless of content, so an unsent local edit still goes out.
|
|
848
|
+
export function hasShoppingListChanged(stored, shoppingList) {
|
|
808
849
|
if (stored === null)
|
|
809
|
-
return
|
|
850
|
+
return pushedShoppingList(shoppingList, false) !== undefined;
|
|
810
851
|
if (getEntity(stored.entity).updateNumber === 0n)
|
|
811
852
|
return true;
|
|
812
|
-
return !proto_ShoppingList.equals(getCartShoppingList(stored),
|
|
853
|
+
return !proto_ShoppingList.equals(getCartShoppingList(stored), pushedShoppingList(shoppingList, false));
|
|
813
854
|
}
|
|
814
855
|
// The list inside a cart message, or absent for one carrying no list - which is an emptied cart
|
|
815
856
|
function getCartShoppingList(message) {
|
|
@@ -817,9 +858,9 @@ function getCartShoppingList(message) {
|
|
|
817
858
|
? message.payload.partyPreferences.shoppingList
|
|
818
859
|
: undefined;
|
|
819
860
|
}
|
|
820
|
-
// A shopping list on its way out, or absent when it holds nothing - see
|
|
821
|
-
// is judged by the items being sent and not by the counts
|
|
822
|
-
// counts below are.
|
|
861
|
+
// A shopping list on its way out, or absent when it holds nothing - see
|
|
862
|
+
// buildPartyPreferencesMessage. Emptiness is judged by the items being sent and not by the counts
|
|
863
|
+
// the list carries, for the reason the counts below are.
|
|
823
864
|
function pushedShoppingList(shoppingList, isFinal) {
|
|
824
865
|
if (shoppingList === null)
|
|
825
866
|
return undefined;
|
|
@@ -1034,7 +1075,8 @@ function workingHoursFromProto(workingHours) {
|
|
|
1034
1075
|
function workingDaysFromProto(workingDays) {
|
|
1035
1076
|
if (workingDays === undefined)
|
|
1036
1077
|
return WorkingDays.ALL_DAYS;
|
|
1037
|
-
|
|
1078
|
+
// Not an optional field, so an unspecified zone arrives as an empty string rather than absent
|
|
1079
|
+
return new WorkingDays(Object.freeze([...workingDays.days]), workingDays.timeZone.length === 0 ? null : workingDays.timeZone, Object.freeze(workingDays.workingHours.map(workingHoursFromProto)));
|
|
1038
1080
|
}
|
|
1039
1081
|
function shoppingListItemFromProto(item) {
|
|
1040
1082
|
return ShoppingListItem.fromScaledValues(item.packageId, item.quantity, item.unitPrice, item.shoppingListOrder, item.isOriginal, item.isModified, item.isStockKeepingRequired, item.priceDiscount, item.discount, Object.freeze(item.taxes.map(taxFromProto)), item.expiry, stringFromProto(item.description));
|
package/dist/SyncCache.d.ts
CHANGED
|
@@ -15,9 +15,9 @@ export default class SyncCache {
|
|
|
15
15
|
readOrders(containerName: string): Promise<ReadonlyArray<proto_Message>>;
|
|
16
16
|
readMessageUpdateNumber(containerName: string): Promise<bigint>;
|
|
17
17
|
writeOrder(containerName: string, order: proto_Message): Promise<number>;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
readPartyPreferences(containerName: string): Promise<proto_Message | null>;
|
|
19
|
+
writePartyPreferences(containerName: string, partyPreferences: proto_Message): Promise<number>;
|
|
20
|
+
deletePartyPreferences(containerName: string): Promise<number>;
|
|
21
21
|
writeMessages(containerName: string, messages: ReadonlyArray<proto_Message>): Promise<number>;
|
|
22
22
|
private readAll;
|
|
23
23
|
private writeBlobs;
|
package/dist/SyncCache.js
CHANGED
|
@@ -53,8 +53,8 @@ export default class SyncCache {
|
|
|
53
53
|
return ProtoMap.orderMessagesFromBlobs(await this.readAll(containerName));
|
|
54
54
|
}
|
|
55
55
|
// The cursor a push sends: the highest update number among this customer's messages already
|
|
56
|
-
// cached here,
|
|
57
|
-
// the store fresh each time rather than kept anywhere.
|
|
56
|
+
// cached here, party preferences and orders alike. Not persisted, like every cursor in this
|
|
57
|
+
// library - read off the store fresh each time rather than kept anywhere.
|
|
58
58
|
async readMessageUpdateNumber(containerName) {
|
|
59
59
|
return ProtoMap.highestCachedMessageUpdateNumber(await this.readAll(containerName));
|
|
60
60
|
}
|
|
@@ -62,36 +62,38 @@ export default class SyncCache {
|
|
|
62
62
|
async writeOrder(containerName, order) {
|
|
63
63
|
return this.writeBlobs(containerName, [ProtoMap.orderToBlob(order)]);
|
|
64
64
|
}
|
|
65
|
-
// The
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
65
|
+
// The party preferences message in this container, or null when there is none. Two mean two
|
|
66
|
+
// callers, which is the same open question as the orders above.
|
|
67
|
+
async readPartyPreferences(containerName) {
|
|
68
|
+
return ProtoMap.partyPreferencesMessageFromBlobs(await this.readAll(containerName));
|
|
69
|
+
}
|
|
70
|
+
async writePartyPreferences(containerName, partyPreferences) {
|
|
71
|
+
return this.writeBlobs(containerName, [ProtoMap.partyPreferencesToBlob(partyPreferences)]);
|
|
72
|
+
}
|
|
73
|
+
// Drops the party preferences message this container holds, and answers zero when it holds none.
|
|
74
|
+
// The one delete here that no tombstone asked for, and the only reason it exists: one written
|
|
75
|
+
// before anybody signed in is handed over to the container named for the membership, and the
|
|
76
|
+
// copy left behind would otherwise be read by the next anonymous visitor and by the next customer
|
|
77
|
+
// to sign in - see Model.businessSyncIn. Nothing else about a container is a caller's to remove:
|
|
78
|
+
// everything in one came off the wire, and what the server has not said is gone stays.
|
|
79
|
+
async deletePartyPreferences(containerName) {
|
|
80
|
+
const partyPreferences = await this.readPartyPreferences(containerName);
|
|
81
|
+
if (partyPreferences === null)
|
|
81
82
|
return 0;
|
|
82
83
|
return this.write(containerName, {
|
|
83
84
|
blobs: NO_BLOBS,
|
|
84
|
-
deletedIds: [ProtoMap.
|
|
85
|
+
deletedIds: [ProtoMap.partyPreferencesToBlob(partyPreferences).id],
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
|
-
// The messages a sync in response carried, in a single batch - a pushed order and the
|
|
88
|
-
// emptied belong in one write, so that this cache cannot come to hold the
|
|
89
|
-
// emptying. Tombstones included, the same as any other entity array read off
|
|
90
|
-
// what a push's own messages are stored as once the server has echoed them back
|
|
91
|
-
// numbers, and it is also how anything else of this customer's that the response
|
|
92
|
-
// here. Neither is checked against a payload type, unlike the two writers above -
|
|
93
|
-
// caller handing in a message it was given, where storing an order as
|
|
94
|
-
// worth catching, and this is for messages the response itself is
|
|
88
|
+
// The messages a sync in response carried, in a single batch - a pushed order and the party
|
|
89
|
+
// preferences message it emptied belong in one write, so that this cache cannot come to hold the
|
|
90
|
+
// order without the emptying. Tombstones included, the same as any other entity array read off
|
|
91
|
+
// the wire: this is what a push's own messages are stored as once the server has echoed them back
|
|
92
|
+
// with real update numbers, and it is also how anything else of this customer's that the response
|
|
93
|
+
// carried lands here. Neither is checked against a payload type, unlike the two writers above -
|
|
94
|
+
// those are for a caller handing in a message it was given, where storing an order as party
|
|
95
|
+
// preferences would be a mistake worth catching, and this is for messages the response itself is
|
|
96
|
+
// the authority on.
|
|
95
97
|
async writeMessages(containerName, messages) {
|
|
96
98
|
return this.write(containerName, ProtoMap.messagesToCacheWrite(messages));
|
|
97
99
|
}
|
package/dist/WorkingDays.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
export default class WorkingDays {
|
|
2
2
|
private readonly _days;
|
|
3
|
+
private readonly _timeZone;
|
|
3
4
|
private readonly _workingHours;
|
|
4
|
-
constructor(_days: ReadonlyArray<boolean>, _workingHours: ReadonlyArray<WorkingHours>);
|
|
5
|
+
constructor(_days: ReadonlyArray<boolean>, _timeZone: string | null, _workingHours: ReadonlyArray<WorkingHours>);
|
|
5
6
|
get days(): ReadonlyArray<boolean>;
|
|
7
|
+
get timeZone(): string | null;
|
|
6
8
|
get workingHours(): ReadonlyArray<WorkingHours>;
|
|
7
9
|
get includesAllDaysOfWeek(): boolean;
|
|
8
10
|
isWorkingDay(dayOfWeek: number): boolean;
|
|
9
11
|
getWorkingHours(dayOfWeek: number): WorkingHours | null;
|
|
10
12
|
isOpenAllDay(dayOfWeek: number): boolean;
|
|
11
|
-
isOpenAt(dayOfWeek: number, minuteOfDay: number): boolean;
|
|
12
13
|
private static checkDayOfWeek;
|
|
13
14
|
static readonly ALL_DAYS: WorkingDays;
|
|
14
15
|
}
|
package/dist/WorkingDays.js
CHANGED
|
@@ -2,11 +2,12 @@ import { Constants as proto_Constants } from "./generated/Constants";
|
|
|
2
2
|
import { InvalidArgumentException } from "./Exceptions";
|
|
3
3
|
// Days and hours a business works. Absence is the permissive value throughout: no WorkingDays
|
|
4
4
|
// means every day (ALL_DAYS); a day with no hours/ranges means worked all day.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
5
|
+
// The hours are minutes after local midnight in timeZone, so a caller asking about a moment reads
|
|
6
|
+
// it in that zone first, as a day and a minute.
|
|
7
7
|
class WorkingDays {
|
|
8
|
-
constructor(_days, _workingHours) {
|
|
8
|
+
constructor(_days, _timeZone, _workingHours) {
|
|
9
9
|
this._days = _days;
|
|
10
|
+
this._timeZone = _timeZone;
|
|
10
11
|
this._workingHours = _workingHours;
|
|
11
12
|
}
|
|
12
13
|
// Index 0 is Sunday and index 6 is Saturday, the same numbering as Date.getDay. Shorter than
|
|
@@ -14,6 +15,13 @@ class WorkingDays {
|
|
|
14
15
|
get days() {
|
|
15
16
|
return this._days;
|
|
16
17
|
}
|
|
18
|
+
// The IANA ID of the zone the hours are in, for example "Asia/Kolkata" - never an abbreviation
|
|
19
|
+
// like "IST". Null when none was specified: settings saved before the protocol carried one, and
|
|
20
|
+
// ALL_DAYS. Then the hours cannot be placed on a clock, and only a caller that knows the
|
|
21
|
+
// business's zone can read a moment into them.
|
|
22
|
+
get timeZone() {
|
|
23
|
+
return this._timeZone;
|
|
24
|
+
}
|
|
17
25
|
// Indexed exactly as days above. Shorter than days, or empty, is normal: see the comment on
|
|
18
26
|
// this class for what a missing entry means.
|
|
19
27
|
get workingHours() {
|
|
@@ -41,20 +49,14 @@ class WorkingDays {
|
|
|
41
49
|
isOpenAllDay(dayOfWeek) {
|
|
42
50
|
return this.isWorkingDay(dayOfWeek) && this.getWorkingHours(dayOfWeek) === null;
|
|
43
51
|
}
|
|
44
|
-
// minuteOfDay is minutes after local midnight, from 0 to Constants.MINUTES_PER_DAY - 1
|
|
45
|
-
isOpenAt(dayOfWeek, minuteOfDay) {
|
|
46
|
-
if (!this.isWorkingDay(dayOfWeek))
|
|
47
|
-
return false;
|
|
48
|
-
const workingHours = this.getWorkingHours(dayOfWeek);
|
|
49
|
-
return workingHours === null || workingHours.contains(minuteOfDay);
|
|
50
|
-
}
|
|
51
52
|
static checkDayOfWeek(dayOfWeek) {
|
|
52
53
|
if (!Number.isInteger(dayOfWeek) || dayOfWeek < 0 || dayOfWeek >= proto_Constants.DAYS_IN_WEEK)
|
|
53
54
|
throw new InvalidArgumentException(`Not a day of the week: ${dayOfWeek}`);
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
|
-
// What an absent WorkingDays means: the business works every day, all day long
|
|
57
|
-
|
|
57
|
+
// What an absent WorkingDays means: the business works every day, all day long. With no hours
|
|
58
|
+
// there is nothing for a zone to place, so it has none.
|
|
59
|
+
WorkingDays.ALL_DAYS = new WorkingDays(Object.freeze([true, true, true, true, true, true, true]), null, Object.freeze([]));
|
|
58
60
|
export default WorkingDays;
|
|
59
61
|
// The hours of a single day that are worked, as the stretches of it that are.
|
|
60
62
|
export class WorkingHours {
|
|
@@ -73,6 +75,8 @@ export class WorkingHours {
|
|
|
73
75
|
get isAllDay() {
|
|
74
76
|
return (this._ranges.length === 0 || (this._ranges.length === 1 && this._ranges[0].isAllDay));
|
|
75
77
|
}
|
|
78
|
+
// minuteOfDay is minutes after local midnight in the zone of the WorkingDays these hours belong
|
|
79
|
+
// to, from 0 to Constants.MINUTES_PER_DAY - 1
|
|
76
80
|
contains(minuteOfDay) {
|
|
77
81
|
if (this.isAllDay)
|
|
78
82
|
return true;
|
|
@@ -20,12 +20,20 @@ export interface WorkingDays {
|
|
|
20
20
|
* @generated from protobuf field: repeated bool days = 1
|
|
21
21
|
*/
|
|
22
22
|
days: boolean[];
|
|
23
|
+
/**
|
|
24
|
+
* The IANA ID of the time zone the working hours are specified in, for example "Asia/Kolkata";
|
|
25
|
+
* never an abbreviation like "IST", which is ambiguous. At most 64 characters. Empty means it
|
|
26
|
+
* was never specified, as in settings saved before this field existed.
|
|
27
|
+
*
|
|
28
|
+
* @generated from protobuf field: string timeZone = 2
|
|
29
|
+
*/
|
|
30
|
+
timeZone: string;
|
|
23
31
|
/**
|
|
24
32
|
* Working hours of each day, indexed as days above (entry 0 = Sunday); up to
|
|
25
33
|
* Constants.DAYS_IN_WEEK entries, fewer is normal. A missing entry, or one with no ranges, means
|
|
26
34
|
* work all day; only meaningful on a day whose days entry is true.
|
|
27
35
|
*
|
|
28
|
-
* @generated from protobuf field: repeated com.merabills.storefront.WorkingHours workingHours =
|
|
36
|
+
* @generated from protobuf field: repeated com.merabills.storefront.WorkingHours workingHours = 3
|
|
29
37
|
*/
|
|
30
38
|
workingHours: WorkingHours[];
|
|
31
39
|
}
|
|
@@ -8,12 +8,14 @@ class WorkingDays$Type extends MessageType {
|
|
|
8
8
|
constructor() {
|
|
9
9
|
super("com.merabills.storefront.WorkingDays", [
|
|
10
10
|
{ no: 1, name: "days", kind: "scalar", repeat: 1 /*RepeatType.PACKED*/, T: 8 /*ScalarType.BOOL*/ },
|
|
11
|
-
{ no: 2, name: "
|
|
11
|
+
{ no: 2, name: "timeZone", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
|
12
|
+
{ no: 3, name: "workingHours", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => WorkingHours }
|
|
12
13
|
]);
|
|
13
14
|
}
|
|
14
15
|
create(value) {
|
|
15
16
|
const message = globalThis.Object.create((this.messagePrototype));
|
|
16
17
|
message.days = [];
|
|
18
|
+
message.timeZone = "";
|
|
17
19
|
message.workingHours = [];
|
|
18
20
|
if (value !== undefined)
|
|
19
21
|
reflectionMergePartial(this, message, value);
|
|
@@ -31,7 +33,10 @@ class WorkingDays$Type extends MessageType {
|
|
|
31
33
|
else
|
|
32
34
|
message.days.push(reader.bool());
|
|
33
35
|
break;
|
|
34
|
-
case /*
|
|
36
|
+
case /* string timeZone */ 2:
|
|
37
|
+
message.timeZone = reader.string();
|
|
38
|
+
break;
|
|
39
|
+
case /* repeated com.merabills.storefront.WorkingHours workingHours */ 3:
|
|
35
40
|
message.workingHours.push(WorkingHours.internalBinaryRead(reader, reader.uint32(), options));
|
|
36
41
|
break;
|
|
37
42
|
default:
|
|
@@ -53,9 +58,12 @@ class WorkingDays$Type extends MessageType {
|
|
|
53
58
|
writer.bool(message.days[i]);
|
|
54
59
|
writer.join();
|
|
55
60
|
}
|
|
56
|
-
/*
|
|
61
|
+
/* string timeZone = 2; */
|
|
62
|
+
if (message.timeZone !== "")
|
|
63
|
+
writer.tag(2, WireType.LengthDelimited).string(message.timeZone);
|
|
64
|
+
/* repeated com.merabills.storefront.WorkingHours workingHours = 3; */
|
|
57
65
|
for (let i = 0; i < message.workingHours.length; i++)
|
|
58
|
-
WorkingHours.internalBinaryWrite(message.workingHours[i], writer.tag(
|
|
66
|
+
WorkingHours.internalBinaryWrite(message.workingHours[i], writer.tag(3, WireType.LengthDelimited).fork(), options).join();
|
|
59
67
|
let u = options.writeUnknownFields;
|
|
60
68
|
if (u !== false)
|
|
61
69
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peabody-soft/mbs-model-ts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "MeraBills storefront client-side model (TypeScript)",
|
|
5
5
|
"author": "Peabody Soft",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"typescript": "^7.0.2"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@peabody-soft/mbs-protos": "^1.
|
|
52
|
+
"@peabody-soft/mbs-protos": "^1.10.0",
|
|
53
53
|
"@protobuf-ts/runtime": "^2.11.1"
|
|
54
54
|
}
|
|
55
55
|
}
|