@teincfood/core 0.9.22 → 0.9.24
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/db/operations.d.ts +31 -0
- package/dist/db/operations.js +55 -0
- package/dist/domain/orders/service.js +25 -13
- package/dist/hooks/delivery.hooks.d.ts +24 -24
- package/dist/hooks/realtime-stubs.d.ts +26 -19
- package/dist/hooks/realtime-stubs.js +29 -124
- package/dist/hooks/rider.hooks.d.ts +48 -48
- package/dist/hooks/use-infinite-list.d.ts +24 -24
- package/dist/hooks/wallet.hooks.d.ts +24 -24
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/realtime/events.service.d.ts +9 -0
- package/dist/realtime/events.service.js +37 -7
- package/dist/realtime/realtime-bridge.d.ts +30 -0
- package/dist/realtime/realtime-bridge.js +149 -0
- package/dist/reference/events.d.ts +1 -1
- package/dist/reference/service.d.ts +10 -1
- package/dist/reference/service.js +79 -19
- package/dist/sync/command-queue.service.d.ts +6 -5
- package/dist/sync/command-queue.service.js +10 -14
- package/dist/sync/full-sync.js +9 -5
- package/dist/sync/order-merge.d.ts +25 -0
- package/dist/sync/order-merge.js +61 -0
- package/dist/sync/pending-orders.service.d.ts +2 -2
- package/dist/sync/sync-jobs.service.d.ts +22 -1
- package/dist/sync/sync-jobs.service.js +81 -22
- package/dist/sync/types.d.ts +7 -0
- package/dist/sync/use-sync-reconciliation.js +12 -3
- package/dist/test-helpers/memory-driver.js +26 -5
- package/package.json +2 -2
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Core-only, single in_flight per business, coalesced via isOnline gate.
|
|
5
5
|
*/
|
|
6
6
|
import { openSyncDatabase } from "../db/connection";
|
|
7
|
-
import {
|
|
7
|
+
import { markOrderSyncFailed } from "../db/operations";
|
|
8
|
+
import { mergeOrderFromServer } from "./order-merge";
|
|
8
9
|
import { getConnectivityState, subscribeConnectivity } from "../adapters/connectivity";
|
|
9
10
|
import { getRefreshToken, setAuthTokens, clearAuth, subscribeAuth } from "../adapters/auth";
|
|
10
11
|
import { getActiveBusiness, subscribeBusiness } from "../adapters/business";
|
|
@@ -267,22 +268,19 @@ async function tryRest(businessId, payload, commandType) {
|
|
|
267
268
|
}
|
|
268
269
|
}
|
|
269
270
|
/**
|
|
270
|
-
* Already-applied absorb for sync_jobs rows:
|
|
271
|
-
* order
|
|
272
|
-
*
|
|
273
|
-
*
|
|
271
|
+
* Already-applied absorb for sync_jobs rows: the server already moved the
|
|
272
|
+
* order to this command's target status, so the queued job is redundant.
|
|
273
|
+
*
|
|
274
|
+
* The comparison uses the SERVER status reported by the pull — never the
|
|
275
|
+
* local row, which may show the device's own queued intent.
|
|
274
276
|
*/
|
|
275
277
|
async function absorbSyncJobIfApplied(db, businessId, entityId, commandType, jobId) {
|
|
276
278
|
const target = getQueuedStatus(commandType);
|
|
277
279
|
if (!target || !entityId)
|
|
278
280
|
return false;
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (!afterHlc || afterHlc.hlc === before?.hlc)
|
|
283
|
-
return false;
|
|
284
|
-
const order = await getOrderById(db, businessId, entityId);
|
|
285
|
-
if (order && order.status === target) {
|
|
281
|
+
const serverStatuses = await pullRemoteOrders(businessId);
|
|
282
|
+
const serverStatus = serverStatuses.get(entityId);
|
|
283
|
+
if (serverStatus === target) {
|
|
286
284
|
await db.runAsync(`UPDATE sync_jobs SET status='completed', updated_at=? WHERE id=?;`, new Date().toISOString(), jobId);
|
|
287
285
|
syncLogger.info("SyncJobs", "Transition already applied on server, absorbing job", {
|
|
288
286
|
businessId,
|
|
@@ -379,7 +377,10 @@ export async function drainOne(businessId, entityId) {
|
|
|
379
377
|
inflight.set(key, new Promise((r) => { resolveInflight = r; }));
|
|
380
378
|
try {
|
|
381
379
|
const db = await openSyncDatabase();
|
|
382
|
-
|
|
380
|
+
// FIFO with a stable tiebreaker: two commands created in the same
|
|
381
|
+
// millisecond (fast taps) must still drain in insertion order, otherwise
|
|
382
|
+
// a transition can overtake the command it depends on.
|
|
383
|
+
const row = await db.getFirstAsync(`SELECT * FROM sync_jobs WHERE business_id = ? AND entity_id = ? AND status='pending' ORDER BY created_at ASC, rowid ASC LIMIT 1;`, businessId, entityId);
|
|
383
384
|
syncLogger.debug("SyncJobs", "drainOne fetched row", { businessId, entityId, found: !!row, command_type: row?.command_type });
|
|
384
385
|
if (!row) {
|
|
385
386
|
syncLogger.debug("SyncJobs", "drainOne no pending row", { businessId, entityId });
|
|
@@ -423,7 +424,7 @@ export async function drainOne(businessId, entityId) {
|
|
|
423
424
|
syncLogger.info("SyncJobs", "Drained via PHX", { businessId, entityId });
|
|
424
425
|
clearRetry(businessId);
|
|
425
426
|
try {
|
|
426
|
-
const next = await db.getFirstAsync(`SELECT id FROM sync_jobs WHERE business_id=? AND entity_id=? AND status='pending' ORDER BY created_at LIMIT 1;`, businessId, entityId);
|
|
427
|
+
const next = await db.getFirstAsync(`SELECT id FROM sync_jobs WHERE business_id=? AND entity_id=? AND status='pending' ORDER BY created_at ASC, rowid ASC LIMIT 1;`, businessId, entityId);
|
|
427
428
|
if (next)
|
|
428
429
|
setTimeout(() => void drainOne(businessId, entityId), 50);
|
|
429
430
|
}
|
|
@@ -444,7 +445,7 @@ export async function drainOne(businessId, entityId) {
|
|
|
444
445
|
clearRetry(businessId);
|
|
445
446
|
// Immediately drain next pending for same entity (FIFO chain: create -> prepare -> ready -> pickup should sync in order without waiting for timer)
|
|
446
447
|
try {
|
|
447
|
-
const next = await db.getFirstAsync(`SELECT id FROM sync_jobs WHERE business_id=? AND entity_id=? AND status='pending' ORDER BY created_at LIMIT 1;`, businessId, entityId);
|
|
448
|
+
const next = await db.getFirstAsync(`SELECT id FROM sync_jobs WHERE business_id=? AND entity_id=? AND status='pending' ORDER BY created_at ASC, rowid ASC LIMIT 1;`, businessId, entityId);
|
|
448
449
|
if (next) {
|
|
449
450
|
syncLogger.debug("SyncJobs", "chaining next job for same entity", { businessId, entityId });
|
|
450
451
|
// fire next without inflight block — we already cleared inflight for this key, so recurse
|
|
@@ -632,27 +633,36 @@ export async function drainOne(businessId, entityId) {
|
|
|
632
633
|
syncLogger.debug("SyncJobs", "drainOne exit", { businessId, entityId });
|
|
633
634
|
}
|
|
634
635
|
}
|
|
636
|
+
/**
|
|
637
|
+
* Pull the server's recent orders into the local read model.
|
|
638
|
+
*
|
|
639
|
+
* Returns the SERVER status per order id. The local row can show a
|
|
640
|
+
* different status (a queued transition keeps the device's intent on top of
|
|
641
|
+
* the server copy), so a caller that needs to know what the server actually
|
|
642
|
+
* holds must read it from this map — never from the merged local row.
|
|
643
|
+
*/
|
|
635
644
|
export async function pullRemoteOrders(businessId) {
|
|
645
|
+
const serverStatuses = new Map();
|
|
636
646
|
if (!businessId || !canBusinessSync(businessId)) {
|
|
637
647
|
syncLogger.debug("SyncJobs", "pullRemoteOrders early-return — no business/auth", { businessId });
|
|
638
|
-
return;
|
|
648
|
+
return serverStatuses;
|
|
639
649
|
}
|
|
640
650
|
const onlineForRest = isOnlineForRest();
|
|
641
651
|
syncLogger.debug("SyncJobs", "pullRemoteOrders entry", { businessId, onlineForRest, hasToken: isAuthenticated() });
|
|
642
652
|
if (!onlineForRest) {
|
|
643
653
|
syncLogger.debug("SyncJobs", "pullRemoteOrders early-return gate", { businessId, onlineForRest, hasToken: isAuthenticated(), connectivity: getConnectivityState() });
|
|
644
|
-
return;
|
|
654
|
+
return serverStatuses;
|
|
645
655
|
}
|
|
646
656
|
// Respect a global 429 cooldown for background pulls.
|
|
647
657
|
if (isRateLimited()) {
|
|
648
658
|
syncLogger.debug("SyncJobs", "pullRemoteOrders skipped — rate limited", { businessId });
|
|
649
|
-
return;
|
|
659
|
+
return serverStatuses;
|
|
650
660
|
}
|
|
651
661
|
try {
|
|
652
662
|
const http = getHttpClient();
|
|
653
663
|
if (!http) {
|
|
654
664
|
syncLogger.debug("SyncJobs", "pullRemoteOrders no http client", { businessId });
|
|
655
|
-
return;
|
|
665
|
+
return serverStatuses;
|
|
656
666
|
}
|
|
657
667
|
// Gradual recent-first pull for single business — page 1 immediate, then background older pages (max 5 pages = 100 recent)
|
|
658
668
|
let page = 1;
|
|
@@ -671,10 +681,14 @@ export async function pullRemoteOrders(businessId) {
|
|
|
671
681
|
const db = await openSyncDatabase();
|
|
672
682
|
for (const o of data) {
|
|
673
683
|
try {
|
|
674
|
-
|
|
684
|
+
// Server truth — but a pending local command keeps its intent
|
|
685
|
+
// (see order-merge). Never let a pull revert queued work.
|
|
686
|
+
await mergeOrderFromServer(db, businessId, o);
|
|
687
|
+
if (o?.id && o?.status)
|
|
688
|
+
serverStatuses.set(o.id, o.status);
|
|
675
689
|
}
|
|
676
690
|
catch (e) {
|
|
677
|
-
syncLogger.debug("SyncJobs", "pullRemoteOrders
|
|
691
|
+
syncLogger.debug("SyncJobs", "pullRemoteOrders merge fail", { err: String(e) });
|
|
678
692
|
}
|
|
679
693
|
}
|
|
680
694
|
// persist page 1 to snapshot for offline fallback
|
|
@@ -706,6 +720,51 @@ export async function pullRemoteOrders(businessId) {
|
|
|
706
720
|
catch (e) {
|
|
707
721
|
syncLogger.debug("SyncJobs", "pullRemoteDeliveries error", { err: String(e) });
|
|
708
722
|
}
|
|
723
|
+
return serverStatuses;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Fetch exactly ONE order from the server and merge it into the local
|
|
727
|
+
* read model.
|
|
728
|
+
*
|
|
729
|
+
* Used when a realtime `order.created` event arrives for an order this
|
|
730
|
+
* device has never seen: the event carries only a summary, so the order has
|
|
731
|
+
* to be fetched — but fetching the whole list (100 orders, several pages,
|
|
732
|
+
* heavy serialization) for one new order is pure waste on the backend.
|
|
733
|
+
*
|
|
734
|
+
* Uses the existing business-scoped list endpoint with an `order_number`
|
|
735
|
+
* filter, so no backend change is required.
|
|
736
|
+
*/
|
|
737
|
+
export async function fetchRemoteOrderByNumber(businessId, orderNumber) {
|
|
738
|
+
if (!businessId || !orderNumber)
|
|
739
|
+
return false;
|
|
740
|
+
if (!canBusinessSync(businessId))
|
|
741
|
+
return false;
|
|
742
|
+
if (!isOnlineForRest())
|
|
743
|
+
return false;
|
|
744
|
+
if (isRateLimited())
|
|
745
|
+
return false;
|
|
746
|
+
try {
|
|
747
|
+
const http = getHttpClient();
|
|
748
|
+
if (!http)
|
|
749
|
+
return false;
|
|
750
|
+
await paceSync();
|
|
751
|
+
const res = await http.get(`/businesses/${businessId}/orders`, { params: { order_number: orderNumber, page_size: 1 } });
|
|
752
|
+
const payload = res.data;
|
|
753
|
+
const rows = payload?.data ?? [];
|
|
754
|
+
const first = rows[0];
|
|
755
|
+
if (!first?.id) {
|
|
756
|
+
syncLogger.debug("SyncJobs", "fetchRemoteOrderByNumber — no row returned", { businessId, orderNumber });
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
const db = await openSyncDatabase();
|
|
760
|
+
await mergeOrderFromServer(db, businessId, first);
|
|
761
|
+
syncLogger.info("SyncJobs", "Fetched single remote order", { businessId, orderNumber, id: first.id });
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
catch (e) {
|
|
765
|
+
syncLogger.debug("SyncJobs", "fetchRemoteOrderByNumber failed", { businessId, orderNumber, err: String(e) });
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
709
768
|
}
|
|
710
769
|
export async function drainPending(businessId) {
|
|
711
770
|
if (!businessId || !canBusinessSync(businessId)) {
|
|
@@ -720,7 +779,7 @@ export async function drainPending(businessId) {
|
|
|
720
779
|
return;
|
|
721
780
|
}
|
|
722
781
|
const db = await openSyncDatabase();
|
|
723
|
-
const rows = await db.getAllAsync(`SELECT entity_id FROM sync_jobs WHERE business_id=? AND status='pending' ORDER BY created_at LIMIT 20;`, businessId);
|
|
782
|
+
const rows = await db.getAllAsync(`SELECT entity_id FROM sync_jobs WHERE business_id=? AND status='pending' ORDER BY created_at ASC, rowid ASC LIMIT 20;`, businessId);
|
|
724
783
|
syncLogger.debug("SyncJobs", "drainPending found rows", { businessId, count: rows.length, rows });
|
|
725
784
|
for (const r of rows) {
|
|
726
785
|
await drainOne(businessId, r.entity_id);
|
package/dist/sync/types.d.ts
CHANGED
|
@@ -27,6 +27,13 @@ export interface EventMetadata {
|
|
|
27
27
|
}
|
|
28
28
|
export interface DomainEvent<T = unknown> extends EventMetadata {
|
|
29
29
|
payload: T;
|
|
30
|
+
/**
|
|
31
|
+
* Where the event came from. Optional for backwards compatibility, but the
|
|
32
|
+
* journal stores it: "local" (this device), "cloud" (Phoenix push) or
|
|
33
|
+
* "lan" (mesh peer). A cloud event must not be re-hydrated as a pending
|
|
34
|
+
* local command after a restart.
|
|
35
|
+
*/
|
|
36
|
+
origin?: "local" | "lan" | "cloud";
|
|
30
37
|
}
|
|
31
38
|
export interface CommandMetadata {
|
|
32
39
|
/** Globally unique command identifier (UUID v4). Used for idempotency. */
|
|
@@ -11,6 +11,7 @@ import { useEffect } from "react";
|
|
|
11
11
|
import { useQueryClient } from "@tanstack/react-query";
|
|
12
12
|
import { syncEngine } from "./engine";
|
|
13
13
|
import { getActiveBusiness } from "../adapters/business";
|
|
14
|
+
import { useGlobalRealtimeBridge } from "../realtime/realtime-bridge";
|
|
14
15
|
import { applyPendingOrderEvent, applyOrderStateChangeEvent, registerPendingOrdersInvalidator, } from "./pending-orders.service";
|
|
15
16
|
import { localNodeService } from "../adapters/local-node";
|
|
16
17
|
import { getSyncDatabase } from "../db/connection";
|
|
@@ -29,6 +30,10 @@ function isLocalOrderEvent(event) {
|
|
|
29
30
|
const RECONCILE_DEBOUNCE_MS = 400;
|
|
30
31
|
export function useSyncReconciliation() {
|
|
31
32
|
const queryClient = useQueryClient();
|
|
33
|
+
// Single app-wide Phoenix subscription. Mounting it here means one root
|
|
34
|
+
// `useSyncReconciliation` is enough for the whole app — screens no longer
|
|
35
|
+
// need their own realtime subscription.
|
|
36
|
+
useGlobalRealtimeBridge();
|
|
32
37
|
useEffect(() => {
|
|
33
38
|
let timer;
|
|
34
39
|
const pending = new Set();
|
|
@@ -102,12 +107,16 @@ export function useSyncReconciliation() {
|
|
|
102
107
|
!localNodeService.isLocalNode()) {
|
|
103
108
|
localNodeService.getClient()?.publishEvent?.(event);
|
|
104
109
|
}
|
|
110
|
+
// Where the event came from: a Phoenix push is "cloud", a LAN relay is
|
|
111
|
+
// "lan", everything produced on this device is "local". The journal
|
|
112
|
+
// stores it so a cloud event is never re-hydrated as pending local work.
|
|
113
|
+
const origin = event.origin ?? "local";
|
|
105
114
|
// Reference data changed (locally or echoed from the cloud): re-sync the
|
|
106
115
|
// affected keys and refresh caches on this device.
|
|
107
|
-
await applyReferenceChangeEvent(event,
|
|
116
|
+
await applyReferenceChangeEvent(event, origin);
|
|
108
117
|
// Order/delivery state changed: refresh order caches so this terminal
|
|
109
118
|
// shows the same order states as the device that made the change.
|
|
110
|
-
await applyOrderStateChangeEvent(event,
|
|
119
|
+
await applyOrderStateChangeEvent(event, origin);
|
|
111
120
|
// Any order/delivery event should refresh dashboard recent orders immediately (fresh on transition)
|
|
112
121
|
if (event.event_type.startsWith("order.") || event.event_type.startsWith("delivery.")) {
|
|
113
122
|
schedule("pending");
|
|
@@ -116,7 +125,7 @@ export function useSyncReconciliation() {
|
|
|
116
125
|
// into the pending-orders store.
|
|
117
126
|
if (event.event_type === "order.create_queued" ||
|
|
118
127
|
event.event_type === "order.created") {
|
|
119
|
-
await applyPendingOrderEvent(event,
|
|
128
|
+
await applyPendingOrderEvent(event, origin);
|
|
120
129
|
}
|
|
121
130
|
if (event.event_type === "order.created" && isLocalOrderEvent(event)) {
|
|
122
131
|
try {
|
|
@@ -8,7 +8,7 @@ export function createMemoryDriver() {
|
|
|
8
8
|
orders: [],
|
|
9
9
|
sync_jobs: [],
|
|
10
10
|
};
|
|
11
|
-
let autoId = { commands: 1, events: 1, migrations: 1 };
|
|
11
|
+
let autoId = { commands: 1, events: 1, migrations: 1, sync_jobs: 1 };
|
|
12
12
|
const driver = {
|
|
13
13
|
async execAsync(_sql) {
|
|
14
14
|
// No-op: CREATE TABLE / INDEX / ALTER all just ensure table array exists.
|
|
@@ -191,9 +191,12 @@ export function createMemoryDriver() {
|
|
|
191
191
|
if (s.includes("INSERT INTO orders")) {
|
|
192
192
|
const [id, business_id, order_number, callout_number, status, order_type, is_pos, items, subtotal_minor, total_minor, tax_minor, tax_lines, payment, customer_name, customer_phone, pos_payment_method, delivery_id, business, user, inserted_at, hlc] = args;
|
|
193
193
|
const existing = tables.orders.find((r) => r.id === id);
|
|
194
|
-
// LWW check via hlc (if existing and incoming older, skip)
|
|
194
|
+
// LWW check via hlc (if existing and incoming older, skip) — only for
|
|
195
|
+
// the local/LAN write path. The server write path (`upsertServerOrder`)
|
|
196
|
+
// has no hlc guard: the server copy always wins.
|
|
197
|
+
const lwwGuarded = s.includes("excluded.hlc >= orders.hlc");
|
|
195
198
|
if (existing) {
|
|
196
|
-
if (hlc && existing.hlc && hlc < existing.hlc)
|
|
199
|
+
if (lwwGuarded && hlc && existing.hlc && hlc < existing.hlc)
|
|
197
200
|
return { lastInsertRowId: 0, changes: 0 };
|
|
198
201
|
Object.assign(existing, { business_id, order_number, callout_number, status, order_type, is_pos, items, subtotal_minor, total_minor, tax_minor, tax_lines, payment, customer_name, customer_phone, pos_payment_method, delivery_id, business, user, inserted_at, hlc });
|
|
199
202
|
return { lastInsertRowId: 0, changes: 1 };
|
|
@@ -206,7 +209,7 @@ export function createMemoryDriver() {
|
|
|
206
209
|
const [id, business_id, command_type, entity_id, payload, created_at, updated_at] = args;
|
|
207
210
|
if (tables.sync_jobs.find((r) => r.id === id))
|
|
208
211
|
return { lastInsertRowId: 0, changes: 0 };
|
|
209
|
-
tables.sync_jobs.push({ id, business_id, command_type, entity_id, payload, status: "pending", attempts: 0, created_at, updated_at });
|
|
212
|
+
tables.sync_jobs.push({ rowid: autoId.sync_jobs++, id, business_id, command_type, entity_id, payload, status: "pending", attempts: 0, created_at, updated_at });
|
|
210
213
|
return { lastInsertRowId: 1, changes: 1 };
|
|
211
214
|
}
|
|
212
215
|
// sync_jobs update status=completed
|
|
@@ -365,6 +368,15 @@ export function createMemoryDriver() {
|
|
|
365
368
|
const row = tables.sync_jobs.find((r) => r.business_id === business_id && r.entity_id === entity_id && r.status === "pending");
|
|
366
369
|
return row ?? null;
|
|
367
370
|
}
|
|
371
|
+
// sync_jobs duplicate-command probe (transition coalescing)
|
|
372
|
+
if (s.includes("SELECT id FROM sync_jobs WHERE business_id = ? AND entity_id = ? AND command_type = ?")) {
|
|
373
|
+
const [business_id, entity_id, command_type] = args;
|
|
374
|
+
const row = tables.sync_jobs.find((r) => r.business_id === business_id &&
|
|
375
|
+
r.entity_id === entity_id &&
|
|
376
|
+
r.command_type === command_type &&
|
|
377
|
+
r.status === "pending");
|
|
378
|
+
return row ?? null;
|
|
379
|
+
}
|
|
368
380
|
// generic orders exists check for migrations backfill: SELECT * FROM orders WHERE business_id=? (not needed)
|
|
369
381
|
return null;
|
|
370
382
|
},
|
|
@@ -479,10 +491,19 @@ export function createMemoryDriver() {
|
|
|
479
491
|
const [business_id] = args;
|
|
480
492
|
const m = s.match(/LIMIT\s+(\d+)/);
|
|
481
493
|
const lim = m ? parseInt(m[1], 10) : 20;
|
|
482
|
-
let rows = tables.sync_jobs.filter((r) => r.business_id === business_id && r.status === "pending").sort((a, b) => (a.created_at < b.created_at ? -1 : 1)).map((r) => ({ entity_id: r.entity_id }));
|
|
494
|
+
let rows = tables.sync_jobs.filter((r) => r.business_id === business_id && r.status === "pending").sort((a, b) => (a.created_at < b.created_at ? -1 : a.created_at > b.created_at ? 1 : a.rowid - b.rowid)).map((r) => ({ entity_id: r.entity_id }));
|
|
483
495
|
rows = rows.slice(0, lim);
|
|
484
496
|
return rows;
|
|
485
497
|
}
|
|
498
|
+
// sync_jobs command list for a single entity (pending intent)
|
|
499
|
+
if (s.includes("FROM sync_jobs WHERE business_id = ? AND entity_id = ?")) {
|
|
500
|
+
const [business_id, entity_id] = args;
|
|
501
|
+
const rows = tables.sync_jobs
|
|
502
|
+
.filter((r) => r.business_id === business_id && r.entity_id === entity_id && r.status === "pending")
|
|
503
|
+
.sort((a, b) => (a.created_at < b.created_at ? -1 : a.created_at > b.created_at ? 1 : a.rowid - b.rowid))
|
|
504
|
+
.map((r) => ({ command_type: r.command_type }));
|
|
505
|
+
return rows;
|
|
506
|
+
}
|
|
486
507
|
if (s.includes("SELECT * FROM sync_jobs WHERE") || s.includes("SELECT id, command_type, status, created_at FROM sync_jobs")) {
|
|
487
508
|
const [business_id] = args;
|
|
488
509
|
const rows = tables.sync_jobs.filter((r) => r.business_id === business_id);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teincfood/core",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "TeincFood shared offline-first core
|
|
3
|
+
"version": "0.9.24",
|
|
4
|
+
"description": "TeincFood shared offline-first core \u2014 types, sync engine, local DB, reference data, and repositories for mobile + desktop",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|