@teincfood/core 0.8.1 → 0.8.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.
@@ -221,18 +221,42 @@ class LocalNodeServiceCore {
221
221
  return first ?? null;
222
222
  }
223
223
  broadcastToClients(event) {
224
- // Mesh broadcast to all peers
224
+ // Mesh broadcast to all peers. Per-peer failures are LOUD (warn with
225
+ // peer id + error): a silent catch here once hid a full mesh blackout
226
+ // class of bug — peers that never receive order events never converge.
227
+ let delivered = 0;
228
+ let failed = 0;
225
229
  for (const c of this.peers.values()) {
226
230
  try {
227
231
  c.publishEvent?.(event);
232
+ delivered++;
233
+ }
234
+ catch (e) {
235
+ failed++;
236
+ syncLogger.warn("LocalNodeService", "Mesh publish failed", {
237
+ event_type: event.event_type,
238
+ entity_id: event.entity_id,
239
+ error: String(e),
240
+ });
228
241
  }
229
- catch { }
230
242
  }
231
243
  // Also via server broadcast to connected clients
232
244
  try {
233
245
  this.server?.broadcast(event);
234
246
  }
235
- catch { }
247
+ catch (e) {
248
+ syncLogger.warn("LocalNodeService", "Mesh server broadcast failed", {
249
+ event_type: event.event_type,
250
+ entity_id: event.entity_id,
251
+ error: String(e),
252
+ });
253
+ }
254
+ syncLogger.debug("LocalNodeService", "Mesh broadcast", {
255
+ event_type: event.event_type,
256
+ entity_id: event.entity_id,
257
+ peers: delivered,
258
+ failed,
259
+ });
236
260
  }
