@teincfood/core 0.10.16 → 0.10.17
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.
|
@@ -60,6 +60,18 @@ declare class LocalNodeServiceCore {
|
|
|
60
60
|
private storeUnsub;
|
|
61
61
|
private setupStoreSubscriptions;
|
|
62
62
|
private teardownStoreSubscriptions;
|
|
63
|
+
/**
|
|
64
|
+
* Apply an event received over the LAN, regardless of whether it arrived on
|
|
65
|
+
* a client socket (`peer`) or was relayed through a node by another terminal
|
|
66
|
+
* (`relay`).
|
|
67
|
+
*
|
|
68
|
+
* Both paths must materialize order rows and invalidate the read model. The
|
|
69
|
+
* relay path previously only applied reference-data events, so an order
|
|
70
|
+
* created on a phone while it had internet (cloud route) was broadcast to
|
|
71
|
+
* the LAN but silently dropped by the peer node — the other terminal never
|
|
72
|
+
* saw it until a later reconciliation.
|
|
73
|
+
*/
|
|
74
|
+
private applyLanEvent;
|
|
63
75
|
private startAsNode;
|
|
64
76
|
/**
|
|
65
77
|
* (Re)publish this node's mDNS advertisement. Isolated from server bring-up
|
|
@@ -422,6 +422,92 @@ class LocalNodeServiceCore {
|
|
|
422
422
|
this.storeUnsub = null;
|
|
423
423
|
this.subscribed = false;
|
|
424
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Apply an event received over the LAN, regardless of whether it arrived on
|
|
427
|
+
* a client socket (`peer`) or was relayed through a node by another terminal
|
|
428
|
+
* (`relay`).
|
|
429
|
+
*
|
|
430
|
+
* Both paths must materialize order rows and invalidate the read model. The
|
|
431
|
+
* relay path previously only applied reference-data events, so an order
|
|
432
|
+
* created on a phone while it had internet (cloud route) was broadcast to
|
|
433
|
+
* the LAN but silently dropped by the peer node — the other terminal never
|
|
434
|
+
* saw it until a later reconciliation.
|
|
435
|
+
*/
|
|
436
|
+
applyLanEvent(event, source) {
|
|
437
|
+
syncLogger.event(event, "received", { origin: "lan", source });
|
|
438
|
+
const deps = this.getDeps();
|
|
439
|
+
const eventType = event.event_type;
|
|
440
|
+
// Materialize LAN order events into the canonical orders table so
|
|
441
|
+
// list queries (POS In Progress, KDS) converge without the backend.
|
|
442
|
+
// Previously only order:sync inserted rows: a cloud/local-shape
|
|
443
|
+
// order.created from a peer fell through to the pending-store apply
|
|
444
|
+
// fns (which never touch the orders table), so the event arrived,
|
|
445
|
+
// every layer reported success, and the row never materialized.
|
|
446
|
+
if (eventType === "order:sync" || eventType === "order.created" || eventType === "order.status_changed") {
|
|
447
|
+
void (async () => {
|
|
448
|
+
try {
|
|
449
|
+
const p = event.payload;
|
|
450
|
+
const bid = deps.getBusiness()?.id;
|
|
451
|
+
const payloadBid = p.business_id;
|
|
452
|
+
if (typeof payloadBid === "string" && bid && payloadBid !== bid) {
|
|
453
|
+
syncLogger.warn("LocalNodeService", "Ignoring LAN order event for another business", {
|
|
454
|
+
event_type: eventType,
|
|
455
|
+
payloadBid,
|
|
456
|
+
bid,
|
|
457
|
+
});
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (p && typeof p.order_number === "string" && p.id) {
|
|
461
|
+
// Full-order payload (order:sync publish or order.created /
|
|
462
|
+
// status_changed echo): upsert the canonical row. HLC guard
|
|
463
|
+
// inside insertOrder keeps this idempotent across mesh + phx
|
|
464
|
+
// double delivery.
|
|
465
|
+
const db = await openSyncDatabase();
|
|
466
|
+
await insertOrder(db, p);
|
|
467
|
+
syncLogger.info("LocalNodeService", "LAN order row materialized", {
|
|
468
|
+
event_type: eventType,
|
|
469
|
+
order_number: p.order_number,
|
|
470
|
+
source,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
// Action-shaped payload (order:sync from a LAN command):
|
|
475
|
+
// apply the transition to the local row.
|
|
476
|
+
const action = p.action;
|
|
477
|
+
const orderId = p.orderId ?? p.order_id;
|
|
478
|
+
if (action && orderId && bid) {
|
|
479
|
+
try {
|
|
480
|
+
await transitionLocalOrder(bid, String(orderId), action);
|
|
481
|
+
syncLogger.info("LocalNodeService", "LAN order transition applied", {
|
|
482
|
+
event_type: eventType,
|
|
483
|
+
action,
|
|
484
|
+
orderId: String(orderId),
|
|
485
|
+
source,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
catch { }
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
syncLogger.warn("LocalNodeService", "LAN order event has no materializable payload", {
|
|
492
|
+
event_type: eventType,
|
|
493
|
+
entity_id: event.entity_id,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
catch (e) {
|
|
499
|
+
syncLogger.warn("LocalNodeService", "LAN order apply failed", { err: String(e), event_type: eventType });
|
|
500
|
+
}
|
|
501
|
+
// The row is in SQLite now — invalidate so lists/detail refetch.
|
|
502
|
+
// (LAN-applied writes bypass the engine pipeline, so no subscriber
|
|
503
|
+
// does this for us.)
|
|
504
|
+
notifyPendingOrdersChanged();
|
|
505
|
+
})();
|
|
506
|
+
}
|
|
507
|
+
applyPendingOrderEvent(event, "lan").catch((e) => syncLogger.error("LocalNodeService", "Failed to apply LAN event", { error: String(e), event_type: event.event_type }));
|
|
508
|
+
applyReferenceChangeEvent(event, "lan").catch((e) => syncLogger.error("LocalNodeService", "Failed to apply LAN reference", { error: String(e), event_type: event.event_type }));
|
|
509
|
+
applyOrderStateChangeEvent(event, "lan").catch((e) => syncLogger.error("LocalNodeService", "Failed to apply LAN order state", { error: String(e), event_type: event.event_type }));
|
|
510
|
+
}
|
|
425
511
|
async startAsNode(port) {
|
|
426
512
|
if (!this.businessId || !this.secret)
|
|
427
513
|
throw new Error("LocalNodeService: business context missing");
|
|
@@ -435,7 +521,7 @@ class LocalNodeServiceCore {
|
|
|
435
521
|
return event ? [event] : [];
|
|
436
522
|
},
|
|
437
523
|
onRelayEvent: (event) => {
|
|
438
|
-
|
|
524
|
+
this.applyLanEvent(event, "relay");
|
|
439
525
|
},
|
|
440
526
|
onClientConnected: (info) => {
|
|
441
527
|
localNodeStore.getState().addConnectedClient({ deviceId: info.deviceId, deviceName: info.deviceName, host: info.host, connectedAt: new Date().toISOString() });
|
|
@@ -609,76 +695,7 @@ class LocalNodeServiceCore {
|
|
|
609
695
|
businessId: this.businessId,
|
|
610
696
|
secret: this.secret,
|
|
611
697
|
onEvent: (event) => {
|
|
612
|
-
|
|
613
|
-
const eventType = event.event_type;
|
|
614
|
-
// Materialize LAN order events into the canonical orders table so
|
|
615
|
-
// list queries (POS In Progress, KDS) converge without the backend.
|
|
616
|
-
// Previously only order:sync inserted rows: a cloud/local-shape
|
|
617
|
-
// order.created from a peer fell through to the pending-store apply
|
|
618
|
-
// fns (which never touch the orders table), so the event arrived,
|
|
619
|
-
// every layer reported success, and the row never materialized.
|
|
620
|
-
if (eventType === "order:sync" || eventType === "order.created" || eventType === "order.status_changed") {
|
|
621
|
-
void (async () => {
|
|
622
|
-
try {
|
|
623
|
-
const p = event.payload;
|
|
624
|
-
const bid = deps.getBusiness()?.id;
|
|
625
|
-
const payloadBid = p.business_id;
|
|
626
|
-
if (typeof payloadBid === "string" && bid && payloadBid !== bid) {
|
|
627
|
-
syncLogger.warn("LocalNodeService", "Ignoring LAN order event for another business", {
|
|
628
|
-
event_type: eventType,
|
|
629
|
-
payloadBid,
|
|
630
|
-
bid,
|
|
631
|
-
});
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
if (p && typeof p.order_number === "string" && p.id) {
|
|
635
|
-
// Full-order payload (order:sync publish or order.created /
|
|
636
|
-
// status_changed echo): upsert the canonical row. HLC guard
|
|
637
|
-
// inside insertOrder keeps this idempotent across mesh + phx
|
|
638
|
-
// double delivery.
|
|
639
|
-
const db = await openSyncDatabase();
|
|
640
|
-
await insertOrder(db, p);
|
|
641
|
-
syncLogger.info("LocalNodeService", "LAN order row materialized", {
|
|
642
|
-
event_type: eventType,
|
|
643
|
-
order_number: p.order_number,
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
else {
|
|
647
|
-
// Action-shaped payload (order:sync from a LAN command):
|
|
648
|
-
// apply the transition to the local row.
|
|
649
|
-
const action = p.action;
|
|
650
|
-
const orderId = p.orderId ?? p.order_id;
|
|
651
|
-
if (action && orderId && bid) {
|
|
652
|
-
try {
|
|
653
|
-
await transitionLocalOrder(bid, String(orderId), action);
|
|
654
|
-
syncLogger.info("LocalNodeService", "LAN order transition applied", {
|
|
655
|
-
event_type: eventType,
|
|
656
|
-
action,
|
|
657
|
-
orderId: String(orderId),
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
|
-
catch { }
|
|
661
|
-
}
|
|
662
|
-
else {
|
|
663
|
-
syncLogger.warn("LocalNodeService", "LAN order event has no materializable payload", {
|
|
664
|
-
event_type: eventType,
|
|
665
|
-
entity_id: event.entity_id,
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
catch (e) {
|
|
671
|
-
syncLogger.warn("LocalNodeService", "LAN order apply failed", { err: String(e), event_type: eventType });
|
|
672
|
-
}
|
|
673
|
-
// The row is in SQLite now — invalidate so lists/detail refetch.
|
|
674
|
-
// (LAN-applied writes bypass the engine pipeline, so no
|
|
675
|
-
// subscriber does this for us.)
|
|
676
|
-
notifyPendingOrdersChanged();
|
|
677
|
-
})();
|
|
678
|
-
}
|
|
679
|
-
applyPendingOrderEvent(event, "lan").catch((e) => syncLogger.error("LocalNodeService", "Failed to apply LAN event", { error: String(e), event_type: event.event_type }));
|
|
680
|
-
applyReferenceChangeEvent(event, "lan").catch((e) => syncLogger.error("LocalNodeService", "Failed to apply LAN reference", { error: String(e), event_type: event.event_type }));
|
|
681
|
-
applyOrderStateChangeEvent(event, "lan").catch((e) => syncLogger.error("LocalNodeService", "Failed to apply LAN order state", { error: String(e), event_type: event.event_type }));
|
|
698
|
+
this.applyLanEvent(event, "peer");
|
|
682
699
|
},
|
|
683
700
|
onConnected: () => {
|
|
684
701
|
this.settleDial(node.deviceId);
|
|
@@ -96,15 +96,22 @@ export function useSyncReconciliation() {
|
|
|
96
96
|
// The node relays engine events (e.g. order.created when a queued
|
|
97
97
|
// command is replayed) to LAN clients so they reconcile their pending
|
|
98
98
|
// orders and refresh caches.
|
|
99
|
+
//
|
|
100
|
+
// A terminal that is only a LAN client (its own listener failed to bind,
|
|
101
|
+
// or it is a phone in the background) also has to forward the events it
|
|
102
|
+
// produces locally. Otherwise a sale completed over the cloud while the
|
|
103
|
+
// phone has mobile data — the exact iOS Wi-Fi + LTE case — never reaches
|
|
104
|
+
// the node until a later full sync. Reference events were already
|
|
105
|
+
// forwarded here; order/payment/delivery events need the same path.
|
|
106
|
+
const relayable = event.event_type === REFERENCE_CHANGED_EVENT_TYPE ||
|
|
107
|
+
event.event_type.startsWith("order.") ||
|
|
108
|
+
event.event_type.startsWith("order:") ||
|
|
109
|
+
event.event_type.startsWith("payment.") ||
|
|
110
|
+
event.event_type.startsWith("delivery.");
|
|
99
111
|
if (localNodeService.isLocalNode()) {
|
|
100
112
|
localNodeService.broadcastToClients(event);
|
|
101
113
|
}
|
|
102
|
-
|
|
103
|
-
// node): forward the event to the node so it rebroadcasts it to every
|
|
104
|
-
// other terminal (the node also applies it locally via its onRelayEvent
|
|
105
|
-
// hook). The node does not echo it back to this device.
|
|
106
|
-
if (event.event_type === REFERENCE_CHANGED_EVENT_TYPE &&
|
|
107
|
-
!localNodeService.isLocalNode()) {
|
|
114
|
+
else if (relayable) {
|
|
108
115
|
localNodeService.getClient()?.publishEvent?.(event);
|
|
109
116
|
}
|
|
110
117
|
// Where the event came from: a Phoenix push is "cloud", a LAN relay is
|
package/package.json
CHANGED