@peabody-soft/mbs-model-ts 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/Address.js +8 -11
  2. package/dist/Attachment.js +7 -8
  3. package/dist/AttachmentBaseUrl.js +10 -10
  4. package/dist/AuthTokens.js +12 -7
  5. package/dist/Business.js +19 -19
  6. package/dist/BusinessIdentifier.js +2 -2
  7. package/dist/BusinessLink.js +16 -16
  8. package/dist/BusinessProfile.js +4 -4
  9. package/dist/CartEditor.js +137 -166
  10. package/dist/Catalog.js +19 -25
  11. package/dist/Constants.js +10 -10
  12. package/dist/ContainerNames.js +10 -11
  13. package/dist/Entity.js +11 -9
  14. package/dist/Enums.js +2 -2
  15. package/dist/Exceptions.js +2 -2
  16. package/dist/GeneralSettings.js +23 -21
  17. package/dist/HttpStatusCodes.js +10 -11
  18. package/dist/Ids.js +24 -30
  19. package/dist/Item.js +6 -7
  20. package/dist/LatLong.js +3 -3
  21. package/dist/LiveData.js +6 -6
  22. package/dist/Measures.js +2 -2
  23. package/dist/Message.js +6 -6
  24. package/dist/MockDatabase.js +2 -2
  25. package/dist/Model.js +294 -365
  26. package/dist/Order.js +20 -17
  27. package/dist/Package.js +7 -8
  28. package/dist/PackageIdentifier.js +2 -2
  29. package/dist/PaymentMethodDetail.js +4 -4
  30. package/dist/Photo.js +6 -7
  31. package/dist/Price.js +8 -9
  32. package/dist/PriceList.js +2 -2
  33. package/dist/Product.js +17 -22
  34. package/dist/ProductCategory.js +3 -4
  35. package/dist/Profile.js +26 -27
  36. package/dist/ProtoMap.d.ts +3 -0
  37. package/dist/ProtoMap.js +288 -294
  38. package/dist/Routes.js +3 -3
  39. package/dist/ServiceExceptions.js +11 -12
  40. package/dist/ServiceProxy.js +19 -23
  41. package/dist/ShoppingList.js +112 -130
  42. package/dist/SyncCache.d.ts +3 -0
  43. package/dist/SyncCache.js +92 -75
  44. package/dist/SyncKeys.js +8 -8
  45. package/dist/SyncState.js +5 -5
  46. package/dist/Tax.js +16 -21
  47. package/dist/TaxHelper.js +22 -30
  48. package/dist/TaxType.js +8 -9
  49. package/dist/UserProfile.js +4 -4
  50. package/dist/WorkingDays.js +15 -15
  51. package/dist/index.js +9 -9
  52. package/package.json +1 -1
package/dist/SyncCache.js CHANGED
@@ -2,53 +2,63 @@ import { Constants as proto_Constants } from "./generated/Constants";
2
2
  import Ids from "./Ids";
3
3
  import * as ProtoMap from "./ProtoMap";
4
4
  import * as SyncKeys from "./SyncKeys";
