@teincfood/core 0.7.2 → 0.7.4

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.
@@ -8,6 +8,16 @@ export declare const POS_KEYS: {
8
8
  };
9
9
  export declare const SELLER_KEYS: {
10
10
  all: readonly ["seller"];
11
+ orders: (businessId?: string) => readonly unknown[];
12
+ };
13
+ export declare const KIOSK_KEYS: {
14
+ all: readonly ["kiosk"];
15
+ };
16
+ export declare const SELLER_ORDERS_KEYS: {
17
+ all: readonly ["seller-orders"];
18
+ };
19
+ export declare const BUSINESS_DELIVERIES_KEYS: {
20
+ all: readonly ["business-deliveries"];
11
21
  };
12
22
  export declare const MENU_KEYS: {
13
23
  all: readonly ["menu"];
@@ -8,6 +8,16 @@ export const POS_KEYS = {
8
8
  };
9
9
  export const SELLER_KEYS = {
10
10
  all: ["seller"],
11
+ orders: (businessId) => ["seller", "orders", businessId].filter((x) => x !== undefined),
12
+ };
13
+ export const KIOSK_KEYS = {
14
+ all: ["kiosk"],
15
+ };
16
+ export const SELLER_ORDERS_KEYS = {
17
+ all: ["seller-orders"],
18
+ };
19
+ export const BUSINESS_DELIVERIES_KEYS = {
20
+ all: ["business-deliveries"],
11
21
  };
12
22
  export const MENU_KEYS = {
13
23
  all: ["menu"],
@@ -110,4 +110,28 @@ export const MIGRATIONS = [
110
110
  `);
111
111
  },
112
112
  },
113
+ {
114
+ version: 4,
115
+ up: async (db) => {
116
+ // Clear stale completed outbox rows and legacy TFL- order numbers that
117
+ // accumulated before 0.7.0's DD-RRR-CCC cutover. Pending/failed/in_flight
118
+ // are kept so an in-flight replay is not lost; only completed + legacy
119
+ // payloads are pruned. Also clears legacy optimistic events.
120
+ await db.execAsync(`
121
+ DELETE FROM commands WHERE status = 'completed';
122
+ `);
123
+ try {
124
+ await db.execAsync(`
125
+ DELETE FROM commands WHERE payload LIKE '%TFL-%';
126
+ `);
127
+ }
128
+ catch { }
129
+ try {
130
+ await db.execAsync(`
131
+ DELETE FROM events WHERE payload LIKE '%TFL-%';
132
+ `);
133
+ }
134
+ catch { }
135
+ },
136
+ },
113
137
  ];
@@ -43,3 +43,6 @@ export declare function getEntitySnapshot<T>(db: SQLiteDatabase, entityType: str
43
43
  export declare function getEntitySnapshotsByType<T>(db: SQLiteDatabase, entityType: string): Promise<EntitySnapshot<T>[]>;
44
44
  export declare function deleteEntitySnapshot(db: SQLiteDatabase, entityType: string, entityId: string): Promise<void>;
45
45
  export declare function pruneEntitySnapshots(db: SQLiteDatabase, entityType: string, cutoffIso: string): Promise<void>;
46
+ export declare function clearAllSyncData(db: SQLiteDatabase): Promise<void>;
47
+ export declare function clearReferenceData(db: SQLiteDatabase): Promise<void>;
48
+ export declare function clearCompletedCommandsAndOrphans(db: SQLiteDatabase): Promise<void>;
@@ -173,3 +173,28 @@ function rowToDomainEvent(row) {
173
173
  timestamp: row.timestamp,
174
174
  };
175
175
  }
176
+ // ─────────────────────────────────────────────────────────────
177
+ // Maintenance — clear stale local data (used by Diagnostics)
178
+ // ─────────────────────────────────────────────────────────────
179
+ export async function clearAllSyncData(db) {
180
+ await db.execAsync(`DELETE FROM commands;`);
181
+ await db.execAsync(`DELETE FROM events;`);
182
+ await db.execAsync(`DELETE FROM entity_snapshots;`);
183
+ // Keep reference_data — it is business-scoped and expensive to re-fetch;
184
+ // only order/command state is cleared. If full wipe needed, call
185
+ // clearReferenceData below.
186
+ }
187
+ export async function clearReferenceData(db) {
188
+ await db.execAsync(`DELETE FROM reference_data;`);
189
+ }
190
+ export async function clearCompletedCommandsAndOrphans(db) {
191
+ await db.execAsync(`DELETE FROM commands WHERE status = 'completed';`);
192
+ try {
193
+ await db.execAsync(`DELETE FROM commands WHERE payload LIKE '%TFL-%';`);
194
+ }
195
+ catch { }
196
+ try {
197
+ await db.execAsync(`DELETE FROM events WHERE payload LIKE '%TFL-%';`);
198
+ }
199
+ catch { }
200
+ }
@@ -156,10 +156,43 @@ class ReferenceSyncService {
156
156
  async fetchFallback(key, businessId) {
157
157
  switch (key) {
158
158
  case "menu:items": {
159
- const response = await menuService.fetchBusinessMenuItems(businessId, 1);
159
+ // Paginate through all pages so offline cache is complete — not just page 1 (20).
160
+ // Safety cap 50 pages (~1000 items) to avoid runaway on misconfigured backend.
161
+ const allItems = [];
162
+ let page = 1;
163
+ let lastPagination = null;
164
+ while (true) {
165
+ const response = (await menuService.fetchBusinessMenuItems(businessId, page));
166
+ allItems.push(...(response.data ?? []));
167
+ lastPagination = response.pagination;
168
+ if (!lastPagination ||
169
+ lastPagination.page >= lastPagination.total_pages ||
170
+ (response.data ?? []).length === 0)
171
+ break;
172
+ page += 1;
173
+ if (page > 50)
174
+ break;
175
+ }
176
+ const merged = {
177
+ data: allItems,
178
+ pagination: lastPagination
179
+ ? {
180
+ ...lastPagination,
181
+ page: 1,
182
+ page_size: allItems.length,
183
+ total_pages: 1,
184
+ total_entries: allItems.length,
185
+ }
186
+ : {
187
+ page: 1,
188
+ page_size: allItems.length,
189
+ total_pages: 1,
190
+ total_entries: allItems.length,
191
+ },
192
+ };
160
193
  return {
161
- payload: response,
162
- version: this.hashPayload(response),
194
+ payload: merged,
195
+ version: this.hashPayload(merged),
163
196
  };
164
197
  }
165
198
  case "menu:categories": {
@@ -13,6 +13,7 @@ import { insertCommand, getPendingCommands, markCommandInFlight, updateCommandSt
13
13
  import { syncLogger } from "../utils/sync-logger";
14
14
  import { generateEventId } from "../utils/uuid";
15
15
  import { generateLocalCalloutNumber, generateOrderNumberForBusiness, } from "./order-number";
16
+ import { subscribeConnectivity } from "../adapters/connectivity";
16
17
  const MAX_RETRIES = 5;
17
18
  const QUEUED_STATUS_BY_COMMAND = {
18
19
  "order:accept": "accepted",
@@ -142,6 +143,17 @@ class CommandQueueService {
142
143
  }
143
144
  setSyncEngine(engine) {
144
145
  this.syncEngine = engine;
146
+ // Auto-replay when the cloud becomes reachable again (e.g. after a
147
+ // timeout-induced `unreachable` flip, then the next successful probe sets
148
+ // `reachable`). No polling — event-driven via connectivity state.
149
+ try {
150
+ subscribeConnectivity((s) => {
151
+ if (s.isCloudReachable === "reachable" && s.isOnline) {
152
+ this.replay().catch(() => { });
153
+ }
154
+ });
155
+ }
156
+ catch { }
145
157
  }
146
158
  /**
147
159
  * Enqueue a command because no transport is currently available.
@@ -206,7 +218,10 @@ class CommandQueueService {
206
218
  await markCommandInFlight(db, command.command_id);
207
219
  try {
208
220
  const events = await this.syncEngine.execute(parsedCommand);
209
- await updateCommandStatus(db, command.command_id, "completed");
221
+ // Outbox is an ephemeral queue — remove the row on success so the
222
+ // Diagnostics outbox count drops to 0 and old completed rows never
223
+ // accumulate. The optimistic event stays in `events` (marked synced).
224
+ await deleteCommand(db, command.command_id);
210
225
  syncLogger.command(parsedCommand, "completed", {
211
226
  event_count: events.length,
212
227
  });
@@ -133,6 +133,12 @@ export async function applyPendingOrderEvent(event, origin = "local") {
133
133
  usePendingOrdersStore.getState().addOrder(order);
134
134
  const db = await openSyncDatabase();
135
135
  await insertEvent(db, { event, origin });
136
+ // Ensure every screen (POS, Orders, Kitchen) sees the optimistic order
137
+ // even when there is no Local Node — TanStack queries that merge pending
138
+ // orders will re-render due to store change, but also invalidate so
139
+ // server-backed lists that are currently cached refresh with the pending
140
+ // overlay.
141
+ invalidator?.();
136
142
  return;
137
143
  }
138
144
  if (QUEUED_STATUS_EVENT_TYPES.has(event.event_type)) {
@@ -15,7 +15,7 @@ import { localNodeService } from "../adapters/local-node";
15
15
  import { getSyncDatabase } from "../db/connection";
16
16
  import { markEventsSynced } from "../db/operations";
17
17
  import { syncLogger } from "../utils/sync-logger";
18
- import { POS_KEYS, SELLER_KEYS, MENU_KEYS, REFERENCE_KEYS } from "../adapters/query-keys";
18
+ import { POS_KEYS, SELLER_KEYS, KIOSK_KEYS, SELLER_ORDERS_KEYS, BUSINESS_DELIVERIES_KEYS, MENU_KEYS, REFERENCE_KEYS, } from "../adapters/query-keys";
19
19
  import { applyReferenceChangeEvent, registerReferenceInvalidator, registerReferenceEventEngine, REFERENCE_CHANGED_EVENT_TYPE, } from "../reference/events";
20
20
  function isLocalOrderEvent(event) {
21
21
  const payload = event.payload;
@@ -39,10 +39,14 @@ export function useSyncReconciliation() {
39
39
  // server-confirmed order replaces the optimistic one. Invalidate from the
40
40
  // broadest key: POS_KEYS.orders() without a businessId would produce
41
41
  // ['pos','orders',undefined] which does not prefix-match business-scoped
42
- // keys.
42
+ // keys. Also cover kiosk/seller-orders/business-deliveries so boards
43
+ // refresh even when there is no Local Node (offline queue + Phoenix).
43
44
  const unregisterInvalidator = registerPendingOrdersInvalidator(() => {
44
45
  queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
45
46
  queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
47
+ queryClient.invalidateQueries({ queryKey: KIOSK_KEYS.all });
48
+ queryClient.invalidateQueries({ queryKey: SELLER_ORDERS_KEYS.all });
49
+ queryClient.invalidateQueries({ queryKey: BUSINESS_DELIVERIES_KEYS.all });
46
50
  });
47
51
  // Reference-change events are published through the Sync Engine but are
48
52
  // not backed by a command, so the engine has no transport dependency on
@@ -10,7 +10,7 @@ export declare function setApiBaseUrl(url: string | undefined): void;
10
10
  export declare function setWsBaseUrl(url: string | undefined): void;
11
11
  /** App metadata */
12
12
  export declare const APP_NAME = "Teinc Food Business";
13
- export declare const APP_VERSION = "0.7.2";
13
+ export declare const APP_VERSION = "0.7.3";
14
14
  /** Async storage keys */
15
15
  export declare const STORAGE_KEYS: {
16
16
  readonly AUTH_TOKEN: "auth_token";
@@ -49,13 +49,13 @@ export declare const DEFAULT_PAGE_SIZE = 20;
49
49
  * Keeps the terminal responsive (fast) even when the internet is slow.
50
50
  */
51
51
  export declare const CLOUD_COMMAND_TIMEOUT_MS = 2500;
52
- export declare const appVersion = "0.7.2";
52
+ export declare const appVersion = "0.7.3";
53
53
  /** Default config bundle for createTeincCore overrides */
54
54
  export declare const DEFAULTS: {
55
55
  readonly API_BASE_URL: string;
56
56
  readonly WS_BASE_URL: string;
57
57
  readonly APP_NAME: "Teinc Food Business";
58
- readonly APP_VERSION: "0.7.2";
58
+ readonly APP_VERSION: "0.7.3";
59
59
  readonly STORAGE_KEYS: {
60
60
  readonly AUTH_TOKEN: "auth_token";
61
61
  readonly REFRESH_TOKEN: "refresh_token";
@@ -38,7 +38,7 @@ export function setWsBaseUrl(url) {
38
38
  }
39
39
  /** App metadata */
40
40
  export const APP_NAME = "Teinc Food Business";
41
- export const APP_VERSION = "0.7.2";
41
+ export const APP_VERSION = "0.7.3";
42
42
  /** Async storage keys */
43
43
  export const STORAGE_KEYS = {
44
44
  AUTH_TOKEN: "auth_token",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teincfood/core",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
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",