@teincfood/core 0.1.0 → 0.1.2
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.js +2 -6
- 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.js +40 -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-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.d.ts +8 -0
- package/dist/types/api/seller.api.js +1 -2
- package/dist/types/api/wallet.api.js +1 -2
- package/dist/types/pos.types.d.ts +65 -18
- package/dist/types/pos.types.js +1 -5
- package/dist/utils/constants.js +24 -27
- package/dist/utils/copyToClipBoard.js +2 -7
- package/dist/utils/currency.js +8 -16
- 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/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/validation.js +11 -24
- package/package.json +12 -1
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Order Repository.
|
|
4
3
|
*
|
|
@@ -8,10 +7,8 @@
|
|
|
8
7
|
* In Phase 1, commands execute immediately through the REST transport.
|
|
9
8
|
* Future phases will queue commands when offline.
|
|
10
9
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const engine_1 = require("../sync/engine");
|
|
14
|
-
const command_builder_1 = require("../sync/command-builder");
|
|
10
|
+
import { syncEngine } from "../sync/engine";
|
|
11
|
+
import { buildCommand } from "../sync/command-builder";
|
|
15
12
|
async function executeCommand(command) {
|
|
16
13
|
console.debug("[OrderRepository] Dispatching command", JSON.stringify({
|
|
17
14
|
command_id: command.command_id,
|
|
@@ -21,7 +18,7 @@ async function executeCommand(command) {
|
|
|
21
18
|
payload_keys: Object.keys(command.payload ?? {}),
|
|
22
19
|
timestamp: new Date().toISOString(),
|
|
23
20
|
}));
|
|
24
|
-
const events = await
|
|
21
|
+
const events = await syncEngine.execute(command);
|
|
25
22
|
const event = events[0];
|
|
26
23
|
console.debug("[OrderRepository] Command result", JSON.stringify({
|
|
27
24
|
command_id: command.command_id,
|
|
@@ -37,9 +34,9 @@ async function executeCommand(command) {
|
|
|
37
34
|
}
|
|
38
35
|
return event.payload;
|
|
39
36
|
}
|
|
40
|
-
|
|
37
|
+
export const orderRepository = {
|
|
41
38
|
async createOrder(input) {
|
|
42
|
-
const command =
|
|
39
|
+
const command = buildCommand({
|
|
43
40
|
commandType: "order:create",
|
|
44
41
|
businessId: input.businessId,
|
|
45
42
|
entityId: input.entityId,
|
|
@@ -48,7 +45,7 @@ exports.orderRepository = {
|
|
|
48
45
|
return executeCommand(command);
|
|
49
46
|
},
|
|
50
47
|
async acceptOrder(input) {
|
|
51
|
-
const command =
|
|
48
|
+
const command = buildCommand({
|
|
52
49
|
commandType: "order:accept",
|
|
53
50
|
businessId: input.businessId,
|
|
54
51
|
entityId: input.orderId,
|
|
@@ -57,7 +54,7 @@ exports.orderRepository = {
|
|
|
57
54
|
return executeCommand(command);
|
|
58
55
|
},
|
|
59
56
|
async rejectOrder(input) {
|
|
60
|
-
const command =
|
|
57
|
+
const command = buildCommand({
|
|
61
58
|
commandType: "order:reject",
|
|
62
59
|
businessId: input.businessId,
|
|
63
60
|
entityId: input.orderId,
|
|
@@ -66,7 +63,7 @@ exports.orderRepository = {
|
|
|
66
63
|
return executeCommand(command);
|
|
67
64
|
},
|
|
68
65
|
async startPreparingOrder(input) {
|
|
69
|
-
const command =
|
|
66
|
+
const command = buildCommand({
|
|
70
67
|
commandType: "order:start_preparing",
|
|
71
68
|
businessId: input.businessId,
|
|
72
69
|
entityId: input.orderId,
|
|
@@ -75,7 +72,7 @@ exports.orderRepository = {
|
|
|
75
72
|
return executeCommand(command);
|
|
76
73
|
},
|
|
77
74
|
async markOrderReady(input) {
|
|
78
|
-
const command =
|
|
75
|
+
const command = buildCommand({
|
|
79
76
|
commandType: "order:mark_ready",
|
|
80
77
|
businessId: input.businessId,
|
|
81
78
|
entityId: input.orderId,
|
|
@@ -84,7 +81,7 @@ exports.orderRepository = {
|
|
|
84
81
|
return executeCommand(command);
|
|
85
82
|
},
|
|
86
83
|
async completePickup(input) {
|
|
87
|
-
const command =
|
|
84
|
+
const command = buildCommand({
|
|
88
85
|
commandType: "order:complete_pickup",
|
|
89
86
|
businessId: input.businessId,
|
|
90
87
|
entityId: input.orderId,
|
|
@@ -93,7 +90,7 @@ exports.orderRepository = {
|
|
|
93
90
|
return executeCommand(command);
|
|
94
91
|
},
|
|
95
92
|
async cancelOrder(input) {
|
|
96
|
-
const command =
|
|
93
|
+
const command = buildCommand({
|
|
97
94
|
commandType: "order:cancel",
|
|
98
95
|
businessId: input.businessId,
|
|
99
96
|
entityId: input.orderId,
|
|
@@ -102,13 +99,13 @@ exports.orderRepository = {
|
|
|
102
99
|
return executeCommand(command);
|
|
103
100
|
},
|
|
104
101
|
async confirmHandover(input) {
|
|
105
|
-
const command =
|
|
102
|
+
const command = buildCommand({
|
|
106
103
|
commandType: "delivery:confirm_handover",
|
|
107
104
|
businessId: input.businessId,
|
|
108
105
|
entityId: input.deliveryId,
|
|
109
106
|
payload: {},
|
|
110
107
|
});
|
|
111
|
-
const events = await
|
|
108
|
+
const events = await syncEngine.execute(command);
|
|
112
109
|
const event = events[0];
|
|
113
110
|
if (!event) {
|
|
114
111
|
throw new Error("OrderRepository: confirmHandover returned no events");
|
|
@@ -116,13 +113,13 @@ exports.orderRepository = {
|
|
|
116
113
|
return event.payload;
|
|
117
114
|
},
|
|
118
115
|
async assignRider(input) {
|
|
119
|
-
const command =
|
|
116
|
+
const command = buildCommand({
|
|
120
117
|
commandType: "delivery:assign",
|
|
121
118
|
businessId: input.businessId,
|
|
122
119
|
entityId: input.orderId,
|
|
123
120
|
payload: { rider_id: input.riderId },
|
|
124
121
|
});
|
|
125
|
-
const events = await
|
|
122
|
+
const events = await syncEngine.execute(command);
|
|
126
123
|
const event = events[0];
|
|
127
124
|
if (!event) {
|
|
128
125
|
throw new Error("OrderRepository: assignRider returned no events");
|
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* POS Repository.
|
|
4
3
|
*
|
|
5
4
|
* Domain-specific operations for the POS terminal. Builds on OrderRepository
|
|
6
5
|
* for commands and keeps read/query operations behind a repository interface.
|
|
7
6
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const order_repository_1 = require("./order.repository");
|
|
12
|
-
exports.posRepository = {
|
|
7
|
+
import { posService } from "../adapters/services";
|
|
8
|
+
import { orderRepository } from "./order.repository";
|
|
9
|
+
export const posRepository = {
|
|
13
10
|
async createOrder(businessId, data, entityId) {
|
|
14
|
-
return
|
|
11
|
+
return orderRepository.createOrder({ businessId, data, entityId });
|
|
15
12
|
},
|
|
16
13
|
async fetchPreview(input) {
|
|
17
|
-
const res = await
|
|
14
|
+
const res = await posService.fetchPOSOrderPreview(input.businessId, {
|
|
18
15
|
items: input.items,
|
|
19
16
|
order_type: input.orderType,
|
|
20
17
|
customer_name: input.customerName,
|
|
@@ -22,7 +19,7 @@ exports.posRepository = {
|
|
|
22
19
|
return res;
|
|
23
20
|
},
|
|
24
21
|
async fetchOrders(businessId, params) {
|
|
25
|
-
const res = await
|
|
22
|
+
const res = await posService.fetchBusinessOrders(businessId, params);
|
|
26
23
|
return res;
|
|
27
24
|
},
|
|
28
25
|
};
|
|
@@ -1,23 +1,18 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Command builder helpers.
|
|
4
3
|
*
|
|
5
4
|
* Centralizes creation of Command objects with consistent metadata
|
|
6
5
|
* (command_id, user_id, device_id, timestamps).
|
|
7
6
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
Object.defineProperty(exports, "getAppInstanceId", { enumerable: true, get: function () { return devices_1.getAppInstanceId; } });
|
|
14
|
-
const uuid_1 = require("../utils/uuid");
|
|
15
|
-
function buildCommand(options) {
|
|
16
|
-
const user = auth_1.useAuthStore.getState().user;
|
|
7
|
+
import { useAuthStore } from "../adapters/auth";
|
|
8
|
+
import { getAppInstanceId } from "../adapters/devices";
|
|
9
|
+
import { generateCommandId } from "../utils/uuid";
|
|
10
|
+
export function buildCommand(options) {
|
|
11
|
+
const user = useAuthStore.getState().user;
|
|
17
12
|
const userId = user?.id ?? "anonymous";
|
|
18
|
-
const deviceId =
|
|
13
|
+
const deviceId = getAppInstanceId();
|
|
19
14
|
return {
|
|
20
|
-
command_id:
|
|
15
|
+
command_id: generateCommandId(),
|
|
21
16
|
command_type: options.commandType,
|
|
22
17
|
command_version: options.commandVersion ?? "1.0",
|
|
23
18
|
business_id: options.businessId,
|
|
@@ -28,3 +23,4 @@ function buildCommand(options) {
|
|
|
28
23
|
payload: options.payload,
|
|
29
24
|
};
|
|
30
25
|
}
|
|
26
|
+
export { getAppInstanceId };
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Command Queue Service.
|
|
4
3
|
*
|
|
@@ -9,18 +8,16 @@
|
|
|
9
8
|
* - Replay queued commands when a transport becomes available.
|
|
10
9
|
* - Provide optimistic domain events so the UI can render pending orders.
|
|
11
10
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const uuid_1 = require("../utils/uuid");
|
|
18
|
-
const order_number_1 = require("./order-number");
|
|
11
|
+
import { getSyncDatabase, openSyncDatabase } from "../db/connection";
|
|
12
|
+
import { insertCommand, getPendingCommands, markCommandInFlight, updateCommandStatus, deleteCommand, insertEvent, } from "../db/operations";
|
|
13
|
+
import { syncLogger } from "../utils/sync-logger";
|
|
14
|
+
import { generateEventId } from "../utils/uuid";
|
|
15
|
+
import { generateLocalOrderNumber, generateLocalCalloutNumber, } from "./order-number";
|
|
19
16
|
const MAX_RETRIES = 5;
|
|
20
17
|
function buildOptimisticEvent(command) {
|
|
21
18
|
const payload = command.payload;
|
|
22
19
|
return {
|
|
23
|
-
event_id:
|
|
20
|
+
event_id: generateEventId(),
|
|
24
21
|
event_type: `${command.command_type.replace(":", ".")}_queued`,
|
|
25
22
|
event_version: "1.0",
|
|
26
23
|
entity_id: command.entity_id ?? command.command_id,
|
|
@@ -40,11 +37,11 @@ function buildOptimisticOrder(command, payload) {
|
|
|
40
37
|
const items = payload.items ?? [];
|
|
41
38
|
const totalMinor = payload.total_minor ?? 0;
|
|
42
39
|
const now = new Date().toISOString();
|
|
43
|
-
const orderNumber = payload.order_number ??
|
|
40
|
+
const orderNumber = payload.order_number ?? generateLocalOrderNumber();
|
|
44
41
|
return {
|
|
45
42
|
id: command.entity_id ?? command.command_id,
|
|
46
43
|
order_number: orderNumber,
|
|
47
|
-
callout_number:
|
|
44
|
+
callout_number: generateLocalCalloutNumber(),
|
|
48
45
|
order_type: payload.order_type ?? "takeout",
|
|
49
46
|
is_pos: true,
|
|
50
47
|
customer_name: payload.customer_name ?? null,
|
|
@@ -104,7 +101,7 @@ function injectLocalOrderNumber(command) {
|
|
|
104
101
|
...command,
|
|
105
102
|
payload: {
|
|
106
103
|
...payload,
|
|
107
|
-
order_number:
|
|
104
|
+
order_number: generateLocalOrderNumber(),
|
|
108
105
|
},
|
|
109
106
|
};
|
|
110
107
|
}
|
|
@@ -121,19 +118,19 @@ class CommandQueueService {
|
|
|
121
118
|
* Returns an optimistic domain event so the caller can update the UI.
|
|
122
119
|
*/
|
|
123
120
|
async enqueue(command) {
|
|
124
|
-
const db =
|
|
121
|
+
const db = getSyncDatabase();
|
|
125
122
|
const preparedCommand = injectLocalOrderNumber(command);
|
|
126
|
-
await
|
|
127
|
-
|
|
128
|
-
const pending = await
|
|
129
|
-
|
|
123
|
+
await insertCommand(db, { command: preparedCommand, status: "pending" });
|
|
124
|
+
syncLogger.command(preparedCommand, "queued");
|
|
125
|
+
const pending = await getPendingCommands(db, Number.MAX_SAFE_INTEGER);
|
|
126
|
+
syncLogger.info("CommandQueueService", "Command queued offline", {
|
|
130
127
|
command_id: preparedCommand.command_id,
|
|
131
128
|
command_type: preparedCommand.command_type,
|
|
132
129
|
order_number: preparedCommand.payload.order_number ?? null,
|
|
133
130
|
outbox_size: pending.length,
|
|
134
131
|
});
|
|
135
132
|
const event = buildOptimisticEvent(preparedCommand);
|
|
136
|
-
|
|
133
|
+
syncLogger.info("CommandQueueService", "Optimistic event built", {
|
|
137
134
|
event_type: event.event_type,
|
|
138
135
|
entity_id: event.entity_id,
|
|
139
136
|
order_number: event.payload.order_number ?? null,
|
|
@@ -143,10 +140,10 @@ class CommandQueueService {
|
|
|
143
140
|
// rehydrated (and shown) while still offline.
|
|
144
141
|
if (preparedCommand.command_type === "order:create") {
|
|
145
142
|
try {
|
|
146
|
-
await
|
|
143
|
+
await insertEvent(db, { event, origin: "local" });
|
|
147
144
|
}
|
|
148
145
|
catch (error) {
|
|
149
|
-
|
|
146
|
+
syncLogger.error("CommandQueueService", "Failed to persist optimistic event", {
|
|
150
147
|
error: String(error),
|
|
151
148
|
});
|
|
152
149
|
}
|
|
@@ -157,7 +154,7 @@ class CommandQueueService {
|
|
|
157
154
|
* Try to execute a single pending command through the current transport.
|
|
158
155
|
*/
|
|
159
156
|
async processCommand(command) {
|
|
160
|
-
const db =
|
|
157
|
+
const db = getSyncDatabase();
|
|
161
158
|
const parsedCommand = {
|
|
162
159
|
command_id: command.command_id,
|
|
163
160
|
command_type: command.command_type,
|
|
@@ -170,20 +167,20 @@ class CommandQueueService {
|
|
|
170
167
|
payload: command.payload,
|
|
171
168
|
};
|
|
172
169
|
if (command.retry_count >= MAX_RETRIES) {
|
|
173
|
-
await
|
|
170
|
+
await updateCommandStatus(db, command.command_id, "failed", "Max retry count exceeded");
|
|
174
171
|
return;
|
|
175
172
|
}
|
|
176
173
|
if (!this.syncEngine) {
|
|
177
174
|
throw new Error("CommandQueueService: SyncEngine not initialized");
|
|
178
175
|
}
|
|
179
|
-
await
|
|
176
|
+
await markCommandInFlight(db, command.command_id);
|
|
180
177
|
try {
|
|
181
178
|
const events = await this.syncEngine.execute(parsedCommand);
|
|
182
|
-
await
|
|
183
|
-
|
|
179
|
+
await updateCommandStatus(db, command.command_id, "completed");
|
|
180
|
+
syncLogger.command(parsedCommand, "completed", {
|
|
184
181
|
event_count: events.length,
|
|
185
182
|
});
|
|
186
|
-
|
|
183
|
+
syncLogger.info("CommandQueueService", "Queued command replayed", {
|
|
187
184
|
command_id: command.command_id,
|
|
188
185
|
command_type: command.command_type,
|
|
189
186
|
event_count: events.length,
|
|
@@ -191,8 +188,8 @@ class CommandQueueService {
|
|
|
191
188
|
}
|
|
192
189
|
catch (error) {
|
|
193
190
|
const message = error instanceof Error ? error.message : String(error);
|
|
194
|
-
await
|
|
195
|
-
|
|
191
|
+
await updateCommandStatus(db, command.command_id, "failed", message);
|
|
192
|
+
syncLogger.command(parsedCommand, "failed", { error: message });
|
|
196
193
|
throw error;
|
|
197
194
|
}
|
|
198
195
|
}
|
|
@@ -202,7 +199,7 @@ class CommandQueueService {
|
|
|
202
199
|
*/
|
|
203
200
|
async replay() {
|
|
204
201
|
if (this.replaying) {
|
|
205
|
-
|
|
202
|
+
syncLogger.debug("CommandQueueService", "Replay already in progress");
|
|
206
203
|
return;
|
|
207
204
|
}
|
|
208
205
|
this.replaying = true;
|
|
@@ -210,11 +207,11 @@ class CommandQueueService {
|
|
|
210
207
|
// Await open (not getSyncDatabase) so a cold-start replay triggered by a
|
|
211
208
|
// connectivity event that fires before the DB open completes does not
|
|
212
209
|
// throw "Sync database not initialized".
|
|
213
|
-
const db = await
|
|
214
|
-
const pending = await
|
|
210
|
+
const db = await openSyncDatabase();
|
|
211
|
+
const pending = await getPendingCommands(db);
|
|
215
212
|
if (pending.length === 0)
|
|
216
213
|
return;
|
|
217
|
-
|
|
214
|
+
syncLogger.info("CommandQueueService", "Replaying commands", {
|
|
218
215
|
count: pending.length,
|
|
219
216
|
});
|
|
220
217
|
for (const command of pending) {
|
|
@@ -235,9 +232,9 @@ class CommandQueueService {
|
|
|
235
232
|
* Cancel a queued command that has not yet been transmitted.
|
|
236
233
|
*/
|
|
237
234
|
async cancel(commandId) {
|
|
238
|
-
const db =
|
|
239
|
-
await
|
|
240
|
-
|
|
235
|
+
const db = getSyncDatabase();
|
|
236
|
+
await deleteCommand(db, commandId);
|
|
237
|
+
syncLogger.debug("CommandQueueService", "Command cancelled", {
|
|
241
238
|
commandId,
|
|
242
239
|
});
|
|
243
240
|
}
|
|
@@ -245,9 +242,9 @@ class CommandQueueService {
|
|
|
245
242
|
* Return the number of commands currently in the outbox.
|
|
246
243
|
*/
|
|
247
244
|
async getPendingCount() {
|
|
248
|
-
const db =
|
|
249
|
-
const commands = await
|
|
245
|
+
const db = getSyncDatabase();
|
|
246
|
+
const commands = await getPendingCommands(db, Number.MAX_SAFE_INTEGER);
|
|
250
247
|
return commands.length;
|
|
251
248
|
}
|
|
252
249
|
}
|
|
253
|
-
|
|
250
|
+
export const commandQueueService = new CommandQueueService();
|
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
|
}
|