@teincfood/core 0.9.25 → 0.9.26

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.
@@ -73,9 +73,14 @@ export function useMenuItemsQuery(filters, enabled = true) {
73
73
  ? networkQuery.data.pages.flatMap((p) => p.data)
74
74
  : undefined;
75
75
  const data = isOnline && networkData !== undefined ? networkData : enabled ? filtered : undefined;
76
+ // A local snapshot (even an empty list) means the POS has something to
77
+ // render: never gate the whole screen on the in-flight network refresh.
78
+ const hasLocalSnapshot = cachedQuery.data != null;
79
+ const pending = cachedQuery.isPending || (!hasLocalSnapshot && networkQuery.isFetching);
76
80
  return {
77
81
  data,
78
- isPending: cachedQuery.isPending && !networkData,
82
+ isPending: pending,
83
+ isLoading: pending,
79
84
  isFetching: networkQuery.isFetching,
80
85
  isError: cachedQuery.isError || networkQuery.isError,
81
86
  error: (cachedQuery.error ?? networkQuery.error),
@@ -117,10 +122,14 @@ export function useMenuCategoriesQuery() {
117
122
  const usingNetwork = isOnline && networkQuery.data;
118
123
  const data = usingNetwork ? networkQuery.data : cachedQuery.data;
119
124
  const hasCache = cachedQuery.data != null;
125
+ // Only report "loading" when there is nothing local to show; the network
126
+ // refresh must never hide an already-cached category list.
127
+ const pending = cachedQuery.isPending || (!hasCache && networkQuery.isFetching);
120
128
  return {
121
129
  ...networkQuery,
122
130
  data,
123
- isPending: cachedQuery.isPending && !networkQuery.data,
131
+ isPending: pending,
132
+ isLoading: pending,
124
133
  isError: hasCache ? false : networkQuery.isError,
125
134
  error: hasCache ? null : networkQuery.error,
126
135
  };
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Local Orders Service — SQLite is real DB (like backend Orders).
3
3
  * Device-partitioned by UUID PK + DD-RRR-CCC per business.
4
- * is_pos orders are accepted at create; online payment requires online.
4
+ * POS orders start in the business's configured flow (staged → accepted,
5
+ * direct → delivered); online payment requires connectivity.
5
6
  */
6
7
  import type { SellerOrder } from "../../types/api/seller.api";
7
8
  import type { POSOrderCreateRequest } from "../../types/pos.types";
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Local Orders Service — SQLite is real DB (like backend Orders).
3
3
  * Device-partitioned by UUID PK + DD-RRR-CCC per business.
4
- * is_pos orders are accepted at create; online payment requires online.
4
+ * POS orders start in the business's configured flow (staged → accepted,
5
+ * direct → delivered); online payment requires connectivity.
5
6
  */
6
7
  import { openSyncDatabase } from "../../db/connection";
7
8
  import { insertOrder, getOrderById, listOrders, updateOrderStatus, updateOrderPayment, getEntitySnapshotsByType } from "../../db/operations";
@@ -25,20 +25,38 @@ export function useStartupReadiness(options = {}) {
25
25
  const [readiness, setReadiness] = useState(null);
26
26
  const [checking, setChecking] = useState(false);
27
27
  const runRef = useRef(false);
28
+ // Identifies the latest readiness run so a slow catch-up for a previous
29
+ // business can never overwrite the verdict for the active one.
30
+ const tokenRef = useRef(0);
31
+ const businessRef = useRef(businessId);
32
+ // Switching business invalidates the previous verdict — its cached reference
33
+ // keys and lease belong to the old business.
34
+ useEffect(() => {
35
+ if (businessRef.current === businessId)
36
+ return;
37
+ businessRef.current = businessId;
38
+ tokenRef.current += 1;
39
+ runRef.current = false;
40
+ setReadiness(null);
41
+ setChecking(false);
42
+ }, [businessId]);
28
43
  const check = useCallback(async () => {
29
44
  if (!businessId)
30
45
  return;
31
46
  if (runRef.current)
32
47
  return;
33
48
  runRef.current = true;
49
+ const token = ++tokenRef.current;
34
50
  setChecking(true);
35
51
  try {
36
52
  const result = await ensureStartupReady(businessId, { timeoutMs });
37
- setReadiness(result);
53
+ if (tokenRef.current === token)
54
+ setReadiness(result);
38
55
  }
39
56
  finally {
40
57
  runRef.current = false;
41
- setChecking(false);
58
+ if (tokenRef.current === token)
59
+ setChecking(false);
42
60
  }
43
61
  }, [businessId, timeoutMs]);
44
62
  // First check when a business is available.
@@ -28,8 +28,10 @@ export async function ensureLease(businessId) {
28
28
  try {
29
29
  const leasedAt = getSync(`${LEASE_KEY(businessId)}:leased_at`);
30
30
  const needsRefresh = !leasedAt || Date.now() - new Date(leasedAt).getTime() > LEASE_TTL_MS - 24 * 60 * 60 * 1000;
31
+ // Never make readiness wait on the refresh — a slow network must not
32
+ // hold POS startup when a valid cached lease already exists.
31
33
  if (needsRefresh)
32
- await refreshLease(businessId);
34
+ void refreshLease(businessId).catch(() => { });
33
35
  }
34
36
  catch {
35
37
  // keep existing on any refresh failure
@@ -77,25 +77,38 @@ export async function getStartupReadiness(businessId) {
77
77
  export async function ensureStartupReady(businessId, opts) {
78
78
  const timeoutMs = opts?.timeoutMs ?? 15000;
79
79
  const { isOnline, isCloudReachable } = getConnectivityState();
80
- if (isOnline && isCloudReachable !== "unreachable") {
81
- const catchUp = (async () => {
82
- try {
83
- await referenceSyncService.sync({ businessId, keys: [...REQUIRED_ORDER_KEYS] });
84
- }
85
- catch (e) {
86
- syncLogger.warn("StartupReadiness", "reference catch-up failed", { err: String(e) });
87
- }
88
- try {
89
- await ensureLease(businessId);
90
- }
91
- catch (e) {
92
- syncLogger.warn("StartupReadiness", "lease catch-up failed", { err: String(e) });
93
- }
94
- })();
95
- await raceWithTimeout(catchUp, timeoutMs);
80
+ const canUseNetwork = isOnline && isCloudReachable !== "unreachable";
81
+ // Evaluate the device first. If everything the POS needs is already cached,
82
+ // the terminal must open immediately — a slow network must never hold the
83
+ // seller on "Preparing offline data…". The catch-up then runs in the
84
+ // background and silently updates the cache.
85
+ const local = await getStartupReadiness(businessId);
86
+ if (local.ready) {
87
+ if (canUseNetwork)
88
+ void runCatchUp(businessId);
89
+ return local;
96
90
  }
91
+ // Incomplete cache: one bounded network attempt may complete it. This is the
92
+ // only case where waiting is useful (e.g. first launch on a new device).
93
+ if (canUseNetwork)
94
+ await raceWithTimeout(runCatchUp(businessId), timeoutMs);
97
95
  return getStartupReadiness(businessId);
98
96
  }
97
+ /** Best-effort catch-up: reference data + device lease. Never throws. */
98
+ async function runCatchUp(businessId) {
99
+ try {
100
+ await referenceSyncService.sync({ businessId, keys: [...REQUIRED_ORDER_KEYS] });
101
+ }
102
+ catch (e) {
103
+ syncLogger.warn("StartupReadiness", "reference catch-up failed", { err: String(e) });
104
+ }
105
+ try {
106
+ await ensureLease(businessId);
107
+ }
108
+ catch (e) {
109
+ syncLogger.warn("StartupReadiness", "lease catch-up failed", { err: String(e) });
110
+ }
111
+ }
99
112
  /** Resolve after either the promise settles or the timeout elapses. */
100
113
  async function raceWithTimeout(promise, ms) {
101
114
  let timer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teincfood/core",
3
- "version": "0.9.25",
3
+ "version": "0.9.26",
4
4
  "description": "TeincFood shared offline-first core — types, sync engine, local DB, reference data, and repositories for mobile + desktop",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",