@peabody-soft/mbs-model-ts 1.6.0 → 1.7.1

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/SyncCache.js CHANGED
@@ -2,18 +2,19 @@ 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 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.
5
+ // What the server has already sent, so the next call can ask only for what changed. A read cache:
6
+ // everything in it came from a response, so dropping a container costs one refetch.
7
+ // It does not write through or sync. The caller calls the Service, does the paging, and chooses
8
+ // which writes are reads. It holds only wire data, which ProtoMap reads. See SYNC LAYER in
9
+ // ../Prompt.txt.
10
+ // - One response is one write. A tombstone is a blob that no older blob replaces. The server sends
11
+ // one for every deleted entity, children included, so the cache never infers a deletion.
11
12
  // - 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
+ // deleted message). readDeletedBusiness, readDeletedProfile and readDeletedOrders return them.
13
14
  // 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
+ // - A cursor is a blob per entity type (its eTag is the cursor). It is not derived from the
16
+ // entities, because tombstones and this client's own changes would move it. It rises only with
17
+ // a page of a read, in that page's batch, and the store keeps the greater.
17
18
  export default class SyncCache {
18
19
  constructor(_database) {
19
20
  this._database = _database;
@@ -21,7 +22,7 @@ export default class SyncCache {
21
22
  }
22
23
  // Everything cached for one business, as the response that carried it, or null before anything is
23
24
  // 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.
25
+ // for an item. Code that builds on this must leave tombstones out, except for the order of a message.
25
26
  async readBusiness(containerName) {
26
27
  return ProtoMap.businessResponseFromBlobs(await this.readAll(containerName));
27
28
  }
@@ -35,19 +36,20 @@ export default class SyncCache {
35
36
  // copy wins and a repeated page changes nothing. A tombstone wins over the copies of what it deleted,
36
37
  // since a delete is a later write. The cursors the page raises go in the same batch, so they cannot
37
38
  // 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
+ // Returns how many blobs it stored, so a caller can tell whether the response changed anything and
40
+ // needs publishing. The store keeps a blob only when its eTag is later, so it can reject a
41
+ // response in full. A rejected response leaves the cache unchanged - see MODEL.
41
42
  async writeBusiness(containerName, response) {
42
43
  return this.writeRead(containerName, ProtoMap.businessResponseToCacheWrite(response), SyncKeys.BUSINESS_SYNC_IN, (cursors) => ProtoMap.raiseBusinessCursors(cursors, response));
43
44
  }
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
+ // Stores a business response that is not a page of a read of this container, for example one merged
46
+ // in from another container. It moves no cursor, because this container was not read for what the
47
+ // response holds. Otherwise the same as writeBusiness.
46
48
  async storeBusiness(containerName, response) {
47
49
  return this.write(containerName, ProtoMap.businessResponseToCacheWrite(response));
48
50
  }
49
51
  // 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.
52
+ // Null when there are none. The entities a deleted entity owned are tombstones too.
51
53
  async readDeletedBusiness(containerName) {
52
54
  return ProtoMap.deletedBusinessResponseFromBlobs(await this.readAll(containerName));
53
55
  }
@@ -55,7 +57,7 @@ export default class SyncCache {
55
57
  async readProfile(containerName) {
56
58
  return ProtoMap.profileResponseFromBlobs(await this.readAll(containerName));
57
59
  }
58
- // The tombstones of a profile, as readDeletedBusiness answers those of a business.
60
+ // The tombstones of a profile, as readDeletedBusiness returns those of a business.
59
61
  async readDeletedProfile(containerName) {
60
62
  return ProtoMap.deletedProfileResponseFromBlobs(await this.readAll(containerName));
61
63
  }
@@ -66,8 +68,9 @@ export default class SyncCache {
66
68
  async writeProfile(containerName, response) {
67
69
  return this.writeRead(containerName, ProtoMap.profileResponseToCacheWrite(response), SyncKeys.PROFILE_SYNC_IN, (cursors) => ProtoMap.raiseProfileCursors(cursors, response));
68
70
  }
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.
71
+ // Stores the response to a call that is not a read (a sign up, an update). It holds a change this
72
+ // client made, so it moves no cursor and the next read brings it back. Otherwise the same as
73
+ // writeProfile.
71
74
  async storeProfile(containerName, response) {
72
75
  return this.write(containerName, ProtoMap.profileResponseToCacheWrite(response));
73
76
  }
@@ -80,22 +83,22 @@ export default class SyncCache {
80
83
  return ProtoMap.authTokensFromBytes(blob.bytes);
81
84
  }
82
85
  // 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.
86
+ // the old one, so the last one written wins. Stores only what this server issued - see
87
+ // ProtoMap.authTokensToBlob for why the authentication token is dropped.
85
88
  async writeAuthTokens(containerName, authTokens) {
86
89
  await this.writeBlobs(containerName, [ProtoMap.authTokensToBlob(authTokens)]);
87
90
  }
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.
91
+ // The open orders of the caller whose responses were written here, because a server sends a caller
92
+ // only their own messages. An open order is one the business has not completed. Completed orders
93
+ // are deleted messages, which readDeletedOrders returns. Nothing here records which caller that was
94
+ // or detects a change of caller. See THE MESSAGES OF A BUSINESS ARE NOT PUBLIC.
92
95
  async readOrders(containerName) {
93
96
  return ProtoMap.orderMessagesFromBlobs(await this.readAll(containerName));
94
97
  }
95
98
  // 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
+ // order by deleting its message, so each is a tombstone. A server sends these whole, because the
100
+ // customer is shown the order afterwards. Each is returned as stored and as sent. A tombstone with
101
+ // no order in it is not included.
99
102
  async readDeletedOrders(containerName) {
100
103
  return ProtoMap.deletedOrderMessagesFromBlobs(await this.readAll(containerName));
101
104
  }
@@ -105,34 +108,37 @@ export default class SyncCache {
105
108
  async readMessageUpdateNumber(containerName) {
106
109
  return (await this.readBusinessCursors(containerName)).getUpdateNumber(proto_Constants.MESSAGE_ENTITY_TYPE_ID);
107
110
  }
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.
111
+ // Throws for a message that is not an order. Only the payload shows which kind a message is, and
112
+ // one stored as the other kind would never be read back.
110
113
  async writeOrder(containerName, order) {
111
114
  return this.writeOwnMessage(containerName, order, ProtoMap.orderToBlob);
112
115
  }
113
- // The party preferences message in this container, or null when there is none. Two mean two
114
- // callers, which is the same open question as the orders above.
116
+ // The party preferences message in this container, or null when there is none. A customer has one
117
+ // per business, so two would mean two callers share the container, the open question described
118
+ // for readOrders.
115
119
  async readPartyPreferences(containerName) {
116
120
  return ProtoMap.partyPreferencesMessageFromBlobs(await this.readAll(containerName));
117
121
  }
118
122
  async writePartyPreferences(containerName, partyPreferences) {
119
123
  return this.writeOwnMessage(containerName, partyPreferences, ProtoMap.partyPreferencesToBlob);
120
124
  }
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.
125
+ // Drops everything cached for a business: entities, cursors and tokens. Use it when a container
126
+ // turns out to hold a different business than the one it is named for. That can happen because a
127
+ // businessId only locates a business. Only the id of its Business tells two businesses apart, and
128
+ // cursors do not carry over from one to the other: the store keeps a blob only when its eTag is
129
+ // greater, so the old cursors would never be lowered and what the new business has below them
130
+ // would never be requested. The container is also dropped from this cache's map of fetched
131
+ // containers.
127
132
  async deleteBusiness(containerName) {
128
133
  this._containers.delete(containerName);
129
134
  await this._database.deleteContainer(containerName);
130
135
  }
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.
136
+ // Deletes the party preferences message this container holds, and returns zero when there is none.
137
+ // It is the only delete that no tombstone caused. A cart written before anybody signed in is handed
138
+ // over to the container named for the membership. The copy left behind would be read by the next
139
+ // anonymous visitor and the next customer to sign in - see Model.businessSyncIn. A caller deletes
140
+ // nothing else from a container: it all came off the wire, and anything the server has not
141
+ // reported as deleted stays.
136
142
  async deletePartyPreferences(containerName) {
137
143
  const partyPreferences = await this.readPartyPreferences(containerName);
138
144
  if (partyPreferences === null)
@@ -142,35 +148,36 @@ export default class SyncCache {
142
148
  deletedIds: [ProtoMap.getMessageId(partyPreferences)],
143
149
  });
144
150
  }
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.
151
+ // Stores the messages a sync in response carried, in one batch, so the cache cannot hold a pushed
152
+ // order without the party preferences message it emptied. Tombstones are included, as for any
153
+ // entity array off the wire. This stores a push's own messages once the server has echoed them with
154
+ // real update numbers. It checks no payload type, unlike writeOrder and writePartyPreferences.
155
+ // Those take a message from the caller, where storing an order as party preferences is a mistake
156
+ // worth catching. Here the response is taken as correct. It moves no cursor, because this client
157
+ // made the change and the next read brings the same messages back.
152
158
  async writeMessages(containerName, messages) {
153
159
  return this.write(containerName, ProtoMap.messagesToCacheWrite(messages));
154
160
  }
155
161
  async readAll(containerName) {
156
162
  return (await this.getContainer(containerName)).readAll();
157
163
  }
158
- // A message the caller holds rather than one a response carried, versioned from what this container
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
164
+ // Stores a message the caller holds, not one a response carried, versioned from the copy this
165
+ // container holds. A message this client amended has no update number, so it is stored at that of
166
+ // the copy it amends - see ProtoMap.messageToBlob
161
167
  async writeOwnMessage(containerName, message, toBlob) {
162
168
  const held = await (await this.getContainer(containerName)).read(ProtoMap.getMessageId(message));
163
169
  return this.writeBlobs(containerName, [toBlob(message, held?.eTag ?? null)]);
164
170
  }
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
171
+ // For writers that store one blob and delete nothing (a credential or a single message), because no
172
+ // tombstone comes with either
167
173
  async writeBlobs(containerName, blobs) {
168
174
  return this.write(containerName, { blobs: blobs, deletedIds: NO_DELETES });
169
175
  }
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.
176
+ // One transaction per call, so an interrupted write can be repeated and never leaves the cache
177
+ // partly written. The store's count covers only what it stored: a blob that the store already holds
178
+ // a copy of that is as new (a tombstone too) is not stored and not counted. A delete has nothing
179
+ // to compare, so the ids are counted instead. The only delete is a caller's own - see
180
+ // deletePartyPreferences.
174
181
  async write(containerName, cacheWrite) {
175
182
  const blobs = cacheWrite.blobs;
176
183
  const deletedIds = cacheWrite.deletedIds;
@@ -179,10 +186,11 @@ export default class SyncCache {
179
186
  const storedCount = await (await this.getContainer(containerName)).write(blobs, deletedIds);
180
187
  return storedCount + deletedIds.length;
181
188
  }
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.
189
+ // Stores a page of a read: its entities and the cursors it moved, in one batch. The count is the
190
+ // number of entities the page changed, so the moved cursors are subtracted from the store's count.
191
+ // The store keeps a cursor only if it is greater. Each moved cursor is greater than the one read,
192
+ // so the store keeps all of them, or fewer when another writer moved one first. That is why the
193
+ // count is clamped at zero.
186
194
  async writeRead(containerName, cacheWrite, cursorKeys, raiseCursors) {
187
195
  const cursors = await this.readCursors(containerName, cursorKeys);
188
196
  const movedKeys = raiseCursors(cursors);
@@ -198,16 +206,16 @@ export default class SyncCache {
198
206
  const blobs = await Promise.all(keys.map((key) => container.read(Ids.getSyncCursorBlobId(key))));
199
207
  return ProtoMap.cursorsFromBlobs(keys, blobs);
200
208
  }
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.
209
+ // Containers are held by name, so each is fetched once. The promise is held, so two calls arriving
210
+ // together share one fetch. Code that deletes a container from the store must also drop it here,
211
+ // because this cache cannot detect that the store no longer has it.
204
212
  getContainer(containerName) {
205
213
  const held = this._containers.get(containerName);
206
214
  if (held !== undefined)
207
215
  return held;
208
216
  const pending = this._database.getContainer(containerName);
209
217
  this._containers.set(containerName, pending);
210
- // A failure is not remembered, or one unlucky read would poison this name for as long as this cache lives
218
+ // Forget a failed fetch, or one failure would make this name fail for as long as this cache lives
211
219
  void pending.catch(() => {
212
220
  if (this._containers.get(containerName) === pending)
213
221
  this._containers.delete(containerName);
package/dist/SyncKeys.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Constants as proto_Constants } from "./generated/Constants";
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
2
+ // Which update numbers a SyncState keeps, and which key each entity type counts under. These are
3
+ // facts about the protocol, so they are kept here and not in SyncState.
4
+ // A state throws for keys outside its set because Business arrives from both sync outs with
5
5
  // unrelated update numbers, which must never mix.
6
6
  // The cursors a business sync in sends: the five cursor fields of a BusinessSyncOutRequest (the
7
7
  // business, its attachments, configurations, messages and items).
@@ -32,8 +32,8 @@ export const ITEM_ENTITY_TYPE_IDS = Object.freeze([
32
32
  proto_Constants.PRICE_ENTITY_TYPE_ID,
33
33
  proto_Constants.PRICE_LIST_ENTITY_TYPE_ID,
34
34
  ]);
35
- // The key an entity counts under: one update number per table, not per class - a product's number
36
- // is the shared item number.
35
+ // The key an entity type counts under. There is one update number per table, so a product's
36
+ // number is the shared item number.
37
37
  export function keyOf(entityTypeId) {
38
38
  return ITEM_ENTITY_TYPE_IDS.includes(entityTypeId)
39
39
  ? proto_Constants.ITEM_ENTITY_TYPE_ID
package/dist/SyncState.js CHANGED
@@ -1,19 +1,27 @@
1
1
  import { InvalidArgumentException } from "./Exceptions";
2
2
  import * as SyncKeys from "./SyncKeys";
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.
3
+ // How far one read has got: an update number per entity type. It is computed from what arrives and
4
+ // sent with the next call, which makes a read incremental. One class serves both directions; only
5
+ // the keys differ.
6
+ // Held in memory. SyncCache persists it, one blob per entity type, so a new session asks only for
7
+ // what changed. A container with no stored state asks from zero and is sent what it already holds.
8
+ // That costs bandwidth only, because the store keeps a blob only when its eTag is greater. "In" and
9
+ // "out" are relative to this store, the opposite of the wire's calls.
5
10
  export default class SyncState {
6
- // The keys this state accepts, which is the whole difference between a business state and a profile one. Others are refused (see SyncKeys).
11
+ // The keys this state accepts. This is the only difference between a business state and a
12
+ // profile state. Other keys throw (see SyncKeys).
7
13
  constructor(_keys) {
8
14
  this._keys = _keys;
9
15
  this._updateNumbers = new Map();
10
16
  }
11
- // Zero (an unset key) asks the server for everything of that type - what a first read sends for every key.
17
+ // Zero (an unset key) requests everything of that type from the server. A first read sends zero
18
+ // for every key.
12
19
  getUpdateNumber(entityTypeId) {
13
20
  return this._updateNumbers.get(this.checkKey(entityTypeId)) ?? 0n;
14
21
  }
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.
22
+ // Raises the cursor for this entity type and returns whether it moved. Tombstones count too,
23
+ // because skipping one would refetch it for ever.
24
+ // A number that is not higher is ignored as stale or redelivered.
17
25
  raiseUpdateNumber(entityTypeId, updateNumber) {
18
26
  const key = this.checkKey(entityTypeId);
19
27
  if (updateNumber <= (this._updateNumbers.get(key) ?? 0n))
package/dist/Tax.js CHANGED
@@ -5,12 +5,12 @@ import { taxRateFromScaledValue } from "./Constants";
5
5
  //
6
6
  // Taxes are included in the prices a customer sees: a product's in its package prices, the
7
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.
8
+ // CartEditor takes them out on the way in and ShoppingListItem.unitPriceWithTaxes adds them back
9
+ // for display. A Tax holds no amount. The amounts are on a line item (ShoppingListItem.taxAmounts).
10
10
  export default class Tax {
11
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.
12
+ // a tax as its raw code, which is unique only within a country (that of the business the tax is
13
+ // on), so the type is made here from the two, as Java TaxType.createId does.
14
14
  static fromScaledValues(countryCode, taxCode, scaledRate) {
15
15
  return new Tax(createTaxType(countryCode, taxCode), taxRateFromScaledValue(scaledRate));
16
16
  }
@@ -22,8 +22,8 @@ export default class Tax {
22
22
  return this._type;
23
23
  }
24
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.
25
+ // TaxType.createId(country, taxCode), with the country in its high bits, so the country can be
26
+ // read from the value itself. Zero for TaxType.UNSPECIFIED_TAX_TYPE, as no country's code is zero.
27
27
  get countryCode() {
28
28
  return countryCodeOf(this._type);
29
29
  }
@@ -36,9 +36,9 @@ export default class Tax {
36
36
  return this._rate;
37
37
  }
38
38
  }
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.
39
+ // The total of one type of tax across a shopping list: the line a bill shows for it, "GST 5.25".
40
+ // Several taxes of one type, on one item or on many, are added into one. It is already part of the
41
+ // list's total and is not added to it.
42
42
  export class TaxAmount {
43
43
  constructor(_type, _amount) {
44
44
  this._type = _type;
@@ -47,7 +47,8 @@ export class TaxAmount {
47
47
  get type() {
48
48
  return this._type;
49
49
  }
50
- // 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 but none are due), and a bill shows no line
51
+ // for it
51
52
  get amount() {
52
53
  return this._amount;
53
54
  }
package/dist/TaxHelper.js CHANGED
@@ -10,11 +10,12 @@ import { taxRateToScaledValue } from "./Constants";
10
10
  // (5% is 500). Floating point would round some taxes the other way. Amounts are bigint because one
11
11
  // can reach 2^44, and multiplying by a rate overflows 2^53.
12
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.
13
+ // Math.round). Each tax is rounded separately, because each is shown and paid separately.
14
14
  class TaxHelper {
15
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.
16
+ // not greater than zero is no tax. Java rejects one (validateTaxes) and drops one that arrives
17
+ // (getTaxCodesAndRates), so here it becomes zero, which keeps the indexes of the taxes and their
18
+ // amounts aligned.
18
19
  static scaledRatesOf(taxes) {
19
20
  return taxes.map((tax) => Number.isFinite(tax.rate) && tax.rate > 0 ? taxRateToScaledValue(tax.rate) : 0);
20
21
  }
@@ -43,17 +44,17 @@ class TaxHelper {
43
44
  result += TaxHelper.calculateTaxAmount(taxBase, taxRate);
44
45
  return result;
45
46
  }
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.
47
+ // The inverse of addTaxes: the amount that gives this amount with taxes. Each tax is rounded
48
+ // separately, so some amounts with taxes cannot be reached. In that case this returns the amount
49
+ // for which addTaxes comes closest (the lower one on a tie). So
50
+ // removeTaxes(addTaxes(amount, rates), rates) is always amount.
50
51
  static removeTaxes(amountWithTaxes, taxRates) {
51
52
  if (amountWithTaxes <= 0n || taxRates.length === 0)
52
53
  return amountWithTaxes;
53
54
  // Ignoring rounding, the amount with taxes is the amount multiplied by (1 + the total rate), so
54
55
  // dividing by that estimates the amount. Rounding moves each tax by at most half a unit and the
55
56
  // 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.
57
+ // looks as far). Check every amount within that distance.
57
58
  const estimate = TaxHelper.divideAndRound(amountWithTaxes * TaxHelper.TAX_RATE_FACTOR, TaxHelper.TAX_RATE_FACTOR + BigInt(TaxHelper.calculateTotalTaxRate(taxRates)));
58
59
  const reach = BigInt(taxRates.length);
59
60
  const first = estimate > reach ? estimate - reach : 0n;
package/dist/TaxType.js CHANGED
@@ -1,11 +1,11 @@
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
2
  // code, unique only within a country, and ProtoMap combines it with the country of the business.
3
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).
4
+ // so each is globally unique. countryCodeOf and taxCodeOf split a value into its parts.
5
+ // Copied by hand from Java's TaxType. Java's TaxTypeIdsTest
6
+ // (repository/test/com/merabills/repository/entities/helpers) pins the values: it fails when GST or
7
+ // the country shift changes or a type is added. A tax added there must be added here with the same
8
+ // value. This library's tests pin the same values (Values.test.ts).
9
9
  export var TaxType;
10
10
  (function (TaxType) {
11
11
  // No country's numeric code is 0, so this cannot collide with a real type
@@ -13,7 +13,8 @@ export var TaxType;
13
13
  // India (country code 356): Goods and Services Tax (tax code 0)
14
14
  TaxType[TaxType["GST"] = 91136] = "GST";
15
15
  })(TaxType || (TaxType = {}));
16
- // Bits of a TaxType value the raw tax code takes, below the country - Java TaxType.COUNTRY_CODE_SHIFT
16
+ // The low bits of a TaxType value that hold the raw tax code, below the country (Java
17
+ // TaxType.COUNTRY_CODE_SHIFT)
17
18
  const TAX_CODE_BITS = 8;
18
19
  const TAX_CODE_MASK = (1 << TAX_CODE_BITS) - 1;
19
20
  // Java TaxType.createId: the country in the high bits, the raw tax code in the low eight. Not
@@ -1,7 +1,7 @@
1
1
  import Entity from "./Entity";
2
- // The customer: whose orders and cart the business sees. Named for the database it lives in
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.
2
+ // The customer, whose orders and cart the business sees. Named for the database it lives in (the
3
+ // profile database, not the business one), because a signed-out storefront has no user but still
4
+ // shows its catalog. 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 customer's delivery addresses, and which is the default. Also resolves
25
- // Order.deliveryAddressId, an id into this list.
24
+ // The customer's delivery addresses, and which is the default. Order.deliveryAddressId is an id
25
+ // into this list.
26
26
  get addresses() {
27
27
  return this._addresses;
28
28
  }
@@ -30,8 +30,8 @@ export default class UserProfile extends Entity {
30
30
  get preferredLanguageCode() {
31
31
  return this._preferredLanguageCode;
32
32
  }
33
- // The two names with a space between them, and just the first name when there is no last name.
34
- // Computed here rather than sent.
33
+ // The first and last names separated by a space, or the first name alone when there is no last
34
+ // name. Computed here, not sent.
35
35
  get formattedName() {
36
36
  if (this._lastName === null || this._lastName.length === 0)
37
37
  return this._firstName;
@@ -1,9 +1,10 @@
1
1
  import { Constants as proto_Constants } from "./generated/Constants";
2
2
  import { InvalidArgumentException } from "./Exceptions";
3
- // Days and hours a business works. Absence is the permissive value throughout: no WorkingDays
4
- // means every day (ALL_DAYS), and a day with no hours or ranges is worked all day.
5
- // Hours are minutes after local midnight in timeZone, so a caller asking about a moment reads it
6
- // in that zone first, as a day and a minute.
3
+ // Days and hours a business works. Missing hours mean open: no WorkingDays means every day
4
+ // (ALL_DAYS), and a day with no hours or ranges is worked all day. A day missing from days is not
5
+ // worked.
6
+ // Hours are minutes after local midnight in timeZone, so to ask about a moment, convert it to
7
+ // that zone first, as a day and a minute.
7
8
  class WorkingDays {
8
9
  constructor(_days, _timeZone, _workingHours) {
9
10
  this._days = _days;
@@ -17,12 +18,12 @@ class WorkingDays {
17
18
  }
18
19
  // The IANA ID of the zone the hours are in, for example "Asia/Kolkata", never an abbreviation
19
20
  // like "IST". Null when none was specified (settings saved before the protocol carried one, and
20
- // ALL_DAYS). The hours then cannot be placed on a clock, and only a caller that knows the
21
- // business's zone can read a moment into them.
21
+ // ALL_DAYS). The hours then cannot be placed on a clock. Only a caller that knows the
22
+ // business's zone can convert a moment to a day and a minute to compare with them.
22
23
  get timeZone() {
23
24
  return this._timeZone;
24
25
  }
25
- // Indexed as days is. Shorter than days, or empty, is normal. See the class comment for what a
26
+ // Indexed like days. Shorter than days, or empty, is normal. See the class comment for what a
26
27
  // missing entry means.
27
28
  get workingHours() {
28
29
  return this._workingHours;
@@ -37,8 +38,9 @@ class WorkingDays {
37
38
  WorkingDays.checkDayOfWeek(dayOfWeek);
38
39
  return this._days[dayOfWeek] === true;
39
40
  }
40
- // The hours of a worked day, or null when that day is worked all day or not at all. A UI showing
41
- // opening times tests the day first, then treats null as all day.
41
+ // The hours of a day, or null when the day has no hours entry or its hours cover the whole day.
42
+ // This does not check whether the day is worked: a UI showing opening times tests isWorkingDay
43
+ // first, then treats null as all day.
42
44
  getWorkingHours(dayOfWeek) {
43
45
  WorkingDays.checkDayOfWeek(dayOfWeek);
44
46
  if (dayOfWeek >= this._workingHours.length)
@@ -54,8 +56,8 @@ class WorkingDays {
54
56
  throw new InvalidArgumentException(`Not a day of the week: ${dayOfWeek}`);
55
57
  }
56
58
  }
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
+ // The value for an absent WorkingDays: the business works every day, all day long. It has no
60
+ // hours, so it has no time zone.
59
61
  WorkingDays.ALL_DAYS = new WorkingDays(Object.freeze([true, true, true, true, true, true, true]), null, Object.freeze([]));
60
62
  export default WorkingDays;
61
63
  // The worked hours of a single day, as ranges.
@@ -63,8 +65,8 @@ export class WorkingHours {
63
65
  constructor(_ranges) {
64
66
  this._ranges = _ranges;
65
67
  }
66
- // Ordered by openMinute, non-overlapping and non-touching: a range that closes when the next
67
- // opens arrives as one unbroken range.
68
+ // Ordered by openMinute, non-overlapping and non-touching: ranges where one closes when the
69
+ // next opens arrive merged into one.
68
70
  get ranges() {
69
71
  return this._ranges;
70
72
  }
@@ -93,8 +95,8 @@ export class WorkingHoursRange {
93
95
  constructor(_openMinute, _closeMinute) {
94
96
  this._openMinute = _openMinute;
95
97
  this._closeMinute = _closeMinute;
96
- // Validated on the way in: a payload that breaks protocol rules fails the sync rather than
97
- // producing opening times a customer would trust as fact.
98
+ // Validated on construction: a payload that breaks protocol rules fails the sync instead of
99
+ // producing opening times that a customer would trust.
98
100
  if (!Number.isInteger(_openMinute) ||
99
101
  _openMinute < 0 ||
100
102
  _openMinute >= proto_Constants.MINUTES_PER_DAY)
@@ -104,12 +106,13 @@ export class WorkingHoursRange {
104
106
  _closeMinute > proto_Constants.MINUTES_PER_DAY)
105
107
  throw new InvalidArgumentException(`Work cannot stop at minute ${_closeMinute} having started at ${_openMinute}`);
106
108
  }
107
- // Worked. From 0 to Constants.MINUTES_PER_DAY - 1.
109
+ // The first minute that is worked, from 0 to Constants.MINUTES_PER_DAY - 1.
108
110
  get openMinute() {
109
111
  return this._openMinute;
110
112
  }
111
- // Not worked; MINUTES_PER_DAY means work stops at midnight. Always greater than openMinute. A
112
- // range never spans midnight, so overnight work is two ranges on two days.
113
+ // The first minute that is not worked. MINUTES_PER_DAY means work stops at midnight. Always
114
+ // greater than openMinute. A range never spans midnight, so overnight work is two ranges on two
115
+ // days.
113
116
  get closeMinute() {
114
117
  return this._closeMinute;
115
118
  }
package/dist/index.js CHANGED
@@ -1,7 +1,10 @@
1
- // The public surface of this library. Excludes Attachment (UIs get Photo instead), the sync layer's internal cursors,
2
- // the auth tokens (except BusinessLink.linkToken, which the protocol hands to a client), ProtoMap (the wire boundary, except buildAuthTokensBytes),
3
- // the scaled-value conversions of Constants.ts (every getter is already unscaled),
4
- // and TaxHelper (tax arithmetic in scaled values; a UI reads a tax off a ShoppingListItem, ShoppingList or CartLine).
1
+ // The public surface of this library. Not exported:
2
+ // - Attachment (a UI gets Photo instead) and the sync layer's internal cursors.
3
+ // - The auth tokens, except BusinessLink.linkToken, which the server sends to a client.
4
+ // - ProtoMap, the wire boundary, except buildAuthTokensBytes.
5
+ // - The scaled-value conversions of Constants.ts (every getter is already unscaled).
6
+ // - TaxHelper, which does tax arithmetic in scaled values. A UI reads a tax off a ShoppingListItem,
7
+ // ShoppingList or CartLine.
5
8
  export { default as Model } from "./Model";
6
9
  export * as ContainerNames from "./ContainerNames";
7
10
  export { buildAuthTokensBytes } from "./ProtoMap";
@@ -45,14 +48,14 @@ export { default as ShoppingList, ItemChange, ShoppingListItem } from "./Shoppin
45
48
  export { default as Tax, TaxAmount } from "./Tax";
46
49
  export { default as WorkingDays, WorkingHours, WorkingHoursRange } from "./WorkingDays";
47
50
  // The enums of the protocol, the limits it obeys, and how an id is read. The price list a
48
- // storefront sells from is exported because a UI naming it on a cart it is building needs it,
49
- // while Catalog.priceList is null until the catalog has arrived.
51
+ // storefront sells from is exported because a UI that builds a cart must name it, and
52
+ // Catalog.priceList is null until the catalog has arrived.
50
53
  export * from "./Enums";
51
54
  export { Constants, MERABILLS_ONLINE_PRICE_LIST_ID, buildPriceListId } from "./Constants";
52
55
  export { default as Ids } from "./Ids";
53
56
  // Failures. The two in Exceptions.ts are programming errors. The rest are what a call fails with
54
- // and are meant to be caught: ServiceException for anything the server refused,
55
- // NetworkConnectionException for a call that never got an answer.
57
+ // and are meant to be caught: ServiceException for an error response from the server,
58
+ // NetworkConnectionException for a call that got no response.
56
59
  export { IllegalOperationException, InvalidArgumentException } from "./Exceptions";
57
60
  export { BadRequestException, ConflictException, InternalServerException, MethodNotAllowedException, NetworkConnectionException, NotFoundException, ServiceException, ServiceUnavailableException, UnauthorizedException, UpgradeRequiredException, } from "./ServiceExceptions";
58
61
  // The mock database: a Database over a Map, so a caller can pair it with any Service. MockService