@teincfood/core 0.7.8 → 0.8.0
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/README.md +26 -21
- package/dist/adapters/auth.d.ts +10 -0
- package/dist/adapters/auth.js +43 -0
- package/dist/adapters/devices.d.ts +3 -1
- package/dist/adapters/devices.js +187 -3
- package/dist/adapters/filesystem.d.ts +4 -0
- package/dist/adapters/kv.d.ts +10 -0
- package/dist/adapters/kv.js +23 -7
- package/dist/adapters/local-node.d.ts +4 -6
- package/dist/adapters/local-node.js +9 -32
- package/dist/adapters/notify.d.ts +12 -0
- package/dist/adapters/notify.js +33 -0
- package/dist/adapters/query-keys.d.ts +4 -0
- package/dist/adapters/query-keys.js +4 -0
- package/dist/adapters/services.d.ts +99 -15
- package/dist/adapters/services.js +244 -4
- package/dist/auth/session.d.ts +66 -0
- package/dist/auth/session.js +172 -0
- package/dist/catalog/hooks.d.ts +10 -0
- package/dist/catalog/hooks.js +82 -11
- package/dist/db/migrations.js +274 -0
- package/dist/db/operations.d.ts +53 -2
- package/dist/db/operations.js +177 -8
- package/dist/domain/business/index.d.ts +5 -0
- package/dist/domain/business/index.js +5 -0
- package/dist/domain/orders/lifecycle.d.ts +6 -0
- package/dist/domain/orders/lifecycle.js +6 -0
- package/dist/domain/orders/service.d.ts +24 -0
- package/dist/domain/orders/service.js +242 -0
- package/dist/domain/payments/index.d.ts +6 -0
- package/dist/domain/payments/index.js +5 -0
- package/dist/domain/reference/index.d.ts +8 -0
- package/dist/domain/reference/index.js +8 -0
- package/dist/e2e/harness.d.ts +1 -0
- package/dist/e2e/harness.js +61 -0
- package/dist/hooks/analytics.hooks.d.ts +25 -0
- package/dist/hooks/analytics.hooks.js +214 -0
- package/dist/hooks/business-hours.hooks.d.ts +8 -0
- package/dist/hooks/business-hours.hooks.js +45 -0
- package/dist/hooks/dashboard.hooks.d.ts +30 -0
- package/dist/hooks/dashboard.hooks.js +132 -0
- package/dist/hooks/delivery.hooks.d.ts +225 -0
- package/dist/hooks/delivery.hooks.js +80 -0
- package/dist/hooks/fleet.hooks.d.ts +37 -0
- package/dist/hooks/fleet.hooks.js +229 -0
- package/dist/hooks/image.hooks.d.ts +20 -0
- package/dist/hooks/image.hooks.js +62 -0
- package/dist/hooks/kiosk.hooks.d.ts +22 -5
- package/dist/hooks/kiosk.hooks.js +74 -5
- package/dist/hooks/members.hooks.d.ts +48 -0
- package/dist/hooks/members.hooks.js +302 -0
- package/dist/hooks/offline-orders.hooks.d.ts +9 -0
- package/dist/hooks/offline-orders.hooks.js +51 -0
- package/dist/hooks/orders.hooks.d.ts +19 -39
- package/dist/hooks/orders.hooks.js +126 -321
- package/dist/hooks/places.hooks.d.ts +21 -0
- package/dist/hooks/places.hooks.js +112 -0
- package/dist/hooks/realtime-stubs.d.ts +28 -0
- package/dist/hooks/realtime-stubs.js +85 -0
- package/dist/hooks/rider.hooks.d.ts +470 -0
- package/dist/hooks/rider.hooks.js +373 -0
- package/dist/hooks/use-infinite-list.d.ts +243 -0
- package/dist/hooks/use-infinite-list.js +58 -0
- package/dist/hooks/wallet.hooks.d.ts +219 -0
- package/dist/hooks/wallet.hooks.js +56 -0
- package/dist/image-cache/service.d.ts +8 -0
- package/dist/image-cache/service.js +15 -3
- package/dist/index.d.ts +60 -5
- package/dist/index.js +78 -5
- package/dist/local-node/adapters.d.ts +87 -0
- package/dist/local-node/adapters.js +4 -0
- package/dist/local-node/index.d.ts +5 -0
- package/dist/local-node/index.js +5 -0
- package/dist/local-node/security.d.ts +25 -0
- package/dist/local-node/security.js +149 -0
- package/dist/local-node/service.d.ts +83 -0
- package/dist/local-node/service.js +706 -0
- package/dist/local-node/store.d.ts +50 -0
- package/dist/local-node/store.js +284 -0
- package/dist/local-node/types.d.ts +92 -0
- package/dist/local-node/types.js +4 -0
- package/dist/printer/receipt.d.ts +4 -0
- package/dist/printer/receipt.js +75 -0
- package/dist/printer/service.js +1 -1
- package/dist/providers/query-client.d.ts +13 -0
- package/dist/providers/query-client.js +58 -0
- package/dist/realtime/events.service.d.ts +18 -0
- package/dist/realtime/events.service.js +76 -0
- package/dist/realtime/order-events.service.d.ts +6 -0
- package/dist/realtime/order-events.service.js +17 -0
- package/dist/realtime/phoenix-socket.d.ts +55 -0
- package/dist/realtime/phoenix-socket.js +269 -0
- package/dist/realtime/useEventsConnection.d.ts +1 -0
- package/dist/realtime/useEventsConnection.js +50 -0
- package/dist/reference/api.d.ts +2 -0
- package/dist/reference/api.js +2 -0
- package/dist/reference/capabilities.d.ts +12 -7
- package/dist/reference/capabilities.js +28 -13
- package/dist/reference/hooks.d.ts +34 -0
- package/dist/reference/hooks.js +124 -0
- package/dist/reference/menu-apply.d.ts +29 -0
- package/dist/reference/menu-apply.js +289 -0
- package/dist/reference/menu-record.d.ts +69 -0
- package/dist/reference/menu-record.js +163 -0
- package/dist/reference/operations.d.ts +3 -0
- package/dist/reference/operations.js +35 -1
- package/dist/reference/service.js +36 -9
- package/dist/reference/types.d.ts +1 -1
- package/dist/repositories/order.repository.d.ts +8 -0
- package/dist/repositories/order.repository.js +24 -2
- package/dist/repositories/pos.repository.d.ts +4 -0
- package/dist/repositories/pos.repository.js +4 -0
- package/dist/sync/backoff.d.ts +21 -0
- package/dist/sync/backoff.js +42 -0
- package/dist/sync/command-builder.js +10 -2
- package/dist/sync/command-queue.service.d.ts +12 -0
- package/dist/sync/command-queue.service.js +115 -18
- package/dist/sync/device-lease.service.d.ts +3 -3
- package/dist/sync/device-lease.service.js +80 -20
- package/dist/sync/engine.d.ts +3 -1
- package/dist/sync/engine.js +44 -4
- package/dist/sync/full-sync.d.ts +6 -0
- package/dist/sync/full-sync.js +72 -0
- package/dist/sync/health-probe.d.ts +24 -0
- package/dist/sync/health-probe.js +138 -0
- package/dist/sync/offline-capable.js +25 -0
- package/dist/sync/online-gates.d.ts +17 -0
- package/dist/sync/online-gates.js +55 -0
- package/dist/sync/order-number.d.ts +4 -4
- package/dist/sync/order-number.js +33 -13
- package/dist/sync/order-snapshots.service.d.ts +1 -0
- package/dist/sync/order-snapshots.service.js +47 -2
- package/dist/sync/pending-orders.service.d.ts +18 -0
- package/dist/sync/pending-orders.service.js +148 -3
- package/dist/sync/reconciliation/index.d.ts +5 -0
- package/dist/sync/reconciliation/index.js +5 -0
- package/dist/sync/snapshots/entity.d.ts +6 -0
- package/dist/sync/snapshots/entity.js +6 -0
- package/dist/sync/sync-jobs.service.d.ts +18 -0
- package/dist/sync/sync-jobs.service.js +539 -0
- package/dist/sync/transition-endpoints.d.ts +11 -0
- package/dist/sync/transition-endpoints.js +30 -0
- package/dist/sync/transport-local-node.js +3 -2
- package/dist/sync/transport-rest.d.ts +7 -0
- package/dist/sync/transport-rest.js +90 -25
- package/dist/sync/use-sync-reconciliation.js +58 -16
- package/dist/test-helpers/memory-driver.js +182 -4
- package/dist/test-helpers/seed-hours.d.ts +1 -0
- package/dist/test-helpers/seed-hours.js +23 -0
- package/dist/types/api/business.api.d.ts +42 -0
- package/dist/types/api/menu.api.d.ts +15 -0
- package/dist/types/api/seller.api.d.ts +20 -0
- package/dist/types/places.d.ts +19 -0
- package/dist/types/places.js +2 -0
- package/dist/types/rider.d.ts +67 -0
- package/dist/types/rider.js +2 -0
- package/dist/utils/attempt-stats.d.ts +23 -0
- package/dist/utils/attempt-stats.js +61 -0
- package/dist/utils/business-hours.d.ts +37 -0
- package/dist/utils/business-hours.js +96 -0
- package/dist/utils/debug-log.d.ts +23 -0
- package/dist/utils/debug-log.js +153 -0
- package/dist/utils/order-filter.d.ts +1 -1
- package/dist/utils/order-filter.js +1 -1
- package/dist/utils/order-transitions.d.ts +30 -0
- package/dist/utils/order-transitions.js +185 -0
- package/dist/utils/sync-logger.js +5 -4
- package/dist/validation/offline.d.ts +46 -0
- package/dist/validation/offline.js +79 -0
- package/package.json +6 -1
- package/dist/pricing/engine.test.d.ts +0 -1
- package/dist/pricing/engine.test.js +0 -142
- package/dist/reference/lww.test.d.ts +0 -1
- package/dist/reference/lww.test.js +0 -23
- package/dist/reference/operations.test.d.ts +0 -1
- package/dist/reference/operations.test.js +0 -66
- package/dist/standalone.integration.test.d.ts +0 -1
- package/dist/standalone.integration.test.js +0 -235
- package/dist/sync/device-lease.test.d.ts +0 -1
- package/dist/sync/device-lease.test.js +0 -38
- package/dist/sync/offline-integration.test.d.ts +0 -1
- package/dist/sync/offline-integration.test.js +0 -52
- package/dist/sync/order-number.test.d.ts +0 -1
- package/dist/sync/order-number.test.js +0 -51
- package/dist/sync/perms.test.d.ts +0 -1
- package/dist/sync/perms.test.js +0 -15
- package/dist/utils/currency.test.d.ts +0 -1
- package/dist/utils/currency.test.js +0 -17
- package/dist/utils/hlc.test.d.ts +0 -1
- package/dist/utils/hlc.test.js +0 -29
- package/dist/utils/phone.test.d.ts +0 -1
- package/dist/utils/phone.test.js +0 -19
- package/dist/utils/responsive.d.ts +0 -13
- package/dist/utils/responsive.js +0 -18
- package/dist/utils/uuid.test.d.ts +0 -1
- package/dist/utils/uuid.test.js +0 -13
- package/dist/utils/validation.test.d.ts +0 -1
- package/dist/utils/validation.test.js +0 -23
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Node store — platform-agnostic, core-owned.
|
|
3
|
+
* Mesh mode: no elected node, every isLocalNodeEnabled device is both publisher and scanner.
|
|
4
|
+
*/
|
|
5
|
+
export interface DiscoveredNode {
|
|
6
|
+
deviceId: string;
|
|
7
|
+
name: string;
|
|
8
|
+
host: string;
|
|
9
|
+
port: number;
|
|
10
|
+
deviceName?: string;
|
|
11
|
+
lastSeenAt: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ConnectedClient {
|
|
14
|
+
deviceId: string;
|
|
15
|
+
deviceName?: string;
|
|
16
|
+
host?: string;
|
|
17
|
+
connectedAt: string;
|
|
18
|
+
}
|
|
19
|
+
interface LocalNodeState {
|
|
20
|
+
isLocalNodeEnabled: boolean;
|
|
21
|
+
localNodePort: number;
|
|
22
|
+
discoveredNodes: DiscoveredNode[];
|
|
23
|
+
connectedClients: ConnectedClient[];
|
|
24
|
+
}
|
|
25
|
+
type Listener = (state: LocalNodeState, prev: LocalNodeState) => void;
|
|
26
|
+
declare class LocalNodeStore {
|
|
27
|
+
private state;
|
|
28
|
+
private listeners;
|
|
29
|
+
constructor();
|
|
30
|
+
private hydrate;
|
|
31
|
+
rehydrate(): Promise<void>;
|
|
32
|
+
private persist;
|
|
33
|
+
private set;
|
|
34
|
+
getState(): LocalNodeState & {
|
|
35
|
+
setIsLocalNodeEnabled: (v: boolean) => void;
|
|
36
|
+
setLocalNodePort: (p: number) => void;
|
|
37
|
+
addDiscoveredNode: (n: DiscoveredNode) => void;
|
|
38
|
+
removeDiscoveredNode: (id: string) => void;
|
|
39
|
+
clearDiscoveredNodes: () => void;
|
|
40
|
+
addConnectedClient: (c: ConnectedClient) => void;
|
|
41
|
+
removeConnectedClient: (id: string) => void;
|
|
42
|
+
clearConnectedClients: () => void;
|
|
43
|
+
};
|
|
44
|
+
subscribe(listener: Listener): () => void;
|
|
45
|
+
_reset(): void;
|
|
46
|
+
}
|
|
47
|
+
export declare const localNodeStore: LocalNodeStore;
|
|
48
|
+
export declare function useLocalNodeStore(): ReturnType<LocalNodeStore["getState"]>;
|
|
49
|
+
export declare function useLocalNodeStore<T>(selector: (s: ReturnType<LocalNodeStore["getState"]>) => T): T;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Node store — platform-agnostic, core-owned.
|
|
3
|
+
* Mesh mode: no elected node, every isLocalNodeEnabled device is both publisher and scanner.
|
|
4
|
+
*/
|
|
5
|
+
import { getKVDriver } from "../adapters/kv";
|
|
6
|
+
const DEFAULT_PORT = 44621;
|
|
7
|
+
const KV_KEY = "teinc_local_node";
|
|
8
|
+
class LocalNodeStore {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.state = {
|
|
11
|
+
isLocalNodeEnabled: false,
|
|
12
|
+
localNodePort: DEFAULT_PORT,
|
|
13
|
+
discoveredNodes: [],
|
|
14
|
+
connectedClients: [],
|
|
15
|
+
};
|
|
16
|
+
this.listeners = new Set();
|
|
17
|
+
this.hydrate();
|
|
18
|
+
}
|
|
19
|
+
hydrate() {
|
|
20
|
+
try {
|
|
21
|
+
const ls = typeof localStorage !== "undefined" ? localStorage.getItem(KV_KEY) : null;
|
|
22
|
+
if (ls) {
|
|
23
|
+
const parsed = JSON.parse(ls);
|
|
24
|
+
this.state = { ...this.state, ...parsed, discoveredNodes: [], connectedClients: [] };
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const old = typeof localStorage !== "undefined" ? localStorage.getItem("local_node_data") : null;
|
|
28
|
+
if (old) {
|
|
29
|
+
try {
|
|
30
|
+
const parsedOld = JSON.parse(old);
|
|
31
|
+
const p = parsedOld.state ?? parsedOld;
|
|
32
|
+
if (p && typeof p.isLocalNodeEnabled === "boolean") {
|
|
33
|
+
this.state = { ...this.state, ...p, discoveredNodes: [], connectedClients: [] };
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
try {
|
|
42
|
+
const kv = getKVDriver();
|
|
43
|
+
const raw = kv?.getItem(KV_KEY);
|
|
44
|
+
if (raw instanceof Promise) {
|
|
45
|
+
raw.then((v) => {
|
|
46
|
+
if (!v) {
|
|
47
|
+
const kv2 = getKVDriver();
|
|
48
|
+
const rawOld = kv2?.getItem("local_node_data");
|
|
49
|
+
if (rawOld instanceof Promise)
|
|
50
|
+
rawOld.then((o) => { if (!o)
|
|
51
|
+
return; try {
|
|
52
|
+
const po = JSON.parse(o);
|
|
53
|
+
const p = po.state ?? po;
|
|
54
|
+
this.state = { ...this.state, ...p, discoveredNodes: [], connectedClients: [] };
|
|
55
|
+
const prev = { ...this.state };
|
|
56
|
+
for (const l of this.listeners)
|
|
57
|
+
try {
|
|
58
|
+
l(this.getState(), prev);
|
|
59
|
+
}
|
|
60
|
+
catch { }
|
|
61
|
+
}
|
|
62
|
+
catch { } }).catch(() => { });
|
|
63
|
+
else if (rawOld)
|
|
64
|
+
try {
|
|
65
|
+
const po = JSON.parse(rawOld);
|
|
66
|
+
const p = po.state ?? po;
|
|
67
|
+
this.state = { ...this.state, ...p, discoveredNodes: [], connectedClients: [] };
|
|
68
|
+
const prev2 = { ...this.state };
|
|
69
|
+
for (const l of this.listeners)
|
|
70
|
+
try {
|
|
71
|
+
l(this.getState(), prev2);
|
|
72
|
+
}
|
|
73
|
+
catch { }
|
|
74
|
+
}
|
|
75
|
+
catch { }
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(v);
|
|
80
|
+
const prev = { ...this.state, discoveredNodes: [...this.state.discoveredNodes], connectedClients: [...this.state.connectedClients] };
|
|
81
|
+
this.state = { ...this.state, ...parsed, discoveredNodes: [], connectedClients: [] };
|
|
82
|
+
for (const l of this.listeners)
|
|
83
|
+
try {
|
|
84
|
+
l(this.getState(), prev);
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
}).catch(() => { });
|
|
90
|
+
}
|
|
91
|
+
else if (raw) {
|
|
92
|
+
const parsed = JSON.parse(raw);
|
|
93
|
+
this.state = { ...this.state, ...parsed, discoveredNodes: [], connectedClients: [] };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch { }
|
|
97
|
+
}
|
|
98
|
+
async rehydrate() {
|
|
99
|
+
try {
|
|
100
|
+
const kv = getKVDriver();
|
|
101
|
+
const raw = await kv.getItem(KV_KEY);
|
|
102
|
+
let parsed = null;
|
|
103
|
+
if (raw) {
|
|
104
|
+
try {
|
|
105
|
+
parsed = JSON.parse(raw);
|
|
106
|
+
}
|
|
107
|
+
catch { }
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
try {
|
|
111
|
+
const ls = typeof localStorage !== "undefined" ? localStorage.getItem(KV_KEY) : null;
|
|
112
|
+
if (ls)
|
|
113
|
+
parsed = JSON.parse(ls);
|
|
114
|
+
}
|
|
115
|
+
catch { }
|
|
116
|
+
}
|
|
117
|
+
if (!parsed) {
|
|
118
|
+
try {
|
|
119
|
+
const oldRaw = await kv.getItem("local_node_data");
|
|
120
|
+
if (oldRaw) {
|
|
121
|
+
const po = JSON.parse(oldRaw);
|
|
122
|
+
parsed = po.state ?? po;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch { }
|
|
126
|
+
}
|
|
127
|
+
if (!parsed)
|
|
128
|
+
return;
|
|
129
|
+
const prev = { ...this.state, discoveredNodes: [...this.state.discoveredNodes], connectedClients: [...this.state.connectedClients] };
|
|
130
|
+
const next = {};
|
|
131
|
+
if (typeof parsed.isLocalNodeEnabled === "boolean")
|
|
132
|
+
next.isLocalNodeEnabled = parsed.isLocalNodeEnabled;
|
|
133
|
+
if (typeof parsed.localNodePort === "number")
|
|
134
|
+
next.localNodePort = parsed.localNodePort;
|
|
135
|
+
const changed = next.isLocalNodeEnabled !== undefined && next.isLocalNodeEnabled !== this.state.isLocalNodeEnabled;
|
|
136
|
+
this.state = { ...this.state, ...next, discoveredNodes: [], connectedClients: [] };
|
|
137
|
+
if (changed) {
|
|
138
|
+
for (const l of this.listeners)
|
|
139
|
+
try {
|
|
140
|
+
l(this.getState(), prev);
|
|
141
|
+
}
|
|
142
|
+
catch { }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch { }
|
|
146
|
+
}
|
|
147
|
+
persist() {
|
|
148
|
+
try {
|
|
149
|
+
const kv = getKVDriver();
|
|
150
|
+
if (!kv)
|
|
151
|
+
return;
|
|
152
|
+
const toSave = {
|
|
153
|
+
isLocalNodeEnabled: this.state.isLocalNodeEnabled,
|
|
154
|
+
localNodePort: this.state.localNodePort,
|
|
155
|
+
};
|
|
156
|
+
const raw = JSON.stringify(toSave);
|
|
157
|
+
kv.setItem(KV_KEY, raw);
|
|
158
|
+
try {
|
|
159
|
+
if (typeof localStorage !== "undefined")
|
|
160
|
+
localStorage.setItem(KV_KEY, raw);
|
|
161
|
+
}
|
|
162
|
+
catch { }
|
|
163
|
+
}
|
|
164
|
+
catch { }
|
|
165
|
+
}
|
|
166
|
+
set(partial) {
|
|
167
|
+
const prev = { ...this.state, discoveredNodes: [...this.state.discoveredNodes], connectedClients: [...this.state.connectedClients] };
|
|
168
|
+
this.state = { ...this.state, ...partial };
|
|
169
|
+
this.persist();
|
|
170
|
+
for (const l of this.listeners) {
|
|
171
|
+
try {
|
|
172
|
+
l(this.getState(), prev);
|
|
173
|
+
}
|
|
174
|
+
catch { }
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
getState() {
|
|
178
|
+
const self = this;
|
|
179
|
+
return {
|
|
180
|
+
...this.state,
|
|
181
|
+
setIsLocalNodeEnabled: (enabled) => self.set({ isLocalNodeEnabled: enabled }),
|
|
182
|
+
setLocalNodePort: (port) => self.set({ localNodePort: port }),
|
|
183
|
+
addDiscoveredNode: (node) => {
|
|
184
|
+
const prev = { ...self.state, discoveredNodes: [...self.state.discoveredNodes] };
|
|
185
|
+
// Dedup by deviceId (same peer re-announced) OR by host:port
|
|
186
|
+
// (TCP sweep's synthetic "lan-X.X.X.X" vs mDNS real UUID for same device).
|
|
187
|
+
const existingById = self.state.discoveredNodes.find((n) => n.deviceId === node.deviceId);
|
|
188
|
+
const existingByAddr = !existingById && node.host && node.port
|
|
189
|
+
? self.state.discoveredNodes.find((n) => n.host === node.host && n.port === node.port)
|
|
190
|
+
: undefined;
|
|
191
|
+
const existing = existingById ?? existingByAddr;
|
|
192
|
+
if (existing) {
|
|
193
|
+
// Prefer the real UUID entry over synthetic "lan-" prefix entries
|
|
194
|
+
const keep = (existing.deviceId.startsWith("lan-") && !node.deviceId.startsWith("lan-"))
|
|
195
|
+
? node : existing;
|
|
196
|
+
self.state = {
|
|
197
|
+
...self.state,
|
|
198
|
+
discoveredNodes: self.state.discoveredNodes.map((n) => (n.deviceId === keep.deviceId || n.host === keep.host && n.port === keep.port)
|
|
199
|
+
? { ...n, ...keep, lastSeenAt: new Date().toISOString() }
|
|
200
|
+
: n),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
self.state = { ...self.state, discoveredNodes: [...self.state.discoveredNodes, { ...node, lastSeenAt: new Date().toISOString() }] };
|
|
205
|
+
}
|
|
206
|
+
for (const l of self.listeners) {
|
|
207
|
+
try {
|
|
208
|
+
l(self.getState(), prev);
|
|
209
|
+
}
|
|
210
|
+
catch { }
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
removeDiscoveredNode: (deviceId) => {
|
|
214
|
+
const prev = { ...self.state, discoveredNodes: [...self.state.discoveredNodes] };
|
|
215
|
+
self.state = { ...self.state, discoveredNodes: self.state.discoveredNodes.filter((n) => n.deviceId !== deviceId) };
|
|
216
|
+
for (const l of self.listeners) {
|
|
217
|
+
try {
|
|
218
|
+
l(self.getState(), prev);
|
|
219
|
+
}
|
|
220
|
+
catch { }
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
clearDiscoveredNodes: () => self.set({ discoveredNodes: [] }),
|
|
224
|
+
addConnectedClient: (client) => {
|
|
225
|
+
const prev = { ...self.state, connectedClients: [...self.state.connectedClients] };
|
|
226
|
+
const existing = self.state.connectedClients.find((c) => c.deviceId === client.deviceId);
|
|
227
|
+
if (existing) {
|
|
228
|
+
self.state = {
|
|
229
|
+
...self.state,
|
|
230
|
+
connectedClients: self.state.connectedClients.map((c) => (c.deviceId === client.deviceId ? { ...c, ...client } : c)),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
self.state = { ...self.state, connectedClients: [...self.state.connectedClients, client] };
|
|
235
|
+
}
|
|
236
|
+
for (const l of self.listeners) {
|
|
237
|
+
try {
|
|
238
|
+
l(self.getState(), prev);
|
|
239
|
+
}
|
|
240
|
+
catch { }
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
removeConnectedClient: (deviceId) => {
|
|
244
|
+
const prev = { ...self.state, connectedClients: [...self.state.connectedClients] };
|
|
245
|
+
self.state = { ...self.state, connectedClients: self.state.connectedClients.filter((c) => c.deviceId !== deviceId) };
|
|
246
|
+
for (const l of self.listeners) {
|
|
247
|
+
try {
|
|
248
|
+
l(self.getState(), prev);
|
|
249
|
+
}
|
|
250
|
+
catch { }
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
clearConnectedClients: () => self.set({ connectedClients: [] }),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
subscribe(listener) {
|
|
257
|
+
this.listeners.add(listener);
|
|
258
|
+
return () => this.listeners.delete(listener);
|
|
259
|
+
}
|
|
260
|
+
_reset() {
|
|
261
|
+
this.state = {
|
|
262
|
+
isLocalNodeEnabled: false,
|
|
263
|
+
localNodePort: DEFAULT_PORT,
|
|
264
|
+
discoveredNodes: [],
|
|
265
|
+
connectedClients: [],
|
|
266
|
+
};
|
|
267
|
+
this.listeners.clear();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
export const localNodeStore = new LocalNodeStore();
|
|
271
|
+
import * as React from "react";
|
|
272
|
+
export function useLocalNodeStore(selector) {
|
|
273
|
+
const [state, setState] = React.useState(() => localNodeStore.getState());
|
|
274
|
+
React.useEffect(() => {
|
|
275
|
+
const unsub = localNodeStore.subscribe((next) => setState(next));
|
|
276
|
+
setState(localNodeStore.getState());
|
|
277
|
+
return unsub;
|
|
278
|
+
}, []);
|
|
279
|
+
if (selector)
|
|
280
|
+
return selector(state);
|
|
281
|
+
return state;
|
|
282
|
+
}
|
|
283
|
+
useLocalNodeStore.getState = () => localNodeStore.getState();
|
|
284
|
+
useLocalNodeStore.subscribe = (fn) => localNodeStore.subscribe(fn);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the Local Node / multi-terminal LAN layer.
|
|
3
|
+
*/
|
|
4
|
+
import type { Command, DomainEvent } from "../sync/types";
|
|
5
|
+
import type { ReferenceDataKey, ReferenceDataRecord } from "../reference/types";
|
|
6
|
+
export type LocalNodeMessageType = "command" | "command_response" | "event" | "relay_event" | "ping" | "pong" | "hello" | "challenge" | "auth" | "auth_ok" | "auth_fail" | "register" | "register_ack" | "reference_fetch" | "reference_response";
|
|
7
|
+
export interface LocalNodeMessage {
|
|
8
|
+
type: LocalNodeMessageType;
|
|
9
|
+
id: string;
|
|
10
|
+
payload: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface LocalNodeCommandMessage extends LocalNodeMessage {
|
|
13
|
+
type: "command";
|
|
14
|
+
payload: Command;
|
|
15
|
+
}
|
|
16
|
+
export interface LocalNodeCommandResponseMessage extends LocalNodeMessage {
|
|
17
|
+
type: "command_response";
|
|
18
|
+
payload: DomainEvent[];
|
|
19
|
+
}
|
|
20
|
+
export interface LocalNodeEventMessage extends LocalNodeMessage {
|
|
21
|
+
type: "event";
|
|
22
|
+
payload: DomainEvent;
|
|
23
|
+
}
|
|
24
|
+
export interface LocalNodeRelayEventMessage extends LocalNodeMessage {
|
|
25
|
+
type: "relay_event";
|
|
26
|
+
payload: DomainEvent;
|
|
27
|
+
}
|
|
28
|
+
export interface LocalNodeHelloMessage extends LocalNodeMessage {
|
|
29
|
+
type: "hello";
|
|
30
|
+
payload: {
|
|
31
|
+
deviceId: string;
|
|
32
|
+
deviceName?: string;
|
|
33
|
+
businessId?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export interface LocalNodeChallengeMessage extends LocalNodeMessage {
|
|
37
|
+
type: "challenge";
|
|
38
|
+
payload: {
|
|
39
|
+
nonce: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export interface LocalNodeAuthMessage extends LocalNodeMessage {
|
|
43
|
+
type: "auth";
|
|
44
|
+
payload: {
|
|
45
|
+
nonce: string;
|
|
46
|
+
proof: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export interface LocalNodeAuthOkMessage extends LocalNodeMessage {
|
|
50
|
+
type: "auth_ok";
|
|
51
|
+
payload: Record<string, never>;
|
|
52
|
+
}
|
|
53
|
+
export interface LocalNodeAuthFailMessage extends LocalNodeMessage {
|
|
54
|
+
type: "auth_fail";
|
|
55
|
+
payload: {
|
|
56
|
+
reason?: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export interface LocalNodeRegisterMessage extends LocalNodeMessage {
|
|
60
|
+
type: "register";
|
|
61
|
+
payload: {
|
|
62
|
+
deviceId: string;
|
|
63
|
+
deviceName?: string;
|
|
64
|
+
businessId?: string;
|
|
65
|
+
connectedAt: string;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export interface LocalNodeRegisterAckMessage extends LocalNodeMessage {
|
|
69
|
+
type: "register_ack";
|
|
70
|
+
payload: Record<string, never>;
|
|
71
|
+
}
|
|
72
|
+
export interface LocalNodeReferenceFetchMessage extends LocalNodeMessage {
|
|
73
|
+
type: "reference_fetch";
|
|
74
|
+
payload: {
|
|
75
|
+
businessId: string;
|
|
76
|
+
keys: ReferenceDataKey[];
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export interface LocalNodeReferenceResponseMessage extends LocalNodeMessage {
|
|
80
|
+
type: "reference_response";
|
|
81
|
+
payload: {
|
|
82
|
+
data: Partial<Record<ReferenceDataKey, ReferenceDataRecord | null>>;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export type LocalNodeConnectionState = "disconnected" | "connecting" | "connected";
|
|
86
|
+
export interface LocalNodeInfo {
|
|
87
|
+
deviceId: string;
|
|
88
|
+
host: string;
|
|
89
|
+
port: number;
|
|
90
|
+
businessId?: string;
|
|
91
|
+
deviceName?: string;
|
|
92
|
+
}
|
|
@@ -9,4 +9,8 @@
|
|
|
9
9
|
import type { SellerOrder } from "../types/api/seller.api";
|
|
10
10
|
export declare function calloutNumber(order: SellerOrder): string;
|
|
11
11
|
export declare function generateEscpos(order: SellerOrder): string;
|
|
12
|
+
export declare function generateHtmlAsync(order: SellerOrder, opts?: {
|
|
13
|
+
logoDataUri?: string | null;
|
|
14
|
+
useCache?: boolean;
|
|
15
|
+
}): Promise<string>;
|
|
12
16
|
export declare function generateHtml(order: SellerOrder, logoDataUri?: string | null): string;
|
package/dist/printer/receipt.js
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* Backend source: lib/teinc_food_backend/orders/receipt.ex
|
|
7
7
|
* Keep this file in sync with backend when template changes.
|
|
8
8
|
*/
|
|
9
|
+
import { openSyncDatabase } from "../db/connection";
|
|
10
|
+
import { getReferenceData } from "../reference/operations";
|
|
11
|
+
import { cacheRemoteImage, resolveImageSource } from "../image-cache/service";
|
|
9
12
|
const LINE_WIDTH = 32;
|
|
10
13
|
const ESC_INIT = "\x1B\x40";
|
|
11
14
|
const ESC_ALIGN_LEFT = "\x1B\x61\x00";
|
|
@@ -116,6 +119,31 @@ function businessAddress(business) {
|
|
|
116
119
|
return b.address;
|
|
117
120
|
return null;
|
|
118
121
|
}
|
|
122
|
+
function businessLogoUrl(order) {
|
|
123
|
+
const b = order.business;
|
|
124
|
+
if (!b)
|
|
125
|
+
return null;
|
|
126
|
+
return (b.logo_url ?? b.logoUrl ?? null);
|
|
127
|
+
}
|
|
128
|
+
/** Fetch remote URL (or tfimg://) and return data URI, or null on failure. Uses Web fetch; works for http(s) and for cached tfimg via Rust protocol. */
|
|
129
|
+
async function fetchAsDataUri(url) {
|
|
130
|
+
try {
|
|
131
|
+
const res = await fetch(url);
|
|
132
|
+
if (!res.ok)
|
|
133
|
+
return null;
|
|
134
|
+
const blob = await res.blob();
|
|
135
|
+
const reader = await new Promise((resolve, reject) => {
|
|
136
|
+
const fr = new FileReader();
|
|
137
|
+
fr.onload = () => resolve(fr.result);
|
|
138
|
+
fr.onerror = () => reject(new Error("FileReader failed"));
|
|
139
|
+
fr.readAsDataURL(blob);
|
|
140
|
+
});
|
|
141
|
+
return reader;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
119
147
|
// ── ESC/POS ────────────────────────────────────────────────────────────
|
|
120
148
|
function doubleLineEscpos() { return "═".repeat(LINE_WIDTH) + LF; }
|
|
121
149
|
function singleLineEscpos() { return "─".repeat(LINE_WIDTH); }
|
|
@@ -222,6 +250,53 @@ export function generateEscpos(order) {
|
|
|
222
250
|
return parts.filter((p) => p != null).join("");
|
|
223
251
|
}
|
|
224
252
|
// ── HTML ───────────────────────────────────────────────────────────────
|
|
253
|
+
export async function generateHtmlAsync(order, opts) {
|
|
254
|
+
const explicit = opts?.logoDataUri;
|
|
255
|
+
if (explicit)
|
|
256
|
+
return generateHtml(order, explicit);
|
|
257
|
+
const logoUrl = businessLogoUrl(order);
|
|
258
|
+
if (!logoUrl) {
|
|
259
|
+
try {
|
|
260
|
+
const bId = order.business_id;
|
|
261
|
+
if (bId) {
|
|
262
|
+
const db = await openSyncDatabase();
|
|
263
|
+
const ref = await getReferenceData(db, bId, "business:profile");
|
|
264
|
+
const refLogo = ref?.payload?.data?.logo_url;
|
|
265
|
+
if (refLogo) {
|
|
266
|
+
const dataUri = await fetchAsDataUri(refLogo);
|
|
267
|
+
if (dataUri)
|
|
268
|
+
return generateHtml(order, dataUri);
|
|
269
|
+
try {
|
|
270
|
+
await cacheRemoteImage(refLogo, { skipIfCached: true });
|
|
271
|
+
const resolved = await resolveImageSource(refLogo);
|
|
272
|
+
const src = resolved?.uri ?? refLogo;
|
|
273
|
+
const dataUri2 = await fetchAsDataUri(src);
|
|
274
|
+
if (dataUri2)
|
|
275
|
+
return generateHtml(order, dataUri2);
|
|
276
|
+
if (src !== refLogo)
|
|
277
|
+
return generateHtml(order, src);
|
|
278
|
+
}
|
|
279
|
+
catch { }
|
|
280
|
+
return generateHtml(order, refLogo);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch { }
|
|
285
|
+
return generateHtml(order, null);
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
await cacheRemoteImage(logoUrl, { skipIfCached: true });
|
|
289
|
+
const resolved = await resolveImageSource(logoUrl);
|
|
290
|
+
const src = resolved?.uri ?? logoUrl;
|
|
291
|
+
const dataUri = await fetchAsDataUri(src);
|
|
292
|
+
if (dataUri)
|
|
293
|
+
return generateHtml(order, dataUri);
|
|
294
|
+
if (src !== logoUrl)
|
|
295
|
+
return generateHtml(order, src);
|
|
296
|
+
}
|
|
297
|
+
catch { }
|
|
298
|
+
return generateHtml(order, logoUrl);
|
|
299
|
+
}
|
|
225
300
|
export function generateHtml(order, logoDataUri) {
|
|
226
301
|
const business = order.business;
|
|
227
302
|
const items = order.items || [];
|
package/dist/printer/service.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getHttpClient } from "../adapters/http";
|
|
|
7
7
|
import { getTcpAdapter, getPrintFileAdapter } from "../adapters/print";
|
|
8
8
|
import { getPrinterState } from "./store";
|
|
9
9
|
import { generateEscpos, generateHtml } from "./receipt";
|
|
10
|
+
import { API_BASE_URL } from "../utils/constants";
|
|
10
11
|
async function fetchReceipt(orderId, format = "html") {
|
|
11
12
|
const http = getHttpClient();
|
|
12
13
|
// http adapter wraps axios; fallback to fetch if not set
|
|
@@ -21,7 +22,6 @@ async function fetchReceipt(orderId, format = "html") {
|
|
|
21
22
|
return String(data);
|
|
22
23
|
}
|
|
23
24
|
// Fallback fetch using API_BASE_URL already injected
|
|
24
|
-
const { API_BASE_URL } = await import("../utils/constants");
|
|
25
25
|
const url = `${API_BASE_URL}/orders/${orderId}/receipt?format=${format}`;
|
|
26
26
|
const resp = await fetch(url, { headers: { Accept: format === "html" ? "text/html" : "text/plain" } });
|
|
27
27
|
if (!resp.ok) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared QueryClient factory — single source for query defaults + cache
|
|
3
|
+
* persistence. Apps inject only platform specifics: the error toast and the
|
|
4
|
+
* storage backend (MMKV on mobile, Tauri store on desktop).
|
|
5
|
+
*/
|
|
6
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
7
|
+
export interface QueryPersistStorage {
|
|
8
|
+
getItem: (key: string) => string | null | undefined | Promise<string | null | undefined>;
|
|
9
|
+
setItem: (key: string, value: string) => void | Promise<void>;
|
|
10
|
+
removeItem: (key: string) => void | Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export declare function createAppQueryClient(onMutationError: (message: string) => void): QueryClient;
|
|
13
|
+
export declare function createQueryPersister(storage: QueryPersistStorage, key?: string, throttleTime?: number): import("@tanstack/query-persist-client-core").Persister;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared QueryClient factory — single source for query defaults + cache
|
|
3
|
+
* persistence. Apps inject only platform specifics: the error toast and the
|
|
4
|
+
* storage backend (MMKV on mobile, Tauri store on desktop).
|
|
5
|
+
*/
|
|
6
|
+
import { MutationCache, QueryClient } from "@tanstack/react-query";
|
|
7
|
+
import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
|
|
8
|
+
import { getConnectivityState } from "../adapters/connectivity";
|
|
9
|
+
export function createAppQueryClient(onMutationError) {
|
|
10
|
+
return new QueryClient({
|
|
11
|
+
mutationCache: new MutationCache({
|
|
12
|
+
onError: (error) => {
|
|
13
|
+
try {
|
|
14
|
+
onMutationError(error instanceof Error ? error.message : "Something went wrong");
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
},
|
|
18
|
+
}),
|
|
19
|
+
defaultOptions: {
|
|
20
|
+
queries: {
|
|
21
|
+
// Offline-first: when the cloud is known unreachable (health probe
|
|
22
|
+
// failed / backend down), fail fast with zero retries — retrying a
|
|
23
|
+
// dead backend only multiplies error spam. Transient failures while
|
|
24
|
+
// reachable keep the standard 2 retries.
|
|
25
|
+
retry: (failureCount, error) => {
|
|
26
|
+
try {
|
|
27
|
+
if (getConnectivityState().isCloudReachable === "unreachable")
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
// Never retry explicit offline short-circuits (axios guard / adapters).
|
|
32
|
+
const msg = error instanceof Error ? error.message : String(error ?? "");
|
|
33
|
+
if (msg.startsWith("Offline") || error?.code === "OFFLINE")
|
|
34
|
+
return false;
|
|
35
|
+
return failureCount < 2;
|
|
36
|
+
},
|
|
37
|
+
refetchOnWindowFocus: true,
|
|
38
|
+
refetchOnReconnect: true,
|
|
39
|
+
staleTime: 30 * 1000,
|
|
40
|
+
gcTime: 5 * 60 * 1000,
|
|
41
|
+
},
|
|
42
|
+
mutations: {
|
|
43
|
+
retry: 1,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function createQueryPersister(storage, key = "teincfood-query-cache", throttleTime = 1000) {
|
|
49
|
+
return createAsyncStoragePersister({
|
|
50
|
+
storage: {
|
|
51
|
+
getItem: (k) => storage.getItem(k),
|
|
52
|
+
setItem: (k, v) => storage.setItem(k, v),
|
|
53
|
+
removeItem: (k) => storage.removeItem(k),
|
|
54
|
+
},
|
|
55
|
+
key,
|
|
56
|
+
throttleTime,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type EventCallback = (event: Record<string, unknown>) => void;
|
|
2
|
+
declare class EventsService {
|
|
3
|
+
private refCount;
|
|
4
|
+
private topic;
|
|
5
|
+
private joined;
|
|
6
|
+
private lastSequence;
|
|
7
|
+
private activeBusinessId;
|
|
8
|
+
private listeners;
|
|
9
|
+
private wildcardListeners;
|
|
10
|
+
private frameHandlerUnsub;
|
|
11
|
+
connect(userId: string, businessId?: string | null): () => void;
|
|
12
|
+
on(eventType: string, callback: EventCallback): () => void;
|
|
13
|
+
onAny(callback: EventCallback): () => void;
|
|
14
|
+
isConnected(): boolean;
|
|
15
|
+
resetSequence(): void;
|
|
16
|
+
}
|
|
17
|
+
export declare const eventsService: EventsService;
|
|
18
|
+
export {};
|