237
261
  setupStoreSubscriptions() {
238
262
  if (this.subscribed)
@@ -467,32 +491,69 @@ class LocalNodeServiceCore {
467
491
  secret: this.secret,
468
492
  onEvent: (event) => {
469
493
  syncLogger.event(event, "received", { origin: "lan" });
470
- if (event.event_type === "order:sync") {
494
+ const eventType = event.event_type;
495
+ // Materialize LAN order events into the canonical orders table so
496
+ // list queries (POS In Progress, KDS) converge without the backend.
497
+ // Previously only order:sync inserted rows: a cloud/local-shape
498
+ // order.created from a peer fell through to the pending-store apply
499
+ // fns (which never touch the orders table), so the event arrived,
500
+ // every layer reported success, and the row never materialized.
501
+ if (eventType === "order:sync" || eventType === "order.created" || eventType === "order.status_changed") {
471
502
  void (async () => {
472
503
  try {
473
504
  const p = event.payload;
505
+ const bid = deps.getBusiness()?.id;
506
+ const payloadBid = p.business_id;
507
+ if (typeof payloadBid === "string" && bid && payloadBid !== bid) {
508
+ syncLogger.warn("LocalNodeService", "Ignoring LAN order event for another business", {
509
+ event_type: eventType,
510
+ payloadBid,
511
+ bid,
512
+ });
513
+ return;
514
+ }
474
515
  if (p && typeof p.order_number === "string" && p.id) {
516
+ // Full-order payload (order:sync publish or order.created /
517
+ // status_changed echo): upsert the canonical row. HLC guard
518
+ // inside insertOrder keeps this idempotent across mesh + phx
519
+ // double delivery.
475
520
  const db = await openSyncDatabase();
476
521
  await insertOrder(db, p);
522
+ syncLogger.info("LocalNodeService", "LAN order row materialized", {
523
+ event_type: eventType,
524
+ order_number: p.order_number,
525
+ });
477
526
  }
478
527
  else {
528
+ // Action-shaped payload (order:sync from a LAN command):
529
+ // apply the transition to the local row.
479
530
  const action = p.action;
480
- const orderId = p.orderId;
481
- const bid = deps.getBusiness()?.id;
531
+ const orderId = p.orderId ?? p.order_id;
482
532
  if (action && orderId && bid) {
483
533
  try {
484
534
  await transitionLocalOrder(bid, String(orderId), action);
535
+ syncLogger.info("LocalNodeService", "LAN order transition applied", {
536
+ event_type: eventType,
537
+ action,
538
+ orderId: String(orderId),
539
+ });
485
540
  }
486
541
  catch { }
487
542
  }
543
+ else {
544
+ syncLogger.warn("LocalNodeService", "LAN order event has no materializable payload", {
545
+ event_type: eventType,
546
+ entity_id: event.entity_id,
547
+ });
548
+ }
488
549
  }
489
550
  }
490
551
  catch (e) {
491
- syncLogger.debug("LocalNodeService", "LAN order:sync apply failed", { err: String(e) });
552
+ syncLogger.warn("LocalNodeService", "LAN order apply failed", { err: String(e), event_type: eventType });
492
553
  }
493
554
  // The row is in SQLite now — invalidate so lists/detail refetch.
494
- // (order:sync bypasses the engine pipeline, so no subscriber does
495
- // this for us.)
555
+ // (LAN-applied writes bypass the engine pipeline, so no
556
+ // subscriber does this for us.)
496
557
  notifyPendingOrdersChanged();
497
558
  })();
498
559
  }
@@ -1,4 +1,5 @@
1
1
  import { phoenixSocket } from "./phoenix-socket";
2
+ import { syncLogger } from "../utils/sync-logger";
2
3
  class EventsService {
3
4
  constructor() {
4
5
  this.refCount = 0;
@@ -29,6 +30,22 @@ class EventsService {
29
30
  this.lastSequence = sequence;
30
31
  else if (typeof data?._sequence === "number")
31
32
  this.lastSequence = data._sequence;
33
+ // Business-state frames are LOUD (info): when live updates stall,
34
+ // these lines prove whether the backend pushed anything at all.
35
+ if (event.startsWith("order.") ||
36
+ event.startsWith("delivery.") ||
37
+ event.startsWith("payment.") ||
38
+ event === "reference_data.changed") {
39
+ try {
40
+ syncLogger.info("EventsService", "Phoenix event received", {
41
+ topic,
42
+ event,
43
+ order_id: data?.order_id ?? data?.id,
44
+ business_id: payloadBid,
45
+ });
46
+ }
47
+ catch { }
48
+ }
32
49
  const typeListeners = this.listeners.get(event);
33
50
  if (typeListeners)
34
51
  typeListeners.forEach((cb) => cb(data));
@@ -39,7 +56,30 @@ class EventsService {
39
56
  joinPayload.last_sequence = this.lastSequence;
40
57
  if (this.activeBusinessId)
41
58
  joinPayload.business_id = this.activeBusinessId;
42
- phoenixSocket.connect().then(() => phoenixSocket.joinChannel(topic, joinPayload)).then(() => { this.joined = true; }).catch(() => { });
59
+ phoenixSocket
60
+ .connect()
61
+ .then(() => phoenixSocket.joinChannel(topic, joinPayload))
62
+ .then(() => {
63
+ this.joined = true;
64
+ try {
65
+ syncLogger.info("EventsService", "Phoenix channel joined", {
66
+ topic,
67
+ business_id: this.activeBusinessId,
68
+ });
69
+ }
70
+ catch { }
71
+ })
72
+ .catch((e) => {
73
+ // A failed join previously vanished into a swallowed rejection:
74
+ // socket "connected" but subscribed to nothing. Loud from now on.
75
+ try {
76
+ syncLogger.warn("EventsService", "Phoenix channel join failed", {
77
+ topic,
78
+ error: String(e),
79
+ });
80
+ }
81
+ catch { }
82
+ });
43
83
  }
44
84
  else if (businessId !== undefined) {
45
85
  // business switched while already joined — update filter without rejoin (next events filtered). If backend requires business:{id} subscription, re-join could be forced — not needed as fallback business_authorized check still passes.
@@ -296,10 +296,18 @@ export async function applyPendingOrderEvent(event, origin = "local") {
296
296
  }
297
297
  if (event.event_type === "order.created") {
298
298
  const payload = event.payload;
299
+ // Local-shape echoes carry local_order_number; cloud-shape echoes
300
+ // (backend id/number, e.g. received over LAN from a peer) carry only
301
+ // order_number. Match on either so both shapes clear the optimistic
302
+ // entry and converge snapshots — previously cloud-shape events hit the
303
+ // early return and triggered no invalidation at all.
299
304
  const localOrderNumber = payload?.local_order_number;
300
- if (typeof localOrderNumber !== "string")
305
+ const matchNumber = typeof localOrderNumber === "string"
306
+ ? localOrderNumber
307
+ : payload?.order_number;
308
+ if (typeof matchNumber !== "string")
301
309
  return;
302
- usePendingOrdersStore.getState().removeByOrderNumber(localOrderNumber);
310
+ usePendingOrdersStore.getState().removeByOrderNumber(matchNumber);
303
311
  // Persist the cloud acknowledgment into the durable journal + read model.
304
312
  await orderSnapshotsService.persistOrderEvent(event, "cloud");
305
313
  const db = await openSyncDatabase();
@@ -313,7 +321,7 @@ export async function applyPendingOrderEvent(event, origin = "local") {
313
321
  if (e.event_type !== PENDING_EVENT_TYPE)
314
322
  continue;
315
323
  if (e.payload?.order_number ===
316
- localOrderNumber) {
324
+ matchNumber) {
317
325
  ids.push(e.event_id);
318
326
  }
319
327
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teincfood/core",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
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",