@teincfood/core 0.1.1 → 0.1.3
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/adapters/auth.js +2 -6
- package/dist/adapters/business.js +2 -6
- package/dist/adapters/connectivity.js +6 -14
- package/dist/adapters/devices.js +2 -6
- package/dist/adapters/http.js +4 -10
- package/dist/adapters/image-cache.d.ts +9 -3
- package/dist/adapters/image-cache.js +12 -8
- package/dist/adapters/kv-types.js +1 -2
- package/dist/adapters/kv.js +11 -24
- package/dist/adapters/local-node.js +3 -7
- package/dist/adapters/pending-orders.js +1 -4
- package/dist/adapters/query-keys.js +4 -7
- package/dist/adapters/services.js +9 -14
- package/dist/adapters/sqlite.js +3 -8
- package/dist/db/connection.js +15 -20
- package/dist/db/migrations.js +1 -4
- package/dist/db/operations.js +19 -36
- package/dist/index.d.ts +3 -0
- package/dist/index.js +43 -93
- package/dist/reference/api.js +7 -12
- package/dist/reference/events.js +25 -33
- package/dist/reference/operations.js +6 -14
- package/dist/reference/service.js +36 -39
- package/dist/reference/types.js +1 -2
- package/dist/repositories/order.repository.js +15 -18
- package/dist/repositories/pos.repository.js +6 -9
- package/dist/sync/command-builder.js +8 -12
- package/dist/sync/command-queue.service.js +35 -38
- package/dist/sync/engine.js +38 -42
- package/dist/sync/order-number.js +6 -11
- package/dist/sync/order-number.test.d.ts +1 -0
- package/dist/sync/order-number.test.js +30 -0
- package/dist/sync/order-snapshots.service.js +19 -22
- package/dist/sync/pending-orders.service.js +27 -33
- package/dist/sync/transport-local-node.js +16 -20
- package/dist/sync/transport-rest.js +18 -22
- package/dist/sync/types.js +1 -2
- package/dist/sync/use-sync-reconciliation.js +37 -40
- package/dist/types/api/auth.api.js +1 -2
- package/dist/types/api/business.api.js +1 -2
- package/dist/types/api/conversation.api.js +1 -2
- package/dist/types/api/error.api.js +1 -2
- package/dist/types/api/fleet.api.js +1 -2
- package/dist/types/api/index.js +11 -27
- package/dist/types/api/kiosk.api.js +1 -2
- package/dist/types/api/menu.api.js +1 -2
- package/dist/types/api/pagination.js +1 -2
- package/dist/types/api/rider.api.js +1 -2
- package/dist/types/api/seller.api.js +1 -2
- package/dist/types/api/wallet.api.js +1 -2
- package/dist/types/pos.types.js +1 -2
- package/dist/utils/constants.js +24 -27
- package/dist/utils/copyToClipBoard.js +2 -7
- package/dist/utils/currency.js +8 -16
- package/dist/utils/currency.test.d.ts +1 -0
- package/dist/utils/currency.test.js +17 -0
- package/dist/utils/error.js +4 -11
- package/dist/utils/formatCurrency.js +1 -4
- package/dist/utils/formatDate.js +2 -6
- package/dist/utils/phone.js +6 -13
- package/dist/utils/phone.test.d.ts +1 -0
- package/dist/utils/phone.test.js +19 -0
- package/dist/utils/responsive.js +1 -4
- package/dist/utils/storage.js +14 -28
- package/dist/utils/sync-logger.js +1 -4
- package/dist/utils/uuid.js +4 -10
- package/dist/utils/uuid.test.d.ts +1 -0
- package/dist/utils/uuid.test.js +13 -0
- package/dist/utils/validation.js +11 -24
- package/dist/utils/validation.test.d.ts +1 -0
- package/dist/utils/validation.test.js +23 -0
- package/package.json +16 -1
package/dist/sync/engine.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Sync Engine orchestrator.
|
|
4
3
|
*
|
|
@@ -12,15 +11,13 @@
|
|
|
12
11
|
* - Local Node transport
|
|
13
12
|
* - cloud reconciliation
|
|
14
13
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const sync_logger_1 = require("../utils/sync-logger");
|
|
23
|
-
const constants_1 = require("../utils/constants");
|
|
14
|
+
import { restTransport } from "./transport-rest";
|
|
15
|
+
import { localNodeTransport } from "./transport-local-node";
|
|
16
|
+
import { commandQueueService } from "./command-queue.service";
|
|
17
|
+
import { getConnectivityState, setConnectivityState } from "../adapters/connectivity";
|
|
18
|
+
import { ApiError } from "../adapters/http";
|
|
19
|
+
import { syncLogger } from "../utils/sync-logger";
|
|
20
|
+
import { CLOUD_COMMAND_TIMEOUT_MS } from "../utils/constants";
|
|
24
21
|
function withTimeout(promise, ms) {
|
|
25
22
|
return new Promise((resolve, reject) => {
|
|
26
23
|
const timer = setTimeout(() => {
|
|
@@ -35,7 +32,7 @@ function withTimeout(promise, ms) {
|
|
|
35
32
|
});
|
|
36
33
|
});
|
|
37
34
|
}
|
|
38
|
-
class SyncEngine {
|
|
35
|
+
export class SyncEngine {
|
|
39
36
|
constructor(options) {
|
|
40
37
|
this.eventSubscribers = [];
|
|
41
38
|
this.transports = options.transports;
|
|
@@ -53,37 +50,37 @@ class SyncEngine {
|
|
|
53
50
|
* quickly instead of blocking the terminal for the full HTTP timeout.
|
|
54
51
|
*/
|
|
55
52
|
async execute(command) {
|
|
56
|
-
|
|
53
|
+
syncLogger.command(command, "issued");
|
|
57
54
|
let strategy = this.resolveStrategy(command);
|
|
58
|
-
const { isCloudReachable, isOnline } =
|
|
59
|
-
|
|
55
|
+
const { isCloudReachable, isOnline } = getConnectivityState();
|
|
56
|
+
syncLogger.info("SyncEngine", "Command strategy resolved", {
|
|
60
57
|
command_id: command.command_id,
|
|
61
58
|
command_type: command.command_type,
|
|
62
59
|
strategy,
|
|
63
60
|
isCloudReachable,
|
|
64
61
|
isOnline,
|
|
65
|
-
cloud_available:
|
|
66
|
-
lan_available:
|
|
62
|
+
cloud_available: restTransport.isAvailable(),
|
|
63
|
+
lan_available: localNodeTransport.isAvailable(),
|
|
67
64
|
});
|
|
68
65
|
if (strategy === "queue") {
|
|
69
|
-
const event = await
|
|
66
|
+
const event = await commandQueueService.enqueue(command);
|
|
70
67
|
this.broadcast(event);
|
|
71
68
|
return [event];
|
|
72
69
|
}
|
|
73
70
|
if (strategy === "cloud") {
|
|
74
71
|
try {
|
|
75
|
-
const events = await withTimeout(
|
|
72
|
+
const events = await withTimeout(restTransport.execute(command), CLOUD_COMMAND_TIMEOUT_MS);
|
|
76
73
|
for (const event of events) {
|
|
77
74
|
this.broadcast(event);
|
|
78
75
|
}
|
|
79
|
-
|
|
76
|
+
syncLogger.command(command, "completed", {
|
|
80
77
|
event_count: events.length,
|
|
81
78
|
});
|
|
82
79
|
// Cloud just worked: it is reachable again. Mark it so future commands
|
|
83
80
|
// go cloud-first, and replay anything queued during the outage.
|
|
84
|
-
|
|
81
|
+
setConnectivityState({ isCloudReachable: "reachable" });
|
|
85
82
|
this.replayQueuedCommands().catch((error) => {
|
|
86
|
-
|
|
83
|
+
syncLogger.error("SyncEngine", "Replay after cloud success failed", {
|
|
87
84
|
error: String(error),
|
|
88
85
|
});
|
|
89
86
|
});
|
|
@@ -94,8 +91,8 @@ class SyncEngine {
|
|
|
94
91
|
// decision (e.g. PAYMENT_REQUIRED, FORBIDDEN). Surface it to the
|
|
95
92
|
// caller instead of masking it as a connectivity problem and silently
|
|
96
93
|
// queueing a command that can never succeed.
|
|
97
|
-
if (error instanceof
|
|
98
|
-
|
|
94
|
+
if (error instanceof ApiError && error.status >= 400 && error.status < 500) {
|
|
95
|
+
syncLogger.warn("SyncEngine", "Cloud command rejected by server", {
|
|
99
96
|
command_type: command.command_type,
|
|
100
97
|
command_id: command.command_id,
|
|
101
98
|
status: error.status,
|
|
@@ -105,18 +102,18 @@ class SyncEngine {
|
|
|
105
102
|
}
|
|
106
103
|
// Cloud is slow or unreachable: mark it and fall back to the Local
|
|
107
104
|
// Node or the outbox so the terminal stays responsive.
|
|
108
|
-
|
|
105
|
+
syncLogger.warn("SyncEngine", "Cloud command timed out, falling back", {
|
|
109
106
|
command_type: command.command_type,
|
|
110
107
|
error: String(error),
|
|
111
108
|
});
|
|
112
|
-
|
|
109
|
+
setConnectivityState({ isCloudReachable: "unreachable" });
|
|
113
110
|
strategy = this.resolveStrategy(command);
|
|
114
111
|
if (strategy === "queue") {
|
|
115
|
-
const event = await
|
|
112
|
+
const event = await commandQueueService.enqueue(command);
|
|
116
113
|
this.broadcast(event);
|
|
117
114
|
return [event];
|
|
118
115
|
}
|
|
119
|
-
|
|
116
|
+
syncLogger.info("SyncEngine", "Cloud timed out, using LAN fallback", {
|
|
120
117
|
command_id: command.command_id,
|
|
121
118
|
command_type: command.command_type,
|
|
122
119
|
strategy,
|
|
@@ -124,7 +121,7 @@ class SyncEngine {
|
|
|
124
121
|
}
|
|
125
122
|
}
|
|
126
123
|
const transport = this.selectTransport(command, strategy);
|
|
127
|
-
|
|
124
|
+
syncLogger.info("SyncEngine", "Executing via transport", {
|
|
128
125
|
command_id: command.command_id,
|
|
129
126
|
command_type: command.command_type,
|
|
130
127
|
strategy,
|
|
@@ -134,7 +131,7 @@ class SyncEngine {
|
|
|
134
131
|
for (const event of events) {
|
|
135
132
|
this.broadcast(event);
|
|
136
133
|
}
|
|
137
|
-
|
|
134
|
+
syncLogger.command(command, "completed", {
|
|
138
135
|
event_count: events.length,
|
|
139
136
|
transport: transport.name,
|
|
140
137
|
});
|
|
@@ -144,7 +141,7 @@ class SyncEngine {
|
|
|
144
141
|
* Trigger replay of queued commands when connectivity returns.
|
|
145
142
|
*/
|
|
146
143
|
async replayQueuedCommands() {
|
|
147
|
-
await
|
|
144
|
+
await commandQueueService.replay();
|
|
148
145
|
}
|
|
149
146
|
subscribe(handler) {
|
|
150
147
|
this.eventSubscribers.push(handler);
|
|
@@ -165,17 +162,17 @@ class SyncEngine {
|
|
|
165
162
|
this.broadcast(event);
|
|
166
163
|
}
|
|
167
164
|
resolveStrategy(_command) {
|
|
168
|
-
const { isCloudReachable, isOnline } =
|
|
165
|
+
const { isCloudReachable, isOnline } = getConnectivityState();
|
|
169
166
|
// Cloud-first when we believe it is reachable OR we have not yet verified
|
|
170
167
|
// it (unknown). Only a confirmed "unreachable" status falls back to the
|
|
171
168
|
// Local Node / outbox. Commands issued on a healthy connection must never
|
|
172
169
|
// get stuck in the local outbox.
|
|
173
170
|
if ((isCloudReachable === "reachable" || isCloudReachable === "unknown") &&
|
|
174
|
-
|
|
171
|
+
restTransport.isAvailable()) {
|
|
175
172
|
return "cloud";
|
|
176
173
|
}
|
|
177
174
|
// Local Node fallback when enabled and connected.
|
|
178
|
-
if (
|
|
175
|
+
if (localNodeTransport.isAvailable()) {
|
|
179
176
|
return "local_node";
|
|
180
177
|
}
|
|
181
178
|
// No network at all: queue immediately.
|
|
@@ -192,11 +189,11 @@ class SyncEngine {
|
|
|
192
189
|
return "cloud";
|
|
193
190
|
}
|
|
194
191
|
selectTransport(_command, strategy) {
|
|
195
|
-
if (strategy === "local_node" &&
|
|
196
|
-
return
|
|
192
|
+
if (strategy === "local_node" && localNodeTransport.isAvailable()) {
|
|
193
|
+
return localNodeTransport;
|
|
197
194
|
}
|
|
198
|
-
if (strategy === "cloud" &&
|
|
199
|
-
return
|
|
195
|
+
if (strategy === "cloud" && restTransport.isAvailable()) {
|
|
196
|
+
return restTransport;
|
|
200
197
|
}
|
|
201
198
|
const transport = this.transports.find((t) => t.isAvailable()) ?? this.transports[0];
|
|
202
199
|
if (!transport) {
|
|
@@ -205,12 +202,11 @@ class SyncEngine {
|
|
|
205
202
|
return transport;
|
|
206
203
|
}
|
|
207
204
|
broadcast(event) {
|
|
208
|
-
|
|
205
|
+
syncLogger.event(event, "broadcast");
|
|
209
206
|
this.eventSubscribers.forEach((handler) => handler(event));
|
|
210
207
|
}
|
|
211
208
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
transports: [transport_rest_1.restTransport, transport_local_node_1.localNodeTransport],
|
|
209
|
+
export const syncEngine = new SyncEngine({
|
|
210
|
+
transports: [restTransport, localNodeTransport],
|
|
215
211
|
});
|
|
216
|
-
|
|
212
|
+
commandQueueService.setSyncEngine(syncEngine);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Local order number allocation for offline/LAN operation.
|
|
4
3
|
*
|
|
@@ -7,11 +6,7 @@
|
|
|
7
6
|
* `TFL-YYYYMMDD-NNNN`. These are reconciled with permanent cloud order numbers
|
|
8
7
|
* once connectivity returns.
|
|
9
8
|
*/
|
|
10
|
-
|
|
11
|
-
exports.generateLocalOrderNumber = generateLocalOrderNumber;
|
|
12
|
-
exports.generateLocalCalloutNumber = generateLocalCalloutNumber;
|
|
13
|
-
exports.isLocalOrderNumber = isLocalOrderNumber;
|
|
14
|
-
const kv_1 = require("../adapters/kv");
|
|
9
|
+
import { getItemSync as getItem, setItemSync as setItem } from "../adapters/kv";
|
|
15
10
|
const LOCAL_ORDER_SEQ_KEY = "local_order_sequence";
|
|
16
11
|
const LOCAL_CALLOUT_SEQ_KEY = "local_callout_sequence";
|
|
17
12
|
function formatDatePart(date) {
|
|
@@ -21,7 +16,7 @@ function formatDatePart(date) {
|
|
|
21
16
|
return `${year}${month}${day}`;
|
|
22
17
|
}
|
|
23
18
|
function getNextSequence(datePart, key) {
|
|
24
|
-
const stored = (
|
|
19
|
+
const stored = getItem(key);
|
|
25
20
|
const map = stored ?? {};
|
|
26
21
|
const next = (map[datePart] ?? 0) + 1;
|
|
27
22
|
map[datePart] = next;
|
|
@@ -32,13 +27,13 @@ function getNextSequence(datePart, key) {
|
|
|
32
27
|
delete map[mapKey];
|
|
33
28
|
}
|
|
34
29
|
}
|
|
35
|
-
(
|
|
30
|
+
setItem(key, map);
|
|
36
31
|
return next;
|
|
37
32
|
}
|
|
38
33
|
/**
|
|
39
34
|
* Generate a local order number for offline-created orders.
|
|
40
35
|
*/
|
|
41
|
-
function generateLocalOrderNumber(date = new Date()) {
|
|
36
|
+
export function generateLocalOrderNumber(date = new Date()) {
|
|
42
37
|
const datePart = formatDatePart(date);
|
|
43
38
|
const seq = getNextSequence(datePart, LOCAL_ORDER_SEQ_KEY);
|
|
44
39
|
return `TFL-${datePart}-${String(seq).padStart(4, "0")}`;
|
|
@@ -46,13 +41,13 @@ function generateLocalOrderNumber(date = new Date()) {
|
|
|
46
41
|
/**
|
|
47
42
|
* Generate a local callout number for offline-created orders.
|
|
48
43
|
*/
|
|
49
|
-
function generateLocalCalloutNumber(date = new Date()) {
|
|
44
|
+
export function generateLocalCalloutNumber(date = new Date()) {
|
|
50
45
|
const datePart = formatDatePart(date);
|
|
51
46
|
return getNextSequence(datePart, LOCAL_CALLOUT_SEQ_KEY);
|
|
52
47
|
}
|
|
53
48
|
/**
|
|
54
49
|
* Check whether an order number was locally allocated.
|
|
55
50
|
*/
|
|
56
|
-
function isLocalOrderNumber(orderNumber) {
|
|
51
|
+
export function isLocalOrderNumber(orderNumber) {
|
|
57
52
|
return orderNumber.startsWith("TFL-");
|
|
58
53
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { generateLocalOrderNumber, generateLocalCalloutNumber, isLocalOrderNumber } from "./order-number";
|
|
3
|
+
import { removeItemSync } from "../adapters/kv";
|
|
4
|
+
// Use in-memory KV for tests (default driver is memory)
|
|
5
|
+
describe("order-number", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
removeItemSync("local_order_sequence");
|
|
8
|
+
removeItemSync("local_callout_sequence");
|
|
9
|
+
});
|
|
10
|
+
it("generates TFL order numbers with date and sequence", () => {
|
|
11
|
+
const d = new Date("2026-08-24T10:00:00.000Z");
|
|
12
|
+
const a = generateLocalOrderNumber(d);
|
|
13
|
+
const b = generateLocalOrderNumber(d);
|
|
14
|
+
expect(a).toBe("TFL-20260824-0001");
|
|
15
|
+
expect(b).toBe("TFL-20260824-0002");
|
|
16
|
+
expect(isLocalOrderNumber(a)).toBe(true);
|
|
17
|
+
expect(isLocalOrderNumber("KF-260701-0042")).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
it("generates incremental callout numbers per day", () => {
|
|
20
|
+
const d = new Date("2026-08-24T10:00:00.000Z");
|
|
21
|
+
expect(generateLocalCalloutNumber(d)).toBe(1);
|
|
22
|
+
expect(generateLocalCalloutNumber(d)).toBe(2);
|
|
23
|
+
});
|
|
24
|
+
it("isolates sequences per day", () => {
|
|
25
|
+
const d1 = new Date("2026-08-24T10:00:00.000Z");
|
|
26
|
+
const d2 = new Date("2026-08-25T10:00:00.000Z");
|
|
27
|
+
expect(generateLocalOrderNumber(d1)).toBe("TFL-20260824-0001");
|
|
28
|
+
expect(generateLocalOrderNumber(d2)).toBe("TFL-20260825-0001");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Durable order read-model.
|
|
4
3
|
*
|
|
@@ -14,11 +13,9 @@
|
|
|
14
13
|
* 2. Successful order list fetches are written through as snapshots so the
|
|
15
14
|
* exact list last seen while online is available offline.
|
|
16
15
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const operations_1 = require("../db/operations");
|
|
21
|
-
const sync_logger_1 = require("../utils/sync-logger");
|
|
16
|
+
import { openSyncDatabase } from "../db/connection";
|
|
17
|
+
import { insertEvent, upsertEntitySnapshot, getEntitySnapshot, getEntitySnapshotsByType, pruneEntitySnapshots, } from "../db/operations";
|
|
18
|
+
import { syncLogger } from "../utils/sync-logger";
|
|
22
19
|
const RETAIN_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
23
20
|
const ORDER_EVENT_TYPES = new Set([
|
|
24
21
|
"order.created",
|
|
@@ -39,13 +36,13 @@ class OrderSnapshotsService {
|
|
|
39
36
|
if (!ORDER_EVENT_TYPES.has(event.event_type))
|
|
40
37
|
return false;
|
|
41
38
|
try {
|
|
42
|
-
const db = await
|
|
43
|
-
await
|
|
39
|
+
const db = await openSyncDatabase();
|
|
40
|
+
await insertEvent(db, { event, origin });
|
|
44
41
|
await this.applyOrderStatusToReadModel(db, event);
|
|
45
42
|
return true;
|
|
46
43
|
}
|
|
47
44
|
catch (error) {
|
|
48
|
-
|
|
45
|
+
syncLogger.error("OrderSnapshots", "Failed to persist order event", {
|
|
49
46
|
error: String(error),
|
|
50
47
|
event_type: event.event_type,
|
|
51
48
|
});
|
|
@@ -65,14 +62,14 @@ class OrderSnapshotsService {
|
|
|
65
62
|
return;
|
|
66
63
|
}
|
|
67
64
|
try {
|
|
68
|
-
const lists = await
|
|
65
|
+
const lists = await getEntitySnapshotsByType(db, "seller_orders");
|
|
69
66
|
for (const list of lists) {
|
|
70
67
|
const data = list.payload.data ?? [];
|
|
71
68
|
const idx = data.findIndex((o) => o.id === orderId);
|
|
72
69
|
if (idx === -1)
|
|
73
70
|
continue;
|
|
74
71
|
const updated = data.map((o, i) => i === idx ? { ...o, status: toStatus } : o);
|
|
75
|
-
await
|
|
72
|
+
await upsertEntitySnapshot(db, {
|
|
76
73
|
entityType: "seller_orders",
|
|
77
74
|
entityId: list.entity_id,
|
|
78
75
|
businessId: list.business_id,
|
|
@@ -83,7 +80,7 @@ class OrderSnapshotsService {
|
|
|
83
80
|
}
|
|
84
81
|
}
|
|
85
82
|
catch (error) {
|
|
86
|
-
|
|
83
|
+
syncLogger.error("OrderSnapshots", "Failed to apply status to read model", {
|
|
87
84
|
error: String(error),
|
|
88
85
|
});
|
|
89
86
|
}
|
|
@@ -97,8 +94,8 @@ class OrderSnapshotsService {
|
|
|
97
94
|
if (!businessId)
|
|
98
95
|
return;
|
|
99
96
|
try {
|
|
100
|
-
const db = await
|
|
101
|
-
await
|
|
97
|
+
const db = await openSyncDatabase();
|
|
98
|
+
await upsertEntitySnapshot(db, {
|
|
102
99
|
entityType: "seller_orders",
|
|
103
100
|
entityId: scope,
|
|
104
101
|
businessId,
|
|
@@ -108,7 +105,7 @@ class OrderSnapshotsService {
|
|
|
108
105
|
});
|
|
109
106
|
}
|
|
110
107
|
catch (error) {
|
|
111
|
-
|
|
108
|
+
syncLogger.error("OrderSnapshots", "Failed to persist seller list", {
|
|
112
109
|
error: String(error),
|
|
113
110
|
businessId,
|
|
114
111
|
});
|
|
@@ -119,14 +116,14 @@ class OrderSnapshotsService {
|
|
|
119
116
|
*/
|
|
120
117
|
async getSellerList(businessId, scope) {
|
|
121
118
|
try {
|
|
122
|
-
const db = await
|
|
123
|
-
const snap = await
|
|
119
|
+
const db = await openSyncDatabase();
|
|
120
|
+
const snap = await getEntitySnapshot(db, "seller_orders", scope);
|
|
124
121
|
if (!snap || snap.business_id !== businessId)
|
|
125
122
|
return null;
|
|
126
123
|
return snap.payload.data ?? null;
|
|
127
124
|
}
|
|
128
125
|
catch (error) {
|
|
129
|
-
|
|
126
|
+
syncLogger.error("OrderSnapshots", "Failed to read seller list", {
|
|
130
127
|
error: String(error),
|
|
131
128
|
});
|
|
132
129
|
return null;
|
|
@@ -138,14 +135,14 @@ class OrderSnapshotsService {
|
|
|
138
135
|
async pruneOld() {
|
|
139
136
|
try {
|
|
140
137
|
const cutoff = new Date(Date.now() - RETAIN_MS).toISOString();
|
|
141
|
-
const db = await
|
|
142
|
-
await
|
|
138
|
+
const db = await openSyncDatabase();
|
|
139
|
+
await pruneEntitySnapshots(db, "seller_orders", cutoff);
|
|
143
140
|
}
|
|
144
141
|
catch (error) {
|
|
145
|
-
|
|
142
|
+
syncLogger.error("OrderSnapshots", "Failed to prune", {
|
|
146
143
|
error: String(error),
|
|
147
144
|
});
|
|
148
145
|
}
|
|
149
146
|
}
|
|
150
147
|
}
|
|
151
|
-
|
|
148
|
+
export const orderSnapshotsService = new OrderSnapshotsService();
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Pending Orders Service.
|
|
4
3
|
*
|
|
@@ -15,16 +14,11 @@
|
|
|
15
14
|
* itself; `order.created` carries `local_order_number` matching the optimistic
|
|
16
15
|
* order's `order_number`.
|
|
17
16
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const connection_1 = require("../db/connection");
|
|
24
|
-
const operations_1 = require("../db/operations");
|
|
25
|
-
const pending_orders_1 = require("../adapters/pending-orders");
|
|
26
|
-
const order_snapshots_service_1 = require("./order-snapshots.service");
|
|
27
|
-
const sync_logger_1 = require("../utils/sync-logger");
|
|
17
|
+
import { openSyncDatabase } from "../db/connection";
|
|
18
|
+
import { insertEvent, getUnsyncedEvents, markEventsSynced, } from "../db/operations";
|
|
19
|
+
import { usePendingOrdersStore } from "../adapters/pending-orders";
|
|
20
|
+
import { orderSnapshotsService } from "./order-snapshots.service";
|
|
21
|
+
import { syncLogger } from "../utils/sync-logger";
|
|
28
22
|
const PENDING_EVENT_TYPE = "order.create_queued";
|
|
29
23
|
/** Order/delivery state-change events that should refresh every terminal. */
|
|
30
24
|
const ORDER_STATE_EVENT_TYPES = new Set([
|
|
@@ -37,7 +31,7 @@ let invalidator = null;
|
|
|
37
31
|
* Register a callback used to invalidate TanStack Query caches when a pending
|
|
38
32
|
* order is reconciled. Returns an unregister function.
|
|
39
33
|
*/
|
|
40
|
-
function registerPendingOrdersInvalidator(fn) {
|
|
34
|
+
export function registerPendingOrdersInvalidator(fn) {
|
|
41
35
|
invalidator = fn;
|
|
42
36
|
return () => {
|
|
43
37
|
if (invalidator === fn)
|
|
@@ -48,15 +42,15 @@ function registerPendingOrdersInvalidator(fn) {
|
|
|
48
42
|
* Load persisted unsynced order:create_queued events into the store.
|
|
49
43
|
* Idempotent; safe to call after app restart while still offline.
|
|
50
44
|
*/
|
|
51
|
-
async function hydratePendingOrders() {
|
|
52
|
-
const store =
|
|
45
|
+
export async function hydratePendingOrders() {
|
|
46
|
+
const store = usePendingOrdersStore.getState();
|
|
53
47
|
if (store.hydrated)
|
|
54
48
|
return;
|
|
55
49
|
try {
|
|
56
|
-
const db = await
|
|
50
|
+
const db = await openSyncDatabase();
|
|
57
51
|
const byNumber = new Map();
|
|
58
52
|
for (const origin of ["local", "lan"]) {
|
|
59
|
-
const events = await
|
|
53
|
+
const events = await getUnsyncedEvents(db, origin, Number.MAX_SAFE_INTEGER);
|
|
60
54
|
for (const event of events) {
|
|
61
55
|
if (event.event_type !== PENDING_EVENT_TYPE)
|
|
62
56
|
continue;
|
|
@@ -65,15 +59,15 @@ async function hydratePendingOrders() {
|
|
|
65
59
|
byNumber.set(order.order_number, order);
|
|
66
60
|
}
|
|
67
61
|
}
|
|
68
|
-
|
|
62
|
+
usePendingOrdersStore.setState({ orders: [...byNumber.values()] });
|
|
69
63
|
}
|
|
70
64
|
catch (error) {
|
|
71
|
-
|
|
65
|
+
syncLogger.error("PendingOrders", "Hydration failed", {
|
|
72
66
|
error: String(error),
|
|
73
67
|
});
|
|
74
68
|
}
|
|
75
69
|
finally {
|
|
76
|
-
|
|
70
|
+
usePendingOrdersStore.getState().setHydrated(true);
|
|
77
71
|
}
|
|
78
72
|
}
|
|
79
73
|
/**
|
|
@@ -92,36 +86,36 @@ async function hydratePendingOrders() {
|
|
|
92
86
|
* pending-orders store (only queued creations are); this only fires the
|
|
93
87
|
* invalidator. Idempotent — duplicate deliveries are harmless.
|
|
94
88
|
*/
|
|
95
|
-
async function applyOrderStateChangeEvent(event, origin = "local") {
|
|
89
|
+
export async function applyOrderStateChangeEvent(event, origin = "local") {
|
|
96
90
|
try {
|
|
97
91
|
if (!ORDER_STATE_EVENT_TYPES.has(event.event_type))
|
|
98
92
|
return;
|
|
99
|
-
|
|
93
|
+
syncLogger.debug("OrderState", "Order state changed", {
|
|
100
94
|
origin,
|
|
101
95
|
event_type: event.event_type,
|
|
102
96
|
entity_id: event.entity_id,
|
|
103
97
|
});
|
|
104
98
|
// Persist the event into the durable journal and patch the offline read
|
|
105
99
|
// model so every terminal keeps the same order states after a restart.
|
|
106
|
-
await
|
|
100
|
+
await orderSnapshotsService.persistOrderEvent(event, origin);
|
|
107
101
|
invalidator?.();
|
|
108
102
|
}
|
|
109
103
|
catch (error) {
|
|
110
|
-
|
|
104
|
+
syncLogger.error("OrderState", "Failed to apply order state change", {
|
|
111
105
|
error: String(error),
|
|
112
106
|
event_type: event.event_type,
|
|
113
107
|
});
|
|
114
108
|
}
|
|
115
109
|
}
|
|
116
|
-
async function applyPendingOrderEvent(event, origin = "local") {
|
|
110
|
+
export async function applyPendingOrderEvent(event, origin = "local") {
|
|
117
111
|
try {
|
|
118
112
|
if (event.event_type === PENDING_EVENT_TYPE) {
|
|
119
113
|
const order = event.payload;
|
|
120
114
|
if (!order?.order_number)
|
|
121
115
|
return;
|
|
122
|
-
|
|
123
|
-
const db = await
|
|
124
|
-
await
|
|
116
|
+
usePendingOrdersStore.getState().addOrder(order);
|
|
117
|
+
const db = await openSyncDatabase();
|
|
118
|
+
await insertEvent(db, { event, origin });
|
|
125
119
|
return;
|
|
126
120
|
}
|
|
127
121
|
if (event.event_type === "order.created") {
|
|
@@ -129,16 +123,16 @@ async function applyPendingOrderEvent(event, origin = "local") {
|
|
|
129
123
|
const localOrderNumber = payload?.local_order_number;
|
|
130
124
|
if (typeof localOrderNumber !== "string")
|
|
131
125
|
return;
|
|
132
|
-
|
|
126
|
+
usePendingOrdersStore.getState().removeByOrderNumber(localOrderNumber);
|
|
133
127
|
// Persist the cloud acknowledgment into the durable journal + read model.
|
|
134
|
-
await
|
|
135
|
-
const db = await
|
|
128
|
+
await orderSnapshotsService.persistOrderEvent(event, "cloud");
|
|
129
|
+
const db = await openSyncDatabase();
|
|
136
130
|
// Mark every persisted optimistic event for this local order number as
|
|
137
131
|
// synced so it does not rehydrate as pending after a restart. Check both
|
|
138
132
|
// origins: the creating terminal persisted "local", LAN clients "lan".
|
|
139
133
|
const ids = [];
|
|
140
134
|
for (const origin of ["local", "lan"]) {
|
|
141
|
-
const unsynced = await
|
|
135
|
+
const unsynced = await getUnsyncedEvents(db, origin, Number.MAX_SAFE_INTEGER);
|
|
142
136
|
for (const e of unsynced) {
|
|
143
137
|
if (e.event_type !== PENDING_EVENT_TYPE)
|
|
144
138
|
continue;
|
|
@@ -149,13 +143,13 @@ async function applyPendingOrderEvent(event, origin = "local") {
|
|
|
149
143
|
}
|
|
150
144
|
}
|
|
151
145
|
if (ids.length > 0) {
|
|
152
|
-
await
|
|
146
|
+
await markEventsSynced(db, ids);
|
|
153
147
|
}
|
|
154
148
|
invalidator?.();
|
|
155
149
|
}
|
|
156
150
|
}
|
|
157
151
|
catch (error) {
|
|
158
|
-
|
|
152
|
+
syncLogger.error("PendingOrders", "Failed to apply event", {
|
|
159
153
|
error: String(error),
|
|
160
154
|
event_type: event.event_type,
|
|
161
155
|
});
|
|
@@ -1,67 +1,63 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Local Node Transport.
|
|
4
3
|
*
|
|
5
4
|
* Routes commands to the elected Local Node over the LAN and receives events
|
|
6
5
|
* broadcast by the Local Node.
|
|
7
6
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const sync_logger_1 = require("../utils/sync-logger");
|
|
12
|
-
class LocalNodeTransport {
|
|
7
|
+
import { useLocalNodeStore, localNodeService } from "../adapters/local-node";
|
|
8
|
+
import { syncLogger } from "../utils/sync-logger";
|
|
9
|
+
export class LocalNodeTransport {
|
|
13
10
|
constructor() {
|
|
14
11
|
this.name = "local_node";
|
|
15
12
|
}
|
|
16
13
|
isAvailable() {
|
|
17
|
-
const store =
|
|
14
|
+
const store = useLocalNodeStore.getState();
|
|
18
15
|
if (!store.isLocalNodeEnabled)
|
|
19
16
|
return false;
|
|
20
17
|
// If this device is the Local Node, it is always available.
|
|
21
|
-
if (
|
|
18
|
+
if (localNodeService.isLocalNode())
|
|
22
19
|
return true;
|
|
23
20
|
// Otherwise, we must be connected to the elected node.
|
|
24
|
-
return
|
|
21
|
+
return localNodeService.isConnectedToNode();
|
|
25
22
|
}
|
|
26
23
|
subscribe() {
|
|
27
24
|
// Events are received through the LocalNodeService event pipeline.
|
|
28
25
|
return () => undefined;
|
|
29
26
|
}
|
|
30
27
|
async execute(command) {
|
|
31
|
-
|
|
28
|
+
syncLogger.transport(this, "executing", {
|
|
32
29
|
command_type: command.command_type,
|
|
33
30
|
command_id: command.command_id,
|
|
34
31
|
});
|
|
35
|
-
if (
|
|
32
|
+
if (localNodeService.isLocalNode()) {
|
|
36
33
|
// This device is the Local Node: execute locally.
|
|
37
|
-
|
|
34
|
+
syncLogger.info("LocalNodeTransport", "Executing on local node (self)", {
|
|
38
35
|
command_type: command.command_type,
|
|
39
36
|
command_id: command.command_id,
|
|
40
37
|
});
|
|
41
|
-
const event = await
|
|
42
|
-
|
|
38
|
+
const event = await localNodeService.handleNodeCommand(command);
|
|
39
|
+
syncLogger.transport(this, "completed", {
|
|
43
40
|
command_type: command.command_type,
|
|
44
41
|
event_type: event?.event_type,
|
|
45
42
|
});
|
|
46
43
|
return event ? [event] : [];
|
|
47
44
|
}
|
|
48
45
|
// Client: send command to the Local Node.
|
|
49
|
-
const client =
|
|
46
|
+
const client = localNodeService.getClient();
|
|
50
47
|
if (!client) {
|
|
51
48
|
throw new Error("LocalNodeTransport: no client connected to Local Node");
|
|
52
49
|
}
|
|
53
|
-
|
|
50
|
+
syncLogger.info("LocalNodeTransport", "Sending command to Local Node", {
|
|
54
51
|
command_type: command.command_type,
|
|
55
52
|
command_id: command.command_id,
|
|
56
|
-
node_device_id:
|
|
53
|
+
node_device_id: localNodeService.getElectedNode()?.deviceId,
|
|
57
54
|
});
|
|
58
55
|
const events = await client.execute(command);
|
|
59
|
-
|
|
56
|
+
syncLogger.transport(this, "completed", {
|
|
60
57
|
command_type: command.command_type,
|
|
61
58
|
event_count: events.length,
|
|
62
59
|
});
|
|
63
60
|
return events;
|
|
64
61
|
}
|
|
65
62
|
}
|
|
66
|
-
|
|
67
|
-
exports.localNodeTransport = new LocalNodeTransport();
|
|
63
|
+
export const localNodeTransport = new LocalNodeTransport();
|