5
- // What the server has already said, so that the next call need only ask for what has changed since. A read cache and nothing else: everything in it came out of a response, so giving a container up costs one refetch.
6
- // Not write through, not an identity (it stamps nothing) and not a sync - a caller calls the Service and hands the response in, which is why the paging, and which writes are reads, are the caller's. It holds the wire and only the wire, and ProtoMap reads every field. See SYNC LAYER in ../Prompt.txt.
7
- // One response is one write: every entity it brought, and every tombstone, is stored in a single
8
- // batch. A tombstone is kept, as a blob that nothing older replaces, and there is nothing to work out
9
- // beyond that, because the server sends a tombstone for every entity it deleted, children included - so
10
- // this cache never infers a deletion from another one.
11
- // How far a container has been read is kept too, as a blob of its own for each entity type and not
12
- // worked out from the entities: a read that came back with tombstones, or changes made by this
13
- // client, would otherwise move it. It moves only with a page of a read, in the batch of the page that
14
- // moved it, and only upwards: each blob is versioned by its cursor, so the store keeps the greater.
5
+ // What the server has already said, so the next call asks only for what changed. A read cache:
6
+ // everything in it came from a response, so giving up a container costs one refetch.
7
+ // It does not write through or sync. The caller calls the Service, does the paging and says which
8
+ // writes are reads. It holds only the wire, which ProtoMap reads. See SYNC LAYER in ../Prompt.txt.
9
+ // - One response is one write. A tombstone is a blob nothing older replaces. The server sends one
10
+ // for every deleted entity, children included, so the cache never infers a deletion.
11
+ // - Tombstones are stored as sent, so a deleted message keeps its order (a completed order is a
12
+ // deleted message). readDeletedBusiness, readDeletedProfile and readDeletedOrders answer them.
13
+ // readOrders and readPartyPreferences leave them out.
14
+ // - A cursor is a blob per entity type (its eTag is the cursor), not derived from the entities,
15
+ // since tombstones and this client's own changes would move it. It rises only with a page of a
16
+ // read, in that page's batch, and the store keeps the greater.
15
17
  export default class SyncCache {
16
18
  constructor(_database) {
17
19
  this._database = _database;
18
20
  this._containers = new Map();
19
21
  }
20
- // Everything cached for one business, as the response that carried it, or null before anything has been written for it.
21
- // Tombstones are among it, as the entities they are of, and whatever builds on this has to leave them out.
22
+ // Everything cached for one business, as the response that carried it, or null before anything is
23
+ // written. Tombstones are included as sent: a deleted entity carries what it was, or only its header
24
+ // for an item. Whatever builds on this must leave tombstones out, except for the order of a message.
22
25
  async readBusiness(containerName) {
23
26
  return ProtoMap.businessResponseFromBlobs(await this.readAll(containerName));
24
27
  }
25
- // The cursors the next read of this business sends, which is how far the reads of it have got: the
26
- // highest update number of every entity type that a page of one brought, tombstones included, and zero for a
27
- // type that none has. Nothing else moves them, so, what this client pushes is read back by the next read.
28
+ // The cursors the next read sends: the highest update number of each entity type that a page of a
29
+ // read brought, tombstones included, or zero for a type none has brought. Only a read moves them, so
30
+ // the next read brings back what this client pushed.
28
31
  async readBusinessCursors(containerName) {
29
32
  return this.readCursors(containerName, SyncKeys.BUSINESS_SYNC_IN);
30
33
  }
31
- // Takes in one page of a business read, one blob per entity versioned by ProtoMap.entityETag, so the later of two copies wins and a page that arrives twice changes nothing the second time.
32
- // A tombstone is stored the same way, and wins over the copies of what it is of, a delete being a later write than any of them. The cursors are raised
33
- // by the page, tombstones included, in the same batch: they cannot get ahead of the entities they
34
- // describe, and an interrupted read repeats itself from where its last page got to.
35
- // Answers how much of the page it kept - the blobs stored, which is what the container now differs
36
- // by. That is how a caller tells a response that changed something from one that changed nothing,
37
- // and what it publishes on: the store keeps a blob only when its eTag is later, so a response can be
38
- // rejected in full, and one that was cannot change what a later read of this container answers - see MODEL.
34
+ // Stores one page of a business read, a blob per entity versioned by ProtoMap.entityETag, so the later
35
+ // copy wins and a repeated page changes nothing. A tombstone wins over the copies of what it deleted,
36
+ // since a delete is a later write. The cursors the page raises go in the same batch, so they cannot
37
+ // get ahead of their entities and an interrupted read resumes from its last page.
38
+ // Answers how many blobs it stored, which tells a caller whether the response changed anything and
39
+ // what to publish on. The store keeps a blob only when its eTag is later, so a response can be
40
+ // rejected in full, and a rejected one cannot change what a later read answers - see MODEL.
39
41
  async writeBusiness(containerName, response) {
40
42
  return this.writeRead(containerName, ProtoMap.businessResponseToCacheWrite(response), SyncKeys.BUSINESS_SYNC_IN, (cursors) => ProtoMap.raiseBusinessCursors(cursors, response));
41
43
  }
42
- // Takes in what a business response brought that was not read, which moves no cursor: it is how
43
- // far the container has been read that this does not change, and what is merged in here is not
44
- // something this container was read for. Otherwise the same as writeBusiness.
44
+ // Stores what a business response brought that was not read. It moves no cursor, since what is merged
45
+ // here is not something this container was read for. Otherwise the same as writeBusiness.
45
46
  async storeBusiness(containerName, response) {
46
47
  return this.write(containerName, ProtoMap.businessResponseToCacheWrite(response));
47
48
  }
49
+ // The tombstones cached for one business, as a response, each as stored and as the server sent it.
50
+ // Null when there are none. The entities a deleted entity owned are tombstones of their own.
51
+ async readDeletedBusiness(containerName) {
52
+ return ProtoMap.deletedBusinessResponseFromBlobs(await this.readAll(containerName));
53
+ }
48
54
  // The same for one profile: the customer, the businesses they may reach, and the link to each.
49
55
  async readProfile(containerName) {
50
56
  return ProtoMap.profileResponseFromBlobs(await this.readAll(containerName));
51
57
  }
58
+ // The tombstones of a profile, as readDeletedBusiness answers those of a business.
59
+ async readDeletedProfile(containerName) {
60
+ return ProtoMap.deletedProfileResponseFromBlobs(await this.readAll(containerName));
61
+ }
52
62
  async readProfileCursors(containerName) {
53
63
  return this.readCursors(containerName, SyncKeys.PROFILE_SYNC_IN);
54
64
  }
@@ -56,35 +66,47 @@ export default class SyncCache {
56
66
  async writeProfile(containerName, response) {
57
67
  return this.writeRead(containerName, ProtoMap.profileResponseToCacheWrite(response), SyncKeys.PROFILE_SYNC_IN, (cursors) => ProtoMap.raiseProfileCursors(cursors, response));
58
68
  }
59
- // What a call that is not a read answered with - a sign up, an update - which is a change this client
60
- // made, and so, moves no cursor: the next read brings it back. Otherwise the same as writeProfile.
69
+ // What a call that is not a read answered with (a sign up, an update). It is a change this client
70
+ // made, so it moves no cursor and the next read brings it back. Otherwise the same as writeProfile.
61
71
  async storeProfile(containerName, response) {
62
72
  return this.write(containerName, ProtoMap.profileResponseToCacheWrite(response));
63
73
  }
64
- // The credentials that sign calls about this container - the user token in a profile container, the link token in a business one - and null before the server has issued either.
74
+ // The credentials that sign calls about this container (the user token in a profile container, the
75
+ // link token in a business one), or null before the server issues either.
65
76
  async readAuthTokens(containerName) {
66
77
  const blob = await (await this.getContainer(containerName)).read(Ids.AUTH_TOKENS_BLOB_ID);
67
78
  if (blob === null)
68
79
  return null;
69
80
  return ProtoMap.authTokensFromBytes(blob.bytes);
70
81
  }
71
- // Unconditional, unlike everything else here: a reissued token is not a later version of an earlier one, so there is nothing to compare and the latest wins.
72
- // Keeps only what this server issued - see ProtoMap.authTokensToBlob for why the authentication token is dropped whatever is passed in.
82
+ // Written without comparison, unlike everything else here: a reissued token is not a later version of
83
+ // the old one, so the latest wins. Keeps only what this server issued - see ProtoMap.authTokensToBlob
84
+ // for why the authentication token is dropped.
73
85
  async writeAuthTokens(containerName, authTokens) {
74
86
  await this.writeBlobs(containerName, [ProtoMap.authTokensToBlob(authTokens)]);
75
87
  }
76
- // The orders of whoever the responses written here answered, since a server sends a caller only their own messages.
77
- // Nothing here records which caller that was, or notices when it changes - see THE MESSAGES OF A BUSINESS ARE NOT PUBLIC.
88
+ // The open orders of whoever the written responses answered, since a server sends a caller only their
89
+ // own messages. Open means the business has not completed them; completed ones are deleted messages,
90
+ // which readDeletedOrders answers. Nothing here records which caller that was or notices when it
91
+ // changes. See THE MESSAGES OF A BUSINESS ARE NOT PUBLIC.
78
92
  async readOrders(containerName) {
79
93
  return ProtoMap.orderMessagesFromBlobs(await this.readAll(containerName));
80
94
  }
81
- // The cursor a push sends: how far the reads of this container have got with this customer's
82
- // messages, party preferences and orders alike - the cursor the next read sends for them. What a push
83
- // stores here does not move it, so that a change made while it was being made is not skipped.
95
+ // The orders the business has completed, by fulfilling or cancelling. The merchant app completes an
96
+ // order by deleting its message, so each is a tombstone, but one a server sends whole because the
97
+ // customer is shown the order afterwards. Each is as stored and as sent. A tombstone with no order is
98
+ // not one of them.
99
+ async readDeletedOrders(containerName) {
100
+ return ProtoMap.deletedOrderMessagesFromBlobs(await this.readAll(containerName));
101
+ }
102
+ // The cursor a push sends: how far reads of this container have got with this customer's messages,
103
+ // party preferences and orders alike. It is the cursor the next read sends. What a push stores here
104
+ // does not move it, so a change made during the push is not skipped.
84
105
  async readMessageUpdateNumber(containerName) {
85
106
  return (await this.readBusinessCursors(containerName)).getUpdateNumber(proto_Constants.MESSAGE_ENTITY_TYPE_ID);
86
107
  }
87
- // Refuses a message that is not an order, since only the payload says which it is: one stored as the other would never be read back.
108
+ // Refuses a message that is not an order. Only the payload says which it is, and one stored as the
109
+ // other would never be read back.
88
110
  async writeOrder(containerName, order) {
89
111
  return this.writeOwnMessage(containerName, order, ProtoMap.orderToBlob);
90
112
  }
@@ -96,23 +118,21 @@ export default class SyncCache {
96
118
  async writePartyPreferences(containerName, partyPreferences) {
97
119
  return this.writeOwnMessage(containerName, partyPreferences, ProtoMap.partyPreferencesToBlob);
98
120
  }
99
- // Gives up everything cached for a business: the entities, the cursors that say how far they were read, and the tokens
100
- // that were filed with them. For when what a container holds turns out to be of another business than the one it is
101
- // named for - which a businessId, being only how a business is found, can come to be. It cannot be told apart from the
102
- // business that the same name now finds by anything but the id of its Business, and a cursor of the one is no cursor
103
- // of the other: this cache keeps a blob only when its eTag is the greater, so, the cursors of the old would never be
104
- // lowered to the numbers of the new, and what the new has below them would never be asked for.
105
- // The container is let go of here as well as in the store, as this cache holds each one it has fetched against its name.
121
+ // Gives up everything cached for a business: entities, cursors and tokens. For when a container turns
122
+ // out to hold another business than the one it is named for, which can happen because a businessId is
123
+ // only how a business is found. Only the id of its Business tells them apart, and a cursor of one is
124
+ // no cursor of the other: the store keeps a blob only when its eTag is greater, so the old cursors
125
+ // would never be lowered and what the new business has below them would never be requested. The
126
+ // container is dropped here too, since this cache holds each fetched container by name.
106
127
  async deleteBusiness(containerName) {
107
128
  this._containers.delete(containerName);
108
129
  await this._database.deleteContainer(containerName);
109
130
  }
110
- // Drops the party preferences message this container holds, and answers zero when it holds none.
111
- // The one delete here that no tombstone asked for, and the only reason it exists: one written
112
- // before anybody signed in is handed over to the container named for the membership, and the
113
- // copy left behind would otherwise be read by the next anonymous visitor and by the next customer
114
- // to sign in - see Model.businessSyncIn. Nothing else about a container is a caller's to remove:
115
- // everything in one came off the wire, and what the server has not said is gone stays.
131
+ // Drops the party preferences message this container holds, and answers zero when there is none. It
132
+ // is the one delete no tombstone asked for. A cart written before anybody signed in is handed over to
133
+ // the container named for the membership, and the copy left behind would be read by the next anonymous
134
+ // visitor and the next customer to sign in - see Model.businessSyncIn. Nothing else in a container is
135
+ // a caller's to remove: it all came off the wire, and anything the server has not said is gone stays.
116
136
  async deletePartyPreferences(containerName) {
117
137
  const partyPreferences = await this.readPartyPreferences(containerName);
118
138
  if (partyPreferences === null)
@@ -122,15 +142,13 @@ export default class SyncCache {
122
142
  deletedIds: [ProtoMap.getMessageId(partyPreferences)],
123
143
  });
124
144
  }
125
- // The messages a sync in response carried, in a single batch - a pushed order and the party
126
- // preferences message it emptied belong in one write, so that this cache cannot come to hold the
127
- // order without the emptying. Tombstones included, the same as any other entity array read off
128
- // the wire: this is what a push's own messages are stored as once the server has echoed them back
129
- // with real update numbers. Neither is checked against a payload type, unlike the two writers above -
130
- // those are for a caller handing in a message it was given, where storing an order as party
131
- // preferences would be a mistake worth catching, and this is for messages the response itself is
132
- // the authority on. It moves no cursor, as it is a change that this client made: the next read
133
- // brings the messages back, and what is stored here is the same, so, that changes nothing.
145
+ // The messages a sync in response carried, in one batch, so the cache cannot hold a pushed order
146
+ // without the party preferences message it emptied. Tombstones are included, as for any entity array
147
+ // off the wire. This stores a push's own messages once the server has echoed them with real update
148
+ // numbers. It checks no payload type, unlike writeOrder and writePartyPreferences: those take a
149
+ // message the caller was given, where an order stored as party preferences is a mistake worth
150
+ // catching, and here the response is the authority. It moves no cursor, since this client made the
151
+ // change and the next read brings the same messages back.
134
152
  async writeMessages(containerName, messages) {
135
153
  return this.write(containerName, ProtoMap.messagesToCacheWrite(messages));
136
154
  }
@@ -138,22 +156,21 @@ export default class SyncCache {
138
156
  return (await this.getContainer(containerName)).readAll();
139
157
  }
140
158
  // A message the caller holds rather than one a response carried, versioned from what this container
141
- // holds of it: one this client amended has no update number of its own, and is stored at the update
142
- // number of the copy it amends - see ProtoMap.messageToBlob
159
+ // holds of it. One this client amended has no update number, so it is stored at that of the copy it
160
+ // amends - see ProtoMap.messageToBlob
143
161
  async writeOwnMessage(containerName, message, toBlob) {
144
162
  const held = await (await this.getContainer(containerName)).read(ProtoMap.getMessageId(message));
145
163
  return this.writeBlobs(containerName, [toBlob(message, held?.eTag ?? null)]);
146
164
  }
147
- // For the writers that store one thing and delete nothing - a credential or a single message,
148
- // none of which a tombstone ever arrives for on its own
165
+ // For writers that store one thing and delete nothing (a credential or a single message), since no
166
+ // tombstone arrives for either on its own
149
167
  async writeBlobs(containerName, blobs) {
150
168
  return this.write(containerName, { blobs: blobs, deletedIds: NO_DELETES });
151
169
  }
152
- // One transaction per call, which is what makes an interrupted write repeat itself rather than leave the cache part written.
153
- // The count the store answers with covers only what it stored: a blob it already holds a copy of that
154
- // is as new, a tombstone it already holds included, is not stored, and is not counted. A delete is
155
- // unconditional, so there is nothing for it to compare, and the ids are counted instead: the only
156
- // delete there is is a caller's own - see deletePartyPreferences.
170
+ // One transaction per call, so an interrupted write repeats instead of leaving the cache part written.
171
+ // The store's count covers only what it stored: a blob it already holds a copy of that is as new (a
172
+ // tombstone too) is not stored and not counted. A delete has nothing to compare, so the ids are
173
+ // counted instead. The only delete is a caller's own - see deletePartyPreferences.
157
174
  async write(containerName, cacheWrite) {
158
175
  const blobs = cacheWrite.blobs;
159
176
  const deletedIds = cacheWrite.deletedIds;
@@ -162,11 +179,10 @@ export default class SyncCache {
162
179
  const storedCount = await (await this.getContainer(containerName)).write(blobs, deletedIds);
163
180
  return storedCount + deletedIds.length;
164
181
  }
165
- // A page of a read: what it brought, and the cursors it moved, in one batch. The cursors are not what the
166
- // page brought, so they are not counted: the count is what the entities of it changed. The store
167
- // keeps a cursor only where it is the greater, which it is of the one that was read, so what it
168
- // stored of them is the cursors that moved - or fewer, where another writer moved one first, which
169
- // is why the count cannot go below zero.
182
+ // A page of a read: what it brought and the cursors it moved, in one batch. The count is what the
183
+ // page's entities changed, so the cursors are subtracted. The store keeps a cursor only where it is
184
+ // greater, and each moved one is greater than the one read, so it stores the moved cursors or fewer,
185
+ // when another writer moved one first. That is why the count is clamped at zero.
170
186
  async writeRead(containerName, cacheWrite, cursorKeys, raiseCursors) {
171
187
  const cursors = await this.readCursors(containerName, cursorKeys);
172
188
  const movedKeys = raiseCursors(cursors);
@@ -182,8 +198,9 @@ export default class SyncCache {
182
198
  const blobs = await Promise.all(keys.map((key) => container.read(Ids.getSyncCursorBlobId(key))));
183
199
  return ProtoMap.cursorsFromBlobs(keys, blobs);
184
200
  }
185
- // Held against their names, so each container is fetched once however often it is used. The promise is held rather than the container, so two calls arriving together share one fetch.
186
- // A caller that deletes a container from the store gives up this cache with it: nothing here can tell that what it holds is no longer what the store would hand out.
201
+ // Held by name, so each container is fetched once. The promise is held, so two calls arriving
202
+ // together share one fetch. Whoever deletes a container from the store must drop it here too, since
203
+ // nothing here can tell that it no longer matches the store.
187
204
  getContainer(containerName) {
188
205
  const held = this._containers.get(containerName);
189
206
  if (held !== undefined)
package/dist/SyncKeys.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { Constants as proto_Constants } from "./generated/Constants";
2
- // Which update numbers a SyncState keeps, and what entity type counts under which key. Kept here
3
- // rather than in SyncState since these are facts about the protocol, not about counting.
4
- // A state refuses keys outside its set on purpose: Business arrives from both sync outs with
5
- // unrelated update numbers, and these must never mix.
6
- // The cursors a business sync in sends: the business itself, its attachments, its configurations,
7
- // its messages and its items. These are the five cursor fields of a BusinessSyncOutRequest.
2
+ // Which update numbers a SyncState keeps, and which entity type counts under which key. Kept here
3
+ // since these are facts about the protocol, not about counting.
4
+ // A state refuses keys outside its set because Business arrives from both sync outs with
5
+ // unrelated update numbers, which must never mix.
6
+ // The cursors a business sync in sends: the five cursor fields of a BusinessSyncOutRequest (the
7
+ // business, its attachments, configurations, messages and items).
8
8
  export const BUSINESS_SYNC_IN = Object.freeze([
9
9
  proto_Constants.BUSINESS_ENTITY_TYPE_ID,
10
10
  proto_Constants.ATTACHMENT_ENTITY_TYPE_ID,
@@ -12,8 +12,8 @@ export const BUSINESS_SYNC_IN = Object.freeze([
12
12
  proto_Constants.MESSAGE_ENTITY_TYPE_ID,
13
13
  proto_Constants.ITEM_ENTITY_TYPE_ID,
14
14
  ]);
15
- // The cursors a profile sync in sends - the three entity types Java keeps in a profile database.
16
- // BUSINESS also appears above, with an unrelated number in each; never mix the two.
15
+ // The cursors a profile sync in sends: the three entity types Java keeps in a profile database.
16
+ // BUSINESS also appears above with an unrelated number, so never mix the two.
17
17
  export const PROFILE_SYNC_IN = Object.freeze([
18
18
  proto_Constants.USER_ENTITY_TYPE_ID,
19
19
  proto_Constants.BUSINESS_ENTITY_TYPE_ID,
package/dist/SyncState.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { InvalidArgumentException } from "./Exceptions";
2
2
  import * as SyncKeys from "./SyncKeys";
3
- // How far one read has got, as an update number per entity type computed from what arrives and sent back on the next call, which is what makes a read incremental. One class for both directions - only the keys differ.
4
- // A value held in memory: SyncCache keeps it between sessions, one blob per entity type, so that a new session asks only for what changed. A container that holds none asks from zero and is sent what it already holds, which costs bandwidth alone, since the store keeps a blob only when its eTag is the greater. "In" and "out" name the direction relative to this store, the opposite way round to the calls of the wire.
3
+ // How far one read has got: an update number per entity type, computed from what arrives and sent back on the next call, which makes a read incremental. One class for both directions; only the keys differ.
4
+ // Held in memory. SyncCache persists it, one blob per entity type, so a new session asks only for what changed. A container with none asks from zero and is sent what it already holds, which costs bandwidth only, since the store keeps a blob only when its eTag is greater. "In" and "out" are relative to this store, the opposite of the wire's calls.
5
5
  export default class SyncState {
6
- // The keys this state accepts, which is the whole difference between a business state and a profile one. Anything else is refused, because a Business arrives from both calls with an unrelated update number in each.
6
+ // The keys this state accepts, which is the whole difference between a business state and a profile one. Others are refused (see SyncKeys).
7
7
  constructor(_keys) {
8
8
  this._keys = _keys;
9
9
  this._updateNumbers = new Map();
@@ -12,8 +12,8 @@ export default class SyncState {
12
12
  getUpdateNumber(entityTypeId) {
13
13
  return this._updateNumbers.get(this.checkKey(entityTypeId)) ?? 0n;
14
14
  }
15
- // Raises the cursor for this entity type (tombstones included - skipping one would refetch it for ever) and reports whether it moved.
16
- // Numbers only ever rise; a lower one is ignored as stale or redelivered.
15
+ // Raises the cursor for this entity type and reports whether it moved. Tombstones count too, since skipping one would refetch it for ever.
16
+ // A lower number is ignored as stale or redelivered.
17
17
  raiseUpdateNumber(entityTypeId, updateNumber) {
18
18
  const key = this.checkKey(entityTypeId);
19
19
  if (updateNumber <= (this._updateNumbers.get(key) ?? 0n))
package/dist/Tax.js CHANGED
@@ -1,20 +1,16 @@
1
1
  import { countryCodeOf, createTaxType, taxCodeOf } from "./TaxType";
2
2
  import { taxRateFromScaledValue } from "./Constants";
3
- // A tax and its rate, applying to a product or a shopping-list line. Rate is exposed as a
4
- // percentage (18 for 18%), though the wire carries it scaled by Constants.TAX_RATE_FACTOR.
3
+ // A tax and its rate, on a product or a shopping-list line. The rate is a percentage (18 for 18%),
4
+ // though the wire carries it scaled by Constants.TAX_RATE_FACTOR.
5
5
  //
6
- // The taxes of a product are INCLUDED in the prices of its packages, and the taxes of the delivery
7
- // charge in the charge: nothing is added to a price a customer is shown. A shopping list item is
8
- // the other way round - it stores its price WITHOUT taxes and works its taxes out on top - which is
9
- // why a price has its taxes taken out on the way into an item (see CartEditor) and put back for
10
- // display (see ShoppingListItem.unitPriceWithTaxes).
11
- // Deliberately doesn't compute tax on an amount - only a line item knows the amount, and does
12
- // that itself.
6
+ // Taxes are included in the prices a customer sees: a product's in its package prices, the
7
+ // delivery charge's in the charge. A shopping list item stores its price without taxes, so
8
+ // CartEditor takes them out on the way in and ShoppingListItem.unitPriceWithTaxes puts them back
9
+ // for display. A Tax holds no amount. Only a line item knows one.
13
10
  export default class Tax {
14
- // Only ProtoMap calls this: it is the one place that sees a rate still multiplied by
15
- // Constants.TAX_RATE_FACTOR. The wire carries a tax as its raw code, which means something only
16
- // within a country - that of the business the tax is on - so the type is made here from the two,
17
- // the same way Java TaxType.createId does.
11
+ // For ProtoMap, which sees a rate still multiplied by Constants.TAX_RATE_FACTOR. The wire carries
12
+ // a tax as its raw code, which means something only within a country (that of the business the
13
+ // tax is on), so the type is made here from the two, as Java TaxType.createId does.
18
14
  static fromScaledValues(countryCode, taxCode, scaledRate) {
19
15
  return new Tax(createTaxType(countryCode, taxCode), taxRateFromScaledValue(scaledRate));
20
16
  }
@@ -25,10 +21,9 @@ export default class Tax {
25
21
  get type() {
26
22
  return this._type;
27
23
  }
28
- // The country this tax belongs to, ISO 3166-1 numeric. A TaxType value is not a raw Java tax
29
- // code - it is Java TaxType.createId(country, taxCode), the country in its high bits - so the
30
- // country comes back out of the value itself rather than having to be carried alongside it.
31
- // Zero for TaxType.UNSPECIFIED_TAX_TYPE, no country's code being zero.
24
+ // The country this tax belongs to, ISO 3166-1 numeric. A TaxType value is Java
25
+ // TaxType.createId(country, taxCode), with the country in its high bits, so the country comes
26
+ // out of the value itself. Zero for TaxType.UNSPECIFIED_TAX_TYPE, as no country's code is zero.
32
27
  get countryCode() {
33
28
  return countryCodeOf(this._type);
34
29
  }
@@ -41,9 +36,9 @@ export default class Tax {
41
36
  return this._rate;
42
37
  }
43
38
  }
44
- // How much one TYPE of tax comes to across a shopping list - the line a bill shows for it, "GST
45
- // 5.25". Several taxes of one type, on one item or on many, are added into one. Always a tax that
46
- // is included in the total the list comes to, never an extra on top of it.
39
+ // How much one type of tax comes to across a shopping list: the line a bill shows for it, "GST
40
+ // 5.25". Several taxes of one type, on one item or on many, are added into one. Always included in
41
+ // the list's total, never an extra on top.
47
42
  export class TaxAmount {
48
43
  constructor(_type, _amount) {
49
44
  this._type = _type;
@@ -52,7 +47,7 @@ export class TaxAmount {
52
47
  get type() {
53
48
  return this._type;
54
49
  }
55
- // Zero is possible - a list of free items has taxes and owes none - and a bill shows no line for it
50
+ // Zero is possible (a list of free items has taxes and owes none), and a bill shows no line for it
56
51
  get amount() {
57
52
  return this._amount;
58
53
  }
package/dist/TaxHelper.js CHANGED
@@ -1,27 +1,20 @@
1
1
  import { Constants as proto_Constants } from "./generated/Constants";
2
2
  import { taxRateToScaledValue } from "./Constants";
3
- // The arithmetic of taxes, which is Java com.merabills.repository.entities.helpers.TaxHelper and
4
- // is meant to give the same answer to the unit. It is the only place in this library where a tax is
5
- // worked out, added to an amount or taken out of one.
3
+ // The arithmetic of taxes, ported from Java com.merabills.repository.entities.helpers.TaxHelper to
4
+ // give the same answer to the unit. The only place in this library that works out a tax, adds it to
5
+ // an amount or takes it out. Not on the public surface (see index.ts): a UI reads unscaled taxes
6
+ // off a ShoppingListItem, ShoppingList or CartLine.
6
7
  //
7
- // It works on SCALED values, not the unscaled numbers the rest of this library speaks: an amount is
8
- // a bigint multiplied by Constants.AMOUNT_FACTOR, and a rate is a number multiplied by
9
- // Constants.TAX_RATE_FACTOR, so 5% is 500. Java does the same in longs and ints, and rounding a
10
- // tax in floating point instead would round some of them the other way, which is the difference
11
- // between a bill that agrees with the merchant's and one that is a paisa out. A bigint rather than
12
- // a number because an amount can be as large as 2^44 and a product with a rate overflows 2^53.
13
- //
14
- // Only integer arithmetic is used: a tax is the tax base multiplied by the rate, and the division
15
- // by TAX_RATE_FACTOR and the rounding (half up, as Math.round) are the last steps. Each tax is
16
- // rounded on its own, as each is shown (and paid) on its own.
17
- //
18
- // Not on the public surface (see index.ts): a UI reads a tax off a ShoppingListItem, a ShoppingList
19
- // or a CartLine, which have done this arithmetic already, in unscaled numbers.
8
+ // Unlike the rest of this library it works on scaled integers, as Java does: an amount is a bigint
9
+ // multiplied by Constants.AMOUNT_FACTOR, a rate a number multiplied by Constants.TAX_RATE_FACTOR
10
+ // (5% is 500). Floating point would round some taxes the other way. Amounts are bigint because one
11
+ // can reach 2^44, and multiplying by a rate overflows 2^53.
12
+ // A tax is the tax base times the rate, divided by TAX_RATE_FACTOR and rounded half up (as
13
+ // Math.round). Each tax is rounded on its own, as each is shown and paid on its own.
20
14
  class TaxHelper {
21
- // The rates of these taxes the way the arithmetic below takes them: scaled, and in the same
22
- // order. A rate that is not greater than zero is no tax at all - Java refuses one (validateTaxes)
23
- // and drops one that arrives (getTaxCodesAndRates) - so it is a zero here, adding and removing
24
- // nothing, and the taxes stay in step with the amounts worked out for them.
15
+ // The rates of these taxes as the arithmetic below takes them: scaled, in the same order. A rate
16
+ // not greater than zero is no tax. Java refuses one (validateTaxes) and drops one that arrives
17
+ // (getTaxCodesAndRates), so here it is zero, which keeps the taxes in step with their amounts.
25
18
  static scaledRatesOf(taxes) {
26
19
  return taxes.map((tax) => Number.isFinite(tax.rate) && tax.rate > 0 ? taxRateToScaledValue(tax.rate) : 0);
27
20
  }
@@ -50,18 +43,17 @@ class TaxHelper {
50
43
  result += TaxHelper.calculateTaxAmount(taxBase, taxRate);
51
44
  return result;
52
45
  }
53
- // The inverse of addTaxes: the amount the taxes with these rates have to be added to for the
54
- // result to be this amount with taxes. Each tax is rounded on its own, so some amounts with taxes
55
- // cannot be reached; this then answers the amount for which addTaxes comes closest (the lower
56
- // one, when two are equally close). So removeTaxes(addTaxes(amount, rates), rates) is always
57
- // amount.
46
+ // The inverse of addTaxes: the amount that gives this amount with taxes. Each tax is rounded on
47
+ // its own, so some amounts with taxes cannot be reached. Then this answers the amount for which
48
+ // addTaxes comes closest (the lower one on a tie). So removeTaxes(addTaxes(amount, rates),
49
+ // rates) is always amount.
58
50
  static removeTaxes(amountWithTaxes, taxRates) {
59
51
  if (amountWithTaxes <= 0n || taxRates.length === 0)
60
52
  return amountWithTaxes;
61
53
  // Ignoring rounding, the amount with taxes is the amount multiplied by (1 + the total rate), so
62
- // estimate the amount by dividing by that. Rounding moves each tax by at most half a unit, and
63
- // the estimate is itself rounded, so the amount looked for is within the number of taxes of the
64
- // estimate (Java looks as far): look at every amount that close.
54
+ // dividing by that estimates the amount. Rounding moves each tax by at most half a unit and the
55
+ // estimate is rounded too, so the amount is within the number of taxes of the estimate (Java
56
+ // looks as far). Look at every amount that close.
65
57
  const estimate = TaxHelper.divideAndRound(amountWithTaxes * TaxHelper.TAX_RATE_FACTOR, TaxHelper.TAX_RATE_FACTOR + BigInt(TaxHelper.calculateTotalTaxRate(taxRates)));
66
58
  const reach = BigInt(taxRates.length);
67
59
  const first = estimate > reach ? estimate - reach : 0n;
@@ -77,8 +69,8 @@ class TaxHelper {
77
69
  }
78
70
  return result;
79
71
  }
80
- // Divides by a positive denominator and rounds half up, exactly as Math.round does. A bigint
81
- // division truncates towards zero, so the floor is taken by hand.
72
+ // Divides by a positive denominator and rounds half up, as Math.round does. A bigint division
73
+ // truncates towards zero, so the floor is taken by hand.
82
74
  static divideAndRound(numerator, denominator) {
83
75
  const doubled = numerator * 2n + denominator;
84
76
  const divisor = denominator * 2n;
package/dist/TaxType.js CHANGED
@@ -1,15 +1,14 @@
1
1
  // The name of a tax, for a UI to show. Not part of the wire protocol: a Tax is sent as its raw tax
2
- // code, which is unique only within a country, and ProtoMap combines it with the country of the
3
- // business the tax is on. The values are Java TaxType.createId(country, taxCode) =
4
- // (country.getNumeric() << 8) | taxCode, so each is globally unique; the country is recovered with
5
- // countryCodeOf and the raw tax code with taxCodeOf. They are copied from Java's TaxType by hand, and
6
- // pinned there by TaxTypeIdsTest (repository/test/com/merabills/repository/entities/helpers), which
7
- // fails when GST or the shift of the country changes, or a type is added, until this file has
8
- // followed - so a tax added there must be added here with the same value. The tests of this
9
- // library pin the same values (Values.test.ts).
2
+ // code, unique only within a country, and ProtoMap combines it with the country of the business.
3
+ // The values are Java TaxType.createId(country, taxCode) = (country.getNumeric() << 8) | taxCode,
4
+ // so each is globally unique. countryCodeOf and taxCodeOf take one apart.
5
+ // Copied by hand from Java's TaxType and pinned there by TaxTypeIdsTest
6
+ // (repository/test/com/merabills/repository/entities/helpers), which fails when GST or the country
7
+ // shift changes or a type is added. A tax added there must be added here with the same value. This
8
+ // library's tests pin the same values (Values.test.ts).
10
9
  export var TaxType;
11
10
  (function (TaxType) {
12
- // No country's numeric code is ever 0, so this can never collide with a real type
11
+ // No country's numeric code is 0, so this cannot collide with a real type
13
12
  TaxType[TaxType["UNSPECIFIED_TAX_TYPE"] = 0] = "UNSPECIFIED_TAX_TYPE";
14
13
  // India (country code 356): Goods and Services Tax (tax code 0)
15
14
  TaxType[TaxType["GST"] = 91136] = "GST";
@@ -1,7 +1,7 @@
1
1
  import Entity from "./Entity";
2
2
  // The customer: whose orders and cart the business sees. Named for the database it lives in
3
- // (profile, not business) - a signed-out storefront has no user but still shows its catalog.
4
- // Exactly the User message, with no derived state added.
3
+ // (profile, not business), since a signed-out storefront has no user but still shows its catalog.
4
+ // Holds the User message and no derived state.
5
5
  export default class UserProfile extends Entity {
6
6
  constructor(header, _firstName, _lastName, _mobilePhoneNumber, _addresses, _preferredLanguageCode) {
7
7
  super(header);
@@ -21,8 +21,8 @@ export default class UserProfile extends Entity {
21
21
  get mobilePhoneNumber() {
22
22
  return this._mobilePhoneNumber;
23
23
  }
24
- // The delivery addresses of this customer, and which of them is the default. This is also what
25
- // resolves Order.deliveryAddressId, which is an id into this list.
24
+ // The customer's delivery addresses, and which is the default. Also resolves
25
+ // Order.deliveryAddressId, an id into this list.
26
26
  get addresses() {
27
27
  return this._addresses;
28
28
  }