@tallyui/pos 3.0.0-next.1 → 3.0.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/index.d.ts +30 -2
- package/dist/index.js +72 -20
- package/package.json +6 -6
- package/src/index.ts +2 -2
- package/src/outbox/index.ts +2 -0
- package/src/outbox/order-outbox.ts +8 -4
- package/src/outbox/register-outbox.ts +4 -3
- package/src/outbox/types.ts +5 -2
- package/src/outbox/use-register-outbox.ts +55 -0
- package/src/store-settings/use-store-settings.ts +9 -1
package/dist/index.d.ts
CHANGED
|
@@ -210,6 +210,9 @@ type StoreSettingsState = {
|
|
|
210
210
|
* The capabilities are read beside the settings, before `ready`, so each resolve emits the settings once, with the
|
|
211
211
|
* rounding already known: no later change of `settings` holds a sale.
|
|
212
212
|
* Unknown rounding keeps settings unresolved and retries by itself; show "Can't reach the store's settings yet. Retrying…" while `nextRetryAt` is set.
|
|
213
|
+
* A till-class or invalid-credentials sign-in error is not retried by itself, so the app prompts to sign in or update the till.
|
|
214
|
+
* An app shows a sign-in prompt when the `error` state's error is a `ConnectorUnauthorizedError` with `code: 'unauthorized'`
|
|
215
|
+
* or a `SignInError` with `code: 'invalid_credentials'`, and an update prompt for `code: 'till_update_required'`.
|
|
213
216
|
*
|
|
214
217
|
* ```tsx
|
|
215
218
|
* const store = useStoreSettings({ connector, context, loadChoice, saveChoice });
|
|
@@ -1950,14 +1953,18 @@ interface OutboxState {
|
|
|
1950
1953
|
/** Set while the store (not offline) has kept failing some orders for 15 minutes, each on its own clock. Those
|
|
1951
1954
|
* orders stay pending and keep retrying. `orders` has each order's own entry: its `since` is when its clock
|
|
1952
1955
|
* would have started had there been no offline gaps (now minus its answered time; while offline, as of the
|
|
1953
|
-
* moment the clock paused), and its latest `reason`. `since` is the earliest of theirs
|
|
1956
|
+
* moment the clock paused), and its latest `reason`. `since` is the earliest of theirs, virtual for the threshold;
|
|
1957
|
+
* `reason` the latest. `firstFailedAt` is the earliest real wall-clock first failure, only when every stuck clock
|
|
1958
|
+
* has one. Each order entry has its own real wall-clock `firstFailedAt`. Both are in memory only, absent after restart. */
|
|
1954
1959
|
stuck?: {
|
|
1955
1960
|
commandIds: string[];
|
|
1956
1961
|
since: number;
|
|
1962
|
+
firstFailedAt?: number;
|
|
1957
1963
|
reason: string;
|
|
1958
1964
|
orders: {
|
|
1959
1965
|
commandId: string;
|
|
1960
1966
|
since: number;
|
|
1967
|
+
firstFailedAt?: number;
|
|
1961
1968
|
reason: string;
|
|
1962
1969
|
}[];
|
|
1963
1970
|
};
|
|
@@ -2043,6 +2050,27 @@ interface RegisterOutbox {
|
|
|
2043
2050
|
}
|
|
2044
2051
|
declare function createRegisterOutbox(options: RegisterOutboxOptions): RegisterOutbox;
|
|
2045
2052
|
|
|
2053
|
+
interface UseRegisterOutboxOptions {
|
|
2054
|
+
/** `register_commands` (see `registerCommandCollection()`); `null` while it opens or when there's no store. A new collection (a store switch) restarts the outbox. */
|
|
2055
|
+
commands: RegisterCommandCollection | null;
|
|
2056
|
+
/** Builds the transport; called when the outbox is created: on each new collection or device id (twice under React StrictMode's double effect, which stops the first outbox). */
|
|
2057
|
+
transport(): CommandTransport<RegisterCommandEnvelope>;
|
|
2058
|
+
/** The device id sent on every command. A change restarts the outbox. */
|
|
2059
|
+
deviceId: string;
|
|
2060
|
+
/** Passed through to createRegisterOutbox (same meaning). The latest function is called each time, so a new one does not restart the outbox. */
|
|
2061
|
+
isEnabled?: () => boolean;
|
|
2062
|
+
/** Called with each result. The latest function is called each time; a new one does not restart the outbox. */
|
|
2063
|
+
onResult?: RegisterOutboxOptions['onResult'];
|
|
2064
|
+
backendNotFound?: RegisterOutboxOptions['backendNotFound'];
|
|
2065
|
+
}
|
|
2066
|
+
interface UseRegisterOutboxResult {
|
|
2067
|
+
/** The outbox state, or idle ({ pending: 0, sending: false }) while there is no collection. */
|
|
2068
|
+
state: OutboxState;
|
|
2069
|
+
/** Sends pending till updates; does nothing while there is no collection. */
|
|
2070
|
+
flush(): Promise<void>;
|
|
2071
|
+
}
|
|
2072
|
+
declare function useRegisterOutbox(options: UseRegisterOutboxOptions): UseRegisterOutboxResult;
|
|
2073
|
+
|
|
2046
2074
|
declare const outboxLogger: Logger;
|
|
2047
2075
|
|
|
2048
2076
|
interface UseOrderOutboxOptions {
|
|
@@ -2249,4 +2277,4 @@ declare function planLegs(plan: TenderPlan, rowsSinceFrom: {
|
|
|
2249
2277
|
};
|
|
2250
2278
|
};
|
|
2251
2279
|
|
|
2252
|
-
export { type Actor, type AddLineInput, type AppliedDiscount, type BackendNotFound, type BasketLine, CartError, type CashMovement, type CashMovementCollection, type CatalogueEntry, type Closure, type ClosureCollection, type ClosureContext, type ClosureScope, type CommandTransport, type Correction, CurrencyProvider, type CurrencyProviderProps, type CustomerSummary, DISCOUNTS_UNSUPPORTED, type Discount, type DisplayLine, type DisplayTotals, type FinalizeOptions, type HttpTransportOptions, type LedgerRow, type LineItem, type LineTaxLine, type LogEntry, type LogLevel, type LogSink, type Logger, MAX_TENDER_MINOR, MICROS_PER_MINOR, type Movement$1 as Movement, type MovementType, type Order, type OrderBuilder, type OrderBuilderOptions, OrderContentMismatchError, type OrderManager, type OrderManagerOptions, type OrderOutbox, type OrderOutboxOptions, type OrderTaxTotals, type OutboxState, type ParkedOrderSummary, type Payment, type PaymentMethod, type PaymentTransport, type PlanLeg, type PosOrder, type PosOrderLine, type PosOrderLocalWarning, PosOrderOpenClosedError, type PosOrderPayment, type PosOrderServerFailures, type PosOrderSyncStatus, type RateTaxLine, type ReceiptConfig, type ReceiptData, type ReceiptLineItem, type RecordedFigures, RegisterApprovalRequiredError, type RegisterBucket, RegisterCloseIncompleteError, type RegisterCommand, type RegisterCommandCollection, type RegisterCounters, type RegisterDocument, type RegisterFact, type RegisterHost, RegisterIdInvalidError, RegisterMovementAmountError, RegisterMovementReasonError, RegisterMovementStrandedError, type RegisterOutbox, type RegisterOutboxOptions, type RegisterSession, RegisterSessionAlreadyOpenError, RegisterSessionClosedError, type RegisterSessionCollection, RegisterSessionRequiredError, type RegisterStore, RegisterTenderInProgressError, type Repository, type ResolveStoreSettingsOptions, SALE_SAVING, type SaleStage, type SentOrder, type SplitTab, type StoreSettingsResolution, type StoreSettingsState, TAX_ROUNDING_MIXED_NOTE, type TaxContext, type TaxLineInput, TaxProvider, type TaxProviderProps, type TaxRateMap, type TenderAction, type TenderKey, type TenderLineId, type TenderPlan, type TenderState, type TenderView, type TransportOutcome, UnsupportedOrderVersionError, type UseOrderOutboxOptions, type UseOrderOutboxResult, type UseRegisterSessionOptions, activePlan, addEntryToCart, addPosOrderCollection, advancePerpetual, allocateOrderDiscount, appliedMinor, backToSelling, bindRegister, buildClosureDocument, buildReceiptData, buildXReportDocument, callbackSink, cashMovementSchema, catalogueEntries, changeMinor, clampClosureScope, closeNeedsApproval, closeSession, closureCommand, closureSchema, computeOrderTax, consoleSink, countVariance, createBackendNotFound, createHttpCommandTransport, createLogger, createOrderBuilder, createOrderManager, createOrderOutbox, createRegisterOutbox, createRepository, denominationTotal, denominations, deriveExpected, deriveSettled, ensureRegister, evenSplitShareMinor, exportCsv, finalizeOrder, findEntryByCode, formatClosureDate, getBoundRegisterId, getDeviceId, initTenderState, initialTenderState, isServerDecimal, labelKeys, minorToDecimal, mintClosureNumber, mintUuid, movementCommand, movementFieldError, needsAttention, nextSaleCounter, normalizeAmount, observeRegister$, openSession, openSessionSelector, outboxLogger, overThreshold, parseMinor, planLegs, posOrderCollection, posOrderSchema, posOrdersLogger, quickTenderedAmounts, ratePpmFromPercent, readBoundRegister, readRegister, reconcileRegisterCommands, recordMovement, recordRegisterFact, registerCommandCollection, registerCommandSchema, registerCommandsLogger, registerFactsLogger, registerSessionCollection, registerSessionSchema, requireOpenSession, resolveStoreSettings, roundMicrosToMinor, saleLogger, sameSale, searchProducts, selectClosureRows, sessionOpenCommand, sessionTransitionCommand, stampSession, startCounting, stockOverlay$, stockOverlayAsOf$, taxFiguresForBasket, taxLinesByRate, taxLogger, taxMicros, taxProviderProps, tenderReducer, toOrderCreateEnvelope, unbindRegister, useCurrencyCode, useCurrencyFormatter, useOrderOutbox, useRegisterSession, useSale, useStoreSettings, useTax, uuidv7, validAmount, varianceText, variantPriceLabel, voidMovement, withPricingContext, writeClosure };
|
|
2280
|
+
export { type Actor, type AddLineInput, type AppliedDiscount, type BackendNotFound, type BasketLine, CartError, type CashMovement, type CashMovementCollection, type CatalogueEntry, type Closure, type ClosureCollection, type ClosureContext, type ClosureScope, type CommandTransport, type Correction, CurrencyProvider, type CurrencyProviderProps, type CustomerSummary, DISCOUNTS_UNSUPPORTED, type Discount, type DisplayLine, type DisplayTotals, type FinalizeOptions, type HttpTransportOptions, type LedgerRow, type LineItem, type LineTaxLine, type LogEntry, type LogLevel, type LogSink, type Logger, MAX_TENDER_MINOR, MICROS_PER_MINOR, type Movement$1 as Movement, type MovementType, type Order, type OrderBuilder, type OrderBuilderOptions, OrderContentMismatchError, type OrderManager, type OrderManagerOptions, type OrderOutbox, type OrderOutboxOptions, type OrderTaxTotals, type OutboxState, type ParkedOrderSummary, type Payment, type PaymentMethod, type PaymentTransport, type PlanLeg, type PosOrder, type PosOrderLine, type PosOrderLocalWarning, PosOrderOpenClosedError, type PosOrderPayment, type PosOrderServerFailures, type PosOrderSyncStatus, type RateTaxLine, type ReceiptConfig, type ReceiptData, type ReceiptLineItem, type RecordedFigures, RegisterApprovalRequiredError, type RegisterBucket, RegisterCloseIncompleteError, type RegisterCommand, type RegisterCommandCollection, type RegisterCounters, type RegisterDocument, type RegisterFact, type RegisterHost, RegisterIdInvalidError, RegisterMovementAmountError, RegisterMovementReasonError, RegisterMovementStrandedError, type RegisterOutbox, type RegisterOutboxOptions, type RegisterSession, RegisterSessionAlreadyOpenError, RegisterSessionClosedError, type RegisterSessionCollection, RegisterSessionRequiredError, type RegisterStore, RegisterTenderInProgressError, type Repository, type ResolveStoreSettingsOptions, SALE_SAVING, type SaleStage, type SentOrder, type SplitTab, type StoreSettingsResolution, type StoreSettingsState, TAX_ROUNDING_MIXED_NOTE, type TaxContext, type TaxLineInput, TaxProvider, type TaxProviderProps, type TaxRateMap, type TenderAction, type TenderKey, type TenderLineId, type TenderPlan, type TenderState, type TenderView, type TransportOutcome, UnsupportedOrderVersionError, type UseOrderOutboxOptions, type UseOrderOutboxResult, type UseRegisterOutboxOptions, type UseRegisterOutboxResult, type UseRegisterSessionOptions, activePlan, addEntryToCart, addPosOrderCollection, advancePerpetual, allocateOrderDiscount, appliedMinor, backToSelling, bindRegister, buildClosureDocument, buildReceiptData, buildXReportDocument, callbackSink, cashMovementSchema, catalogueEntries, changeMinor, clampClosureScope, closeNeedsApproval, closeSession, closureCommand, closureSchema, computeOrderTax, consoleSink, countVariance, createBackendNotFound, createHttpCommandTransport, createLogger, createOrderBuilder, createOrderManager, createOrderOutbox, createRegisterOutbox, createRepository, denominationTotal, denominations, deriveExpected, deriveSettled, ensureRegister, evenSplitShareMinor, exportCsv, finalizeOrder, findEntryByCode, formatClosureDate, getBoundRegisterId, getDeviceId, initTenderState, initialTenderState, isServerDecimal, labelKeys, minorToDecimal, mintClosureNumber, mintUuid, movementCommand, movementFieldError, needsAttention, nextSaleCounter, normalizeAmount, observeRegister$, openSession, openSessionSelector, outboxLogger, overThreshold, parseMinor, planLegs, posOrderCollection, posOrderSchema, posOrdersLogger, quickTenderedAmounts, ratePpmFromPercent, readBoundRegister, readRegister, reconcileRegisterCommands, recordMovement, recordRegisterFact, registerCommandCollection, registerCommandSchema, registerCommandsLogger, registerFactsLogger, registerSessionCollection, registerSessionSchema, requireOpenSession, resolveStoreSettings, roundMicrosToMinor, saleLogger, sameSale, searchProducts, selectClosureRows, sessionOpenCommand, sessionTransitionCommand, stampSession, startCounting, stockOverlay$, stockOverlayAsOf$, taxFiguresForBasket, taxLinesByRate, taxLogger, taxMicros, taxProviderProps, tenderReducer, toOrderCreateEnvelope, unbindRegister, useCurrencyCode, useCurrencyFormatter, useOrderOutbox, useRegisterOutbox, useRegisterSession, useSale, useStoreSettings, useTax, uuidv7, validAmount, varianceText, variantPriceLabel, voidMovement, withPricingContext, writeClosure };
|
package/dist/index.js
CHANGED
|
@@ -324,6 +324,7 @@ function taxProviderProps(settings) {
|
|
|
324
324
|
|
|
325
325
|
// src/store-settings/use-store-settings.ts
|
|
326
326
|
import { useCallback as useCallback2, useEffect, useRef, useState } from "react";
|
|
327
|
+
import { errorKind, SignInError } from "@tallyui/core";
|
|
327
328
|
var LOADING = { state: "loading" };
|
|
328
329
|
var StoreCapabilitiesUnavailableError = class extends Error {
|
|
329
330
|
constructor(cause) {
|
|
@@ -343,6 +344,7 @@ function readTaxRounding({ connector, context }) {
|
|
|
343
344
|
return capabilities.taxRounding;
|
|
344
345
|
},
|
|
345
346
|
(cause) => {
|
|
347
|
+
if (errorKind(cause) === "till" || cause instanceof SignInError && cause.code === "invalid_credentials") throw cause;
|
|
346
348
|
throw new StoreCapabilitiesUnavailableError(cause);
|
|
347
349
|
}
|
|
348
350
|
);
|
|
@@ -3563,7 +3565,7 @@ function useSale(settings, opts) {
|
|
|
3563
3565
|
const subscription = rendered.order$.subscribe(setOrder);
|
|
3564
3566
|
return () => subscription.unsubscribe();
|
|
3565
3567
|
}, [rendered]);
|
|
3566
|
-
const
|
|
3568
|
+
const idle3 = stage.kind === "cart" && !order.lineItems.length;
|
|
3567
3569
|
useLayoutEffect(() => {
|
|
3568
3570
|
const live = builderNow.current.getSnapshot();
|
|
3569
3571
|
const liveIdle = stageNow.current.kind === "cart" && !live.lineItems.length && !inFlight.current && !pending.current;
|
|
@@ -3650,7 +3652,7 @@ function useSale(settings, opts) {
|
|
|
3650
3652
|
order,
|
|
3651
3653
|
stage,
|
|
3652
3654
|
error,
|
|
3653
|
-
idle:
|
|
3655
|
+
idle: idle3,
|
|
3654
3656
|
/** A completion is pending (see `complete()`): the sale is locked, and the UI offers Retry. */
|
|
3655
3657
|
saving,
|
|
3656
3658
|
/** The failed save's order is confirmed stored (see `isStored`): the UI also offers Continue (`continueSale()`). */
|
|
@@ -3970,8 +3972,14 @@ function createOrderOutbox(options) {
|
|
|
3970
3972
|
return {
|
|
3971
3973
|
commandIds: stuck.map(([id2]) => id2),
|
|
3972
3974
|
since: Math.min(...stuck.map(([, clock]) => clock.since)),
|
|
3975
|
+
...stuck.every(([, clock]) => clock.firstFailedAt !== void 0) ? { firstFailedAt: Math.min(...stuck.map(([, clock]) => clock.firstFailedAt)) } : {},
|
|
3973
3976
|
reason: stuck.reduce((latest, entry) => entry[1].seq > latest[1].seq ? entry : latest)[1].reason,
|
|
3974
|
-
orders: stuck.map(([commandId, { since, reason }]) => ({
|
|
3977
|
+
orders: stuck.map(([commandId, { since, firstFailedAt, reason }]) => ({
|
|
3978
|
+
commandId,
|
|
3979
|
+
since,
|
|
3980
|
+
reason,
|
|
3981
|
+
...firstFailedAt === void 0 ? {} : { firstFailedAt }
|
|
3982
|
+
}))
|
|
3975
3983
|
};
|
|
3976
3984
|
}
|
|
3977
3985
|
function isDue(id2) {
|
|
@@ -4023,7 +4031,7 @@ function createOrderOutbox(options) {
|
|
|
4023
4031
|
}
|
|
4024
4032
|
function serverFailed(orders, alone, reason, retryAfterMs) {
|
|
4025
4033
|
const at2 = now();
|
|
4026
|
-
for (const { commandId } of orders) clocks.set(commandId, {
|
|
4034
|
+
for (const { commandId } of orders) clocks.set(commandId, { ...clocks.get(commandId) ?? { since: at2, firstFailedAt: at2 }, seq: ++failureSeq, reason });
|
|
4027
4035
|
const id2 = orders[0].commandId;
|
|
4028
4036
|
if (alone === "isolated") {
|
|
4029
4037
|
isolate(id2, reason, retryAfterMs);
|
|
@@ -4428,13 +4436,14 @@ function createRegisterOutbox(options) {
|
|
|
4428
4436
|
return {
|
|
4429
4437
|
commandIds: stuck.map(([id2]) => id2),
|
|
4430
4438
|
since: Math.min(...stuck.map(([, clock]) => clock.since)),
|
|
4439
|
+
firstFailedAt: Math.min(...stuck.map(([, clock]) => clock.firstFailedAt)),
|
|
4431
4440
|
reason: stuck.reduce((latest, entry) => entry[1].seq > latest[1].seq ? entry : latest)[1].reason,
|
|
4432
|
-
orders: stuck.map(([commandId, { since, reason }]) => ({ commandId, since, reason }))
|
|
4441
|
+
orders: stuck.map(([commandId, { since, firstFailedAt, reason }]) => ({ commandId, since, firstFailedAt, reason }))
|
|
4433
4442
|
};
|
|
4434
4443
|
}
|
|
4435
4444
|
function serverFailed(commands, reason) {
|
|
4436
4445
|
const at2 = now();
|
|
4437
|
-
for (const { commandId } of commands) clocks.set(commandId, {
|
|
4446
|
+
for (const { commandId } of commands) clocks.set(commandId, { ...clocks.get(commandId) ?? { since: at2, firstFailedAt: at2 }, seq: ++failureSeq, reason });
|
|
4438
4447
|
}
|
|
4439
4448
|
async function updateState(patch = {}) {
|
|
4440
4449
|
const pending = await countFresh(collection, { syncStatus: "pending" });
|
|
@@ -4632,28 +4641,70 @@ function createRegisterOutbox(options) {
|
|
|
4632
4641
|
};
|
|
4633
4642
|
}
|
|
4634
4643
|
|
|
4635
|
-
// src/outbox/use-
|
|
4636
|
-
import { useEffect as useEffect4,
|
|
4644
|
+
// src/outbox/use-register-outbox.ts
|
|
4645
|
+
import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
|
|
4637
4646
|
var idle = { pending: 0, sending: false };
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
const { storeKey, deviceId } = options;
|
|
4647
|
+
function useRegisterOutbox(options) {
|
|
4648
|
+
const { commands, deviceId } = options;
|
|
4641
4649
|
const latest = useRef4(options);
|
|
4642
4650
|
latest.current = options;
|
|
4643
4651
|
const current = useRef4(null);
|
|
4644
|
-
const openingError = useRef4(null);
|
|
4645
|
-
const loggedMismatches = useRef4(/* @__PURE__ */ new Set());
|
|
4646
|
-
const [orders, setOrders] = useState4(null);
|
|
4647
4652
|
const [state, setState] = useState4(idle);
|
|
4648
|
-
const [recent, setRecent] = useState4([]);
|
|
4649
|
-
const savesInFlight = useRef4(0);
|
|
4650
|
-
const [savesInFlightCount, setSavesInFlightCount] = useState4(0);
|
|
4651
4653
|
useEffect4(() => {
|
|
4654
|
+
setState(idle);
|
|
4655
|
+
if (!commands) return;
|
|
4656
|
+
const outbox = createRegisterOutbox({
|
|
4657
|
+
collection: commands,
|
|
4658
|
+
deviceId,
|
|
4659
|
+
transport: latest.current.transport(),
|
|
4660
|
+
isEnabled: latest.current.isEnabled ? () => latest.current.isEnabled?.() ?? true : void 0,
|
|
4661
|
+
onResult: latest.current.onResult ? async (command, result) => {
|
|
4662
|
+
await latest.current.onResult?.(command, result);
|
|
4663
|
+
} : void 0,
|
|
4664
|
+
backendNotFound: latest.current.backendNotFound
|
|
4665
|
+
});
|
|
4666
|
+
current.current = { commands, deviceId, outbox };
|
|
4667
|
+
const status = outbox.state$.subscribe(setState);
|
|
4668
|
+
outbox.start();
|
|
4669
|
+
return () => {
|
|
4670
|
+
outbox.stop();
|
|
4671
|
+
status.unsubscribe();
|
|
4672
|
+
current.current = null;
|
|
4673
|
+
setState(idle);
|
|
4674
|
+
};
|
|
4675
|
+
}, [commands, deviceId]);
|
|
4676
|
+
const opened = current.current;
|
|
4677
|
+
return {
|
|
4678
|
+
state: opened?.commands === commands && opened.deviceId === deviceId ? state : idle,
|
|
4679
|
+
async flush() {
|
|
4680
|
+
const active = current.current;
|
|
4681
|
+
if (active?.commands === commands && active.deviceId === deviceId) await active.outbox.flush();
|
|
4682
|
+
}
|
|
4683
|
+
};
|
|
4684
|
+
}
|
|
4685
|
+
|
|
4686
|
+
// src/outbox/use-order-outbox.ts
|
|
4687
|
+
import { useEffect as useEffect5, useMemo as useMemo4, useRef as useRef5, useState as useState5 } from "react";
|
|
4688
|
+
var idle2 = { pending: 0, sending: false };
|
|
4689
|
+
var noneStuck = [];
|
|
4690
|
+
function useOrderOutbox(options) {
|
|
4691
|
+
const { storeKey, deviceId } = options;
|
|
4692
|
+
const latest = useRef5(options);
|
|
4693
|
+
latest.current = options;
|
|
4694
|
+
const current = useRef5(null);
|
|
4695
|
+
const openingError = useRef5(null);
|
|
4696
|
+
const loggedMismatches = useRef5(/* @__PURE__ */ new Set());
|
|
4697
|
+
const [orders, setOrders] = useState5(null);
|
|
4698
|
+
const [state, setState] = useState5(idle2);
|
|
4699
|
+
const [recent, setRecent] = useState5([]);
|
|
4700
|
+
const savesInFlight = useRef5(0);
|
|
4701
|
+
const [savesInFlightCount, setSavesInFlightCount] = useState5(0);
|
|
4702
|
+
useEffect5(() => {
|
|
4652
4703
|
let active = true;
|
|
4653
4704
|
let dispose;
|
|
4654
4705
|
openingError.current = null;
|
|
4655
4706
|
setOrders(null);
|
|
4656
|
-
setState(
|
|
4707
|
+
setState(idle2);
|
|
4657
4708
|
setRecent([]);
|
|
4658
4709
|
if (!storeKey) return;
|
|
4659
4710
|
void latest.current.open(storeKey).then(async (store) => {
|
|
@@ -4691,7 +4742,7 @@ function useOrderOutbox(options) {
|
|
|
4691
4742
|
dispose?.();
|
|
4692
4743
|
};
|
|
4693
4744
|
}, [storeKey, deviceId]);
|
|
4694
|
-
|
|
4745
|
+
useEffect5(() => {
|
|
4695
4746
|
latest.current.onBusy?.(state.sending);
|
|
4696
4747
|
return () => latest.current.onBusy?.(false);
|
|
4697
4748
|
}, [state.sending]);
|
|
@@ -4700,7 +4751,7 @@ function useOrderOutbox(options) {
|
|
|
4700
4751
|
const stuckCommandIds = useMemo4(() => stuckKey ? stuckKey.split(",") : noneStuck, [stuckKey]);
|
|
4701
4752
|
return {
|
|
4702
4753
|
orders: ready ? orders : null,
|
|
4703
|
-
state: ready ? state :
|
|
4754
|
+
state: ready ? state : idle2,
|
|
4704
4755
|
recent: ready ? recent : [],
|
|
4705
4756
|
async flush() {
|
|
4706
4757
|
const opened = current.current;
|
|
@@ -5071,6 +5122,7 @@ export {
|
|
|
5071
5122
|
useCurrencyCode,
|
|
5072
5123
|
useCurrencyFormatter,
|
|
5073
5124
|
useOrderOutbox,
|
|
5125
|
+
useRegisterOutbox,
|
|
5074
5126
|
useRegisterSession,
|
|
5075
5127
|
useSale,
|
|
5076
5128
|
useStoreSettings,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tallyui/pos",
|
|
3
|
-
"version": "3.0.0
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shared POS business logic for Tally UI — tax, orders, payments, discounts, receipts",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -27,20 +27,20 @@
|
|
|
27
27
|
"directory": "packages/pos"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@tallyui/core": "^3.0.0
|
|
30
|
+
"@tallyui/core": "^3.0.0",
|
|
31
31
|
"react": ">=18",
|
|
32
32
|
"rxdb": "~17.5.0",
|
|
33
33
|
"rxjs": ">=7"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@tallyui/core": "3.0.0
|
|
36
|
+
"@tallyui/core": "3.0.0",
|
|
37
37
|
"@types/react": ">=18",
|
|
38
38
|
"react": "^19.2.4",
|
|
39
39
|
"rxdb": "17.5.0",
|
|
40
40
|
"rxjs": "7.8.2",
|
|
41
|
-
"@tallyui/connector-medusa": "3.0.0
|
|
42
|
-
"@tallyui/connector-woocommerce": "3.0.0
|
|
43
|
-
"@tallyui/database": "3.0.0
|
|
41
|
+
"@tallyui/connector-medusa": "3.0.0",
|
|
42
|
+
"@tallyui/connector-woocommerce": "3.0.0",
|
|
43
|
+
"@tallyui/database": "3.0.0"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsup",
|
package/src/index.ts
CHANGED
|
@@ -58,8 +58,8 @@ export type { SaleStage, CatalogueEntry } from './sale';
|
|
|
58
58
|
export { uuidv7, finalizeOrder, toOrderCreateEnvelope, UnsupportedOrderVersionError, posOrderSchema, posOrderCollection, addPosOrderCollection, PosOrderOpenClosedError, posOrdersLogger, getDeviceId, needsAttention, sameSale, OrderContentMismatchError } from './pos-order';
|
|
59
59
|
export type { PosOrderSyncStatus, PosOrderLine, PosOrderPayment, PosOrderLocalWarning, PosOrderServerFailures, PosOrder, FinalizeOptions } from './pos-order';
|
|
60
60
|
export { createHttpCommandTransport, createOrderOutbox, useOrderOutbox, outboxLogger } from './outbox';
|
|
61
|
-
export { createRegisterOutbox } from './outbox';
|
|
62
|
-
export type { RegisterOutboxOptions, RegisterOutbox } from './outbox';
|
|
61
|
+
export { createRegisterOutbox, useRegisterOutbox } from './outbox';
|
|
62
|
+
export type { RegisterOutboxOptions, RegisterOutbox, UseRegisterOutboxOptions, UseRegisterOutboxResult } from './outbox';
|
|
63
63
|
export { createBackendNotFound } from './outbox/backend-not-found';
|
|
64
64
|
export type { BackendNotFound } from './outbox/backend-not-found';
|
|
65
65
|
export type { HttpTransportOptions, OrderOutboxOptions, OrderOutbox, TransportOutcome, CommandTransport, OutboxState, UseOrderOutboxOptions, UseOrderOutboxResult } from './outbox';
|
package/src/outbox/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ export { createOrderOutbox } from './order-outbox';
|
|
|
4
4
|
export type { OrderOutboxOptions, OrderOutbox } from './order-outbox';
|
|
5
5
|
export { createRegisterOutbox } from './register-outbox';
|
|
6
6
|
export type { RegisterOutboxOptions, RegisterOutbox } from './register-outbox';
|
|
7
|
+
export { useRegisterOutbox } from './use-register-outbox';
|
|
8
|
+
export type { UseRegisterOutboxOptions, UseRegisterOutboxResult } from './use-register-outbox';
|
|
7
9
|
export type { TransportOutcome, CommandTransport, OutboxState } from './types';
|
|
8
10
|
export { useOrderOutbox, outboxLogger } from './use-order-outbox';
|
|
9
11
|
export type { UseOrderOutboxOptions, UseOrderOutboxResult } from './use-order-outbox';
|
|
@@ -104,8 +104,9 @@ export function createOrderOutbox(options: OrderOutboxOptions): OrderOutbox {
|
|
|
104
104
|
// clock (`pausedAt`), and the next response the store answers, of any kind and for any order, resumes them all,
|
|
105
105
|
// moving `since` on by the offline gap: a pause is about the connection. So `since` is when the clock would have
|
|
106
106
|
// started had there been no offline gaps: now minus its answered time (while paused, as of the pause). Progress,
|
|
107
|
-
// or the order leaving pending, clears it.
|
|
108
|
-
|
|
107
|
+
// or the order leaving pending, clears it. `firstFailedAt` is the wall-clock time of the clock's first answered
|
|
108
|
+
// failure; a pause never moves it, and a clock restored from stored `serverFailures` has none (it is memory only).
|
|
109
|
+
const clocks = new Map<string, { since: number; firstFailedAt?: number; pausedAt?: number; seq: number; reason: string }>();
|
|
109
110
|
let failureSeq = 0; // numbers failures, so `stuck` can name the latest reason
|
|
110
111
|
// Timer runs alternate between the batch turn (the batch, or the walk's probe) and the isolated turn (one due
|
|
111
112
|
// isolated order); a turn with nothing to send gives way to the other, so neither side can starve the other.
|
|
@@ -119,8 +120,11 @@ export function createOrderOutbox(options: OrderOutboxOptions): OrderOutbox {
|
|
|
119
120
|
const stuck = [...clocks].filter(([, clock]) => (clock.pausedAt ?? now()) - clock.since >= stuckAfter);
|
|
120
121
|
if (!stuck.length) return undefined;
|
|
121
122
|
return { commandIds: stuck.map(([id]) => id), since: Math.min(...stuck.map(([, clock]) => clock.since)),
|
|
123
|
+
...(stuck.every(([, clock]) => clock.firstFailedAt !== undefined)
|
|
124
|
+
? { firstFailedAt: Math.min(...stuck.map(([, clock]) => clock.firstFailedAt!)) } : {}),
|
|
122
125
|
reason: stuck.reduce((latest, entry) => (entry[1].seq > latest[1].seq ? entry : latest))[1].reason,
|
|
123
|
-
orders: stuck.map(([commandId, { since, reason }]) => ({ commandId, since, reason
|
|
126
|
+
orders: stuck.map(([commandId, { since, firstFailedAt, reason }]) => ({ commandId, since, reason,
|
|
127
|
+
...(firstFailedAt === undefined ? {} : { firstFailedAt }) })) };
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
// An isolated order is due once its backoff has passed, or when the clock was set back further than any backoff.
|
|
@@ -190,7 +194,7 @@ export function createOrderOutbox(options: OrderOutboxOptions): OrderOutbox {
|
|
|
190
194
|
// floor for the retry: the store's Retry-After, or an isolated order's own backoff.
|
|
191
195
|
function serverFailed(orders: PosOrder[], alone: 'probe' | 'isolated' | undefined, reason: string, retryAfterMs?: number) {
|
|
192
196
|
const at = now();
|
|
193
|
-
for (const { commandId } of orders) clocks.set(commandId, {
|
|
197
|
+
for (const { commandId } of orders) clocks.set(commandId, { ...(clocks.get(commandId) ?? { since: at, firstFailedAt: at }), seq: ++failureSeq, reason });
|
|
194
198
|
const id = orders[0].commandId;
|
|
195
199
|
if (alone === 'isolated') {
|
|
196
200
|
isolate(id, reason, retryAfterMs);
|
|
@@ -73,19 +73,20 @@ export function createRegisterOutbox(options: RegisterOutboxOptions): RegisterOu
|
|
|
73
73
|
// of a register's ledger holds up the commands behind it, by design, since register facts apply in `seq` order.
|
|
74
74
|
// In memory only: a restart starts with no clocks, and the first answered failure after it starts them afresh
|
|
75
75
|
// (accepted: register commands are few, and a restart re-sends at once).
|
|
76
|
-
const clocks = new Map<string, { since: number; pausedAt?: number; seq: number; reason: string }>();
|
|
76
|
+
const clocks = new Map<string, { since: number; firstFailedAt: number; pausedAt?: number; seq: number; reason: string }>();
|
|
77
77
|
let failureSeq = 0; // numbers failures, so `stuck` can name the latest reason
|
|
78
78
|
|
|
79
79
|
function stuckState(): OutboxState['stuck'] {
|
|
80
80
|
const stuck = [...clocks].filter(([, clock]) => (clock.pausedAt ?? now()) - clock.since >= stuckAfter);
|
|
81
81
|
if (!stuck.length) return undefined;
|
|
82
82
|
return { commandIds: stuck.map(([id]) => id), since: Math.min(...stuck.map(([, clock]) => clock.since)),
|
|
83
|
+
firstFailedAt: Math.min(...stuck.map(([, clock]) => clock.firstFailedAt)),
|
|
83
84
|
reason: stuck.reduce((latest, entry) => (entry[1].seq > latest[1].seq ? entry : latest))[1].reason,
|
|
84
|
-
orders: stuck.map(([commandId, { since, reason }]) => ({ commandId, since, reason })) };
|
|
85
|
+
orders: stuck.map(([commandId, { since, firstFailedAt, reason }]) => ({ commandId, since, firstFailedAt, reason })) };
|
|
85
86
|
}
|
|
86
87
|
function serverFailed(commands: RegisterCommand[], reason: string) {
|
|
87
88
|
const at = now();
|
|
88
|
-
for (const { commandId } of commands) clocks.set(commandId, {
|
|
89
|
+
for (const { commandId } of commands) clocks.set(commandId, { ...(clocks.get(commandId) ?? { since: at, firstFailedAt: at }), seq: ++failureSeq, reason });
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
async function updateState(patch: Partial<OutboxState> = {}) {
|
package/src/outbox/types.ts
CHANGED
|
@@ -27,8 +27,11 @@ export interface OutboxState {
|
|
|
27
27
|
/** Set while the store (not offline) has kept failing some orders for 15 minutes, each on its own clock. Those
|
|
28
28
|
* orders stay pending and keep retrying. `orders` has each order's own entry: its `since` is when its clock
|
|
29
29
|
* would have started had there been no offline gaps (now minus its answered time; while offline, as of the
|
|
30
|
-
* moment the clock paused), and its latest `reason`. `since` is the earliest of theirs
|
|
31
|
-
|
|
30
|
+
* moment the clock paused), and its latest `reason`. `since` is the earliest of theirs, virtual for the threshold;
|
|
31
|
+
* `reason` the latest. `firstFailedAt` is the earliest real wall-clock first failure, only when every stuck clock
|
|
32
|
+
* has one. Each order entry has its own real wall-clock `firstFailedAt`. Both are in memory only, absent after restart. */
|
|
33
|
+
stuck?: { commandIds: string[]; since: number; firstFailedAt?: number; reason: string;
|
|
34
|
+
orders: { commandId: string; since: number; firstFailedAt?: number; reason: string }[] };
|
|
32
35
|
/** Set after 3 consecutive 404 answers (counted across the outboxes sharing one BackendNotFound): the store address
|
|
33
36
|
* may be wrong, or the store's plugin isn't installed or is switched off. The outbox keeps retrying; the next answer
|
|
34
37
|
* that isn't a 404 clears it (offline changes nothing). `since` is when the first of those 404s arrived. */
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { RegisterCommandEnvelope } from '@tallyui/core';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import type { RegisterCommandCollection } from '../register/register-commands';
|
|
4
|
+
import { createRegisterOutbox, type RegisterOutboxOptions } from './register-outbox';
|
|
5
|
+
import type { CommandTransport, OutboxState } from './types';
|
|
6
|
+
|
|
7
|
+
const idle: OutboxState = { pending: 0, sending: false };
|
|
8
|
+
|
|
9
|
+
export interface UseRegisterOutboxOptions {
|
|
10
|
+
/** `register_commands` (see `registerCommandCollection()`); `null` while it opens or when there's no store. A new collection (a store switch) restarts the outbox. */
|
|
11
|
+
commands: RegisterCommandCollection | null;
|
|
12
|
+
/** Builds the transport; called when the outbox is created: on each new collection or device id (twice under React StrictMode's double effect, which stops the first outbox). */
|
|
13
|
+
transport(): CommandTransport<RegisterCommandEnvelope>;
|
|
14
|
+
/** The device id sent on every command. A change restarts the outbox. */
|
|
15
|
+
deviceId: string;
|
|
16
|
+
/** Passed through to createRegisterOutbox (same meaning). The latest function is called each time, so a new one does not restart the outbox. */
|
|
17
|
+
isEnabled?: () => boolean;
|
|
18
|
+
/** Called with each result. The latest function is called each time; a new one does not restart the outbox. */
|
|
19
|
+
onResult?: RegisterOutboxOptions['onResult'];
|
|
20
|
+
backendNotFound?: RegisterOutboxOptions['backendNotFound'];
|
|
21
|
+
}
|
|
22
|
+
export interface UseRegisterOutboxResult {
|
|
23
|
+
/** The outbox state, or idle ({ pending: 0, sending: false }) while there is no collection. */
|
|
24
|
+
state: OutboxState;
|
|
25
|
+
/** Sends pending till updates; does nothing while there is no collection. */
|
|
26
|
+
flush(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useRegisterOutbox(options: UseRegisterOutboxOptions): UseRegisterOutboxResult {
|
|
30
|
+
const { commands, deviceId } = options;
|
|
31
|
+
const latest = useRef(options);
|
|
32
|
+
latest.current = options;
|
|
33
|
+
const current = useRef<{ commands: RegisterCommandCollection; deviceId: string; outbox: ReturnType<typeof createRegisterOutbox> } | null>(null);
|
|
34
|
+
const [state, setState] = useState<OutboxState>(idle);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
setState(idle);
|
|
38
|
+
if (!commands) return;
|
|
39
|
+
const outbox = createRegisterOutbox({ collection: commands, deviceId, transport: latest.current.transport(),
|
|
40
|
+
isEnabled: latest.current.isEnabled ? () => latest.current.isEnabled?.() ?? true : undefined,
|
|
41
|
+
onResult: latest.current.onResult ? async (command, result) => { await latest.current.onResult?.(command, result); } : undefined,
|
|
42
|
+
backendNotFound: latest.current.backendNotFound });
|
|
43
|
+
current.current = { commands, deviceId, outbox };
|
|
44
|
+
const status = outbox.state$.subscribe(setState);
|
|
45
|
+
outbox.start();
|
|
46
|
+
return () => { outbox.stop(); status.unsubscribe(); current.current = null; setState(idle); };
|
|
47
|
+
}, [commands, deviceId]);
|
|
48
|
+
|
|
49
|
+
const opened = current.current;
|
|
50
|
+
return { state: opened?.commands === commands && opened.deviceId === deviceId ? state : idle,
|
|
51
|
+
async flush() {
|
|
52
|
+
const active = current.current;
|
|
53
|
+
if (active?.commands === commands && active.deviceId === deviceId) await active.outbox.flush();
|
|
54
|
+
} };
|
|
55
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { errorKind, SignInError } from '@tallyui/core';
|
|
2
3
|
import type { StoreSettings, StoreSettingsChoice, StoreSettingsChoices, TaxRounding } from '@tallyui/core';
|
|
3
4
|
import { resolveStoreSettings } from './resolve-store-settings';
|
|
4
5
|
import type { ResolveStoreSettingsOptions } from './resolve-store-settings';
|
|
@@ -34,7 +35,11 @@ function readTaxRounding({ connector, context }: ResolveStoreSettingsOptions): P
|
|
|
34
35
|
if (!capabilities) throw new StoreCapabilitiesUnavailableError();
|
|
35
36
|
return capabilities.taxRounding;
|
|
36
37
|
},
|
|
37
|
-
(cause) => {
|
|
38
|
+
(cause) => {
|
|
39
|
+
// The till must fix its own credentials or software; retrying cannot help.
|
|
40
|
+
if (errorKind(cause) === 'till' || (cause instanceof SignInError && cause.code === 'invalid_credentials')) throw cause;
|
|
41
|
+
throw new StoreCapabilitiesUnavailableError(cause);
|
|
42
|
+
},
|
|
38
43
|
);
|
|
39
44
|
}
|
|
40
45
|
|
|
@@ -46,6 +51,9 @@ function readTaxRounding({ connector, context }: ResolveStoreSettingsOptions): P
|
|
|
46
51
|
* The capabilities are read beside the settings, before `ready`, so each resolve emits the settings once, with the
|
|
47
52
|
* rounding already known: no later change of `settings` holds a sale.
|
|
48
53
|
* Unknown rounding keeps settings unresolved and retries by itself; show "Can't reach the store's settings yet. Retrying…" while `nextRetryAt` is set.
|
|
54
|
+
* A till-class or invalid-credentials sign-in error is not retried by itself, so the app prompts to sign in or update the till.
|
|
55
|
+
* An app shows a sign-in prompt when the `error` state's error is a `ConnectorUnauthorizedError` with `code: 'unauthorized'`
|
|
56
|
+
* or a `SignInError` with `code: 'invalid_credentials'`, and an update prompt for `code: 'till_update_required'`.
|
|
49
57
|
*
|
|
50
58
|
* ```tsx
|
|
51
59
|
* const store = useStoreSettings({ connector, context, loadChoice, saveChoice });
|