@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,76 @@
|
|
|
1
|
+
import { phoenixSocket } from "./phoenix-socket";
|
|
2
|
+
class EventsService {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.refCount = 0;
|
|
5
|
+
this.topic = null;
|
|
6
|
+
this.joined = false;
|
|
7
|
+
this.lastSequence = 0;
|
|
8
|
+
this.activeBusinessId = null;
|
|
9
|
+
this.listeners = new Map();
|
|
10
|
+
this.wildcardListeners = new Set();
|
|
11
|
+
this.frameHandlerUnsub = null;
|
|
12
|
+
}
|
|
13
|
+
connect(userId, businessId) {
|
|
14
|
+
this.refCount++;
|
|
15
|
+
const topic = `events:${userId}`;
|
|
16
|
+
if (businessId !== undefined)
|
|
17
|
+
this.activeBusinessId = businessId ?? null;
|
|
18
|
+
if (this.refCount === 1 && !this.frameHandlerUnsub) {
|
|
19
|
+
this.topic = topic;
|
|
20
|
+
this.frameHandlerUnsub = phoenixSocket.onFrame(topic, ([_joinRef, _ref, _topic, event, payload]) => {
|
|
21
|
+
const data = payload;
|
|
22
|
+
const payloadBid = data?.business_id;
|
|
23
|
+
if (this.activeBusinessId && payloadBid && payloadBid !== this.activeBusinessId)
|
|
24
|
+
return;
|
|
25
|
+
const sequence = data?._sequence;
|
|
26
|
+
if (typeof sequence === "number" && sequence <= this.lastSequence)
|
|
27
|
+
return;
|
|
28
|
+
if (typeof sequence === "number" && sequence > this.lastSequence)
|
|
29
|
+
this.lastSequence = sequence;
|
|
30
|
+
else if (typeof data?._sequence === "number")
|
|
31
|
+
this.lastSequence = data._sequence;
|
|
32
|
+
const typeListeners = this.listeners.get(event);
|
|
33
|
+
if (typeListeners)
|
|
34
|
+
typeListeners.forEach((cb) => cb(data));
|
|
35
|
+
this.wildcardListeners.forEach((cb) => cb(data));
|
|
36
|
+
});
|
|
37
|
+
const joinPayload = {};
|
|
38
|
+
if (this.lastSequence > 0)
|
|
39
|
+
joinPayload.last_sequence = this.lastSequence;
|
|
40
|
+
if (this.activeBusinessId)
|
|
41
|
+
joinPayload.business_id = this.activeBusinessId;
|
|
42
|
+
phoenixSocket.connect().then(() => phoenixSocket.joinChannel(topic, joinPayload)).then(() => { this.joined = true; }).catch(() => { });
|
|
43
|
+
}
|
|
44
|
+
else if (businessId !== undefined) {
|
|
45
|
+
// 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.
|
|
46
|
+
this.activeBusinessId = businessId ?? null;
|
|
47
|
+
}
|
|
48
|
+
let disconnected = false;
|
|
49
|
+
return () => {
|
|
50
|
+
if (disconnected)
|
|
51
|
+
return;
|
|
52
|
+
disconnected = true;
|
|
53
|
+
this.refCount = Math.max(0, this.refCount - 1);
|
|
54
|
+
if (this.refCount === 0) {
|
|
55
|
+
if (this.topic && this.joined)
|
|
56
|
+
phoenixSocket.leaveChannel(this.topic);
|
|
57
|
+
this.frameHandlerUnsub?.();
|
|
58
|
+
this.frameHandlerUnsub = null;
|
|
59
|
+
this.joined = false;
|
|
60
|
+
this.topic = null;
|
|
61
|
+
// reset sequence on full disconnect so next user doesn't dedup? Keep until next connect with same user - if user changes, lastSequence should reset. Caller should ensure new userId -> new instance or reset.
|
|
62
|
+
// Keep for replay.
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
on(eventType, callback) {
|
|
67
|
+
if (!this.listeners.has(eventType))
|
|
68
|
+
this.listeners.set(eventType, new Set());
|
|
69
|
+
this.listeners.get(eventType).add(callback);
|
|
70
|
+
return () => { this.listeners.get(eventType)?.delete(callback); };
|
|
71
|
+
}
|
|
72
|
+
onAny(callback) { this.wildcardListeners.add(callback); return () => this.wildcardListeners.delete(callback); }
|
|
73
|
+
isConnected() { return this.joined && phoenixSocket.isConnected(); }
|
|
74
|
+
resetSequence() { this.lastSequence = 0; }
|
|
75
|
+
}
|
|
76
|
+
export const eventsService = new EventsService();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { eventsService } from "./events.service";
|
|
2
|
+
const ORDER_EVENT_TYPES = [
|
|
3
|
+
"order.created",
|
|
4
|
+
"order.status_changed",
|
|
5
|
+
"delivery.assigned",
|
|
6
|
+
"delivery.status_changed",
|
|
7
|
+
"delivery.pickup_confirmed",
|
|
8
|
+
"payment.completed",
|
|
9
|
+
"payment.failed",
|
|
10
|
+
"reference_data.changed",
|
|
11
|
+
];
|
|
12
|
+
export const orderEventsService = {
|
|
13
|
+
onOrderUpdates(callback) {
|
|
14
|
+
const unsubs = ORDER_EVENT_TYPES.map((type) => eventsService.on(type, (data) => callback({ ...data, type })));
|
|
15
|
+
return () => unsubs.forEach((fn) => fn());
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type ConnectionState = "disconnected" | "connecting" | "connected" | "reconnecting";
|
|
2
|
+
export type PhoenixV2Frame = [string | null, string | null, string, string, Record<string, unknown>];
|
|
3
|
+
export type PhoenixOutbound = {
|
|
4
|
+
topic: string;
|
|
5
|
+
event: string;
|
|
6
|
+
payload: Record<string, unknown>;
|
|
7
|
+
ref?: string | null;
|
|
8
|
+
joinRef?: string | null;
|
|
9
|
+
};
|
|
10
|
+
type FrameHandler = (frame: PhoenixV2Frame) => void;
|
|
11
|
+
declare class PhoenixSocket {
|
|
12
|
+
socket: WebSocket | null;
|
|
13
|
+
private refCounter;
|
|
14
|
+
private reconnectAttempt;
|
|
15
|
+
private reconnectTimer;
|
|
16
|
+
private heartbeatTimer;
|
|
17
|
+
private heartbeatPend;
|
|
18
|
+
private heartbeatMissed;
|
|
19
|
+
private static readonly MAX_MISSED;
|
|
20
|
+
private intentionalClose;
|
|
21
|
+
private connectGen;
|
|
22
|
+
private connectPromise;
|
|
23
|
+
private connectionState;
|
|
24
|
+
private connectionListeners;
|
|
25
|
+
private pendingJoins;
|
|
26
|
+
private pendingPushes;
|
|
27
|
+
joinedChannels: Set<string>;
|
|
28
|
+
private pendingRejoin;
|
|
29
|
+
private topicJoinRefs;
|
|
30
|
+
private frameHandlers;
|
|
31
|
+
private getRef;
|
|
32
|
+
private buildSocketUrl;
|
|
33
|
+
private setState;
|
|
34
|
+
onConnectionStateChange(fn: (s: ConnectionState) => void): () => void;
|
|
35
|
+
getConnectionState(): ConnectionState;
|
|
36
|
+
connect(): Promise<void>;
|
|
37
|
+
disconnect(): void;
|
|
38
|
+
retryConnection(): void;
|
|
39
|
+
private scheduleReconnect;
|
|
40
|
+
private cancelReconnect;
|
|
41
|
+
private startHeartbeat;
|
|
42
|
+
private stopHeartbeat;
|
|
43
|
+
private sendRaw;
|
|
44
|
+
push(topic: string, event: string, payload: Record<string, unknown>): void;
|
|
45
|
+
pushWithReply(topic: string, event: string, payload: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
46
|
+
private handleFrame;
|
|
47
|
+
onFrame(topic: string, handler: FrameHandler): () => void;
|
|
48
|
+
joinChannel(topic: string, payload?: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
49
|
+
leaveChannel(topic: string): void;
|
|
50
|
+
private rejoinPending;
|
|
51
|
+
isConnected(): boolean;
|
|
52
|
+
hasJoined(topic: string): boolean;
|
|
53
|
+
}
|
|
54
|
+
export declare const phoenixSocket: PhoenixSocket;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { WS_BASE_URL } from "../utils/constants";
|
|
2
|
+
import { getAuthToken } from "../adapters/auth";
|
|
3
|
+
import { getConnectivityState } from "../adapters/connectivity";
|
|
4
|
+
import { recordAttempt } from "../utils/attempt-stats";
|
|
5
|
+
import { syncLogger } from "../utils/sync-logger";
|
|
6
|
+
const RECONNECT_BASE = 1000;
|
|
7
|
+
const RECONNECT_MAX = 10000;
|
|
8
|
+
const HEARTBEAT_MS = 30000;
|
|
9
|
+
class PhoenixSocket {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.socket = null;
|
|
12
|
+
this.refCounter = 0;
|
|
13
|
+
this.reconnectAttempt = 0;
|
|
14
|
+
this.reconnectTimer = null;
|
|
15
|
+
this.heartbeatTimer = null;
|
|
16
|
+
this.heartbeatPend = false;
|
|
17
|
+
this.heartbeatMissed = 0;
|
|
18
|
+
this.intentionalClose = false;
|
|
19
|
+
this.connectGen = 0;
|
|
20
|
+
this.connectPromise = null;
|
|
21
|
+
this.connectionState = "disconnected";
|
|
22
|
+
this.connectionListeners = new Set();
|
|
23
|
+
this.pendingJoins = new Map();
|
|
24
|
+
this.pendingPushes = new Map();
|
|
25
|
+
this.joinedChannels = new Set();
|
|
26
|
+
this.pendingRejoin = new Set();
|
|
27
|
+
this.topicJoinRefs = new Map();
|
|
28
|
+
this.frameHandlers = new Map();
|
|
29
|
+
}
|
|
30
|
+
getRef() { return String(++this.refCounter); }
|
|
31
|
+
buildSocketUrl(token) {
|
|
32
|
+
const base = WS_BASE_URL.replace(/\/+$/, "");
|
|
33
|
+
const path = base.endsWith("/ws") ? "/websocket" : "/ws/websocket";
|
|
34
|
+
return `${base}${path}?token=${encodeURIComponent(token)}&vsn=2.0.0`;
|
|
35
|
+
}
|
|
36
|
+
setState(s) {
|
|
37
|
+
this.connectionState = s;
|
|
38
|
+
this.connectionListeners.forEach((fn) => fn(s));
|
|
39
|
+
}
|
|
40
|
+
onConnectionStateChange(fn) {
|
|
41
|
+
this.connectionListeners.add(fn);
|
|
42
|
+
return () => this.connectionListeners.delete(fn);
|
|
43
|
+
}
|
|
44
|
+
getConnectionState() { return this.connectionState; }
|
|
45
|
+
connect() {
|
|
46
|
+
if (this.socket?.readyState === WebSocket.OPEN)
|
|
47
|
+
return Promise.resolve();
|
|
48
|
+
if (this.connectPromise)
|
|
49
|
+
return this.connectPromise;
|
|
50
|
+
// Offline-first: don't even open a socket when the cloud is known
|
|
51
|
+
// unreachable — the browser logs a WS error per attempt otherwise.
|
|
52
|
+
try {
|
|
53
|
+
if (getConnectivityState().isCloudReachable === "unreachable") {
|
|
54
|
+
this.scheduleReconnect();
|
|
55
|
+
return Promise.reject(new Error("Offline — cloud unreachable"));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
recordAttempt("ws_connect");
|
|
60
|
+
this.cancelReconnect();
|
|
61
|
+
this.intentionalClose = false;
|
|
62
|
+
this.setState("connecting");
|
|
63
|
+
this.connectPromise = new Promise((resolve, reject) => {
|
|
64
|
+
const token = getAuthToken();
|
|
65
|
+
if (!token) {
|
|
66
|
+
this.setState("disconnected");
|
|
67
|
+
this.connectPromise = null;
|
|
68
|
+
reject(new Error("Not authenticated"));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const myGen = ++this.connectGen;
|
|
72
|
+
const url = this.buildSocketUrl(token);
|
|
73
|
+
this.socket = new WebSocket(url);
|
|
74
|
+
this.socket.onopen = () => {
|
|
75
|
+
if (myGen !== this.connectGen)
|
|
76
|
+
return;
|
|
77
|
+
this.reconnectAttempt = 0;
|
|
78
|
+
this.setState("connected");
|
|
79
|
+
this.startHeartbeat();
|
|
80
|
+
this.rejoinPending().catch(() => { });
|
|
81
|
+
this.connectPromise = null;
|
|
82
|
+
resolve();
|
|
83
|
+
};
|
|
84
|
+
this.socket.onerror = () => { if (myGen !== this.connectGen)
|
|
85
|
+
return; if (this.connectionState === "connecting") {
|
|
86
|
+
this.setState("disconnected");
|
|
87
|
+
this.connectPromise = null;
|
|
88
|
+
} };
|
|
89
|
+
this.socket.onmessage = (ev) => { try {
|
|
90
|
+
const frame = JSON.parse(ev.data);
|
|
91
|
+
this.handleFrame(frame);
|
|
92
|
+
}
|
|
93
|
+
catch { } };
|
|
94
|
+
this.socket.onclose = () => {
|
|
95
|
+
if (myGen !== this.connectGen)
|
|
96
|
+
return;
|
|
97
|
+
this.stopHeartbeat();
|
|
98
|
+
if (!this.intentionalClose) {
|
|
99
|
+
this.joinedChannels.forEach((t) => this.pendingRejoin.add(t));
|
|
100
|
+
this.joinedChannels.clear();
|
|
101
|
+
this.scheduleReconnect();
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
this.joinedChannels.clear();
|
|
105
|
+
this.pendingRejoin.clear();
|
|
106
|
+
this.setState("disconnected");
|
|
107
|
+
}
|
|
108
|
+
this.connectPromise = null;
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
return this.connectPromise;
|
|
112
|
+
}
|
|
113
|
+
disconnect() {
|
|
114
|
+
this.intentionalClose = true;
|
|
115
|
+
this.cancelReconnect();
|
|
116
|
+
this.stopHeartbeat();
|
|
117
|
+
this.joinedChannels.clear();
|
|
118
|
+
this.pendingRejoin.clear();
|
|
119
|
+
this.pendingJoins.clear();
|
|
120
|
+
this.pendingPushes.clear();
|
|
121
|
+
this.topicJoinRefs.clear();
|
|
122
|
+
this.frameHandlers.clear();
|
|
123
|
+
if (this.socket) {
|
|
124
|
+
this.socket.close();
|
|
125
|
+
this.socket = null;
|
|
126
|
+
}
|
|
127
|
+
this.setState("disconnected");
|
|
128
|
+
this.connectPromise = null;
|
|
129
|
+
}
|
|
130
|
+
retryConnection() { this.cancelReconnect(); this.reconnectAttempt = 0; this.connect().catch(() => { }); }
|
|
131
|
+
scheduleReconnect() {
|
|
132
|
+
if (this.intentionalClose)
|
|
133
|
+
return;
|
|
134
|
+
this.setState("reconnecting");
|
|
135
|
+
// Offline-first: when the cloud is known unreachable (backend down),
|
|
136
|
+
// back off to a slow 60s poll instead of the 1s→10s hot loop — the
|
|
137
|
+
// useEventsConnection gate + probe regain path re-establishes fast.
|
|
138
|
+
let unreachable = false;
|
|
139
|
+
try {
|
|
140
|
+
unreachable = getConnectivityState().isCloudReachable === "unreachable";
|
|
141
|
+
}
|
|
142
|
+
catch { }
|
|
143
|
+
const delay = unreachable
|
|
144
|
+
? 60000
|
|
145
|
+
: Math.min(RECONNECT_BASE * Math.pow(2, this.reconnectAttempt), RECONNECT_MAX);
|
|
146
|
+
this.reconnectAttempt++;
|
|
147
|
+
recordAttempt("ws_reconnect", `delay=${delay} attempt=${this.reconnectAttempt}`);
|
|
148
|
+
try {
|
|
149
|
+
syncLogger.debug("PhoenixSocket", "scheduleReconnect", { delay, attempt: this.reconnectAttempt });
|
|
150
|
+
}
|
|
151
|
+
catch { }
|
|
152
|
+
this.reconnectTimer = setTimeout(() => this.connect().catch(() => { }), delay);
|
|
153
|
+
}
|
|
154
|
+
cancelReconnect() { if (this.reconnectTimer) {
|
|
155
|
+
clearTimeout(this.reconnectTimer);
|
|
156
|
+
this.reconnectTimer = null;
|
|
157
|
+
} }
|
|
158
|
+
startHeartbeat() {
|
|
159
|
+
this.heartbeatPend = false;
|
|
160
|
+
this.heartbeatMissed = 0;
|
|
161
|
+
this.heartbeatTimer = setInterval(() => {
|
|
162
|
+
recordAttempt("ws_heartbeat", `missed=${this.heartbeatMissed}`);
|
|
163
|
+
if (this.heartbeatPend) {
|
|
164
|
+
this.heartbeatMissed++;
|
|
165
|
+
if (this.heartbeatMissed >= PhoenixSocket.MAX_MISSED)
|
|
166
|
+
this.socket?.close();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.heartbeatPend = true;
|
|
170
|
+
this.sendRaw({ topic: "phoenix", event: "heartbeat", payload: {} });
|
|
171
|
+
}, HEARTBEAT_MS);
|
|
172
|
+
}
|
|
173
|
+
stopHeartbeat() { if (this.heartbeatTimer) {
|
|
174
|
+
clearInterval(this.heartbeatTimer);
|
|
175
|
+
this.heartbeatTimer = null;
|
|
176
|
+
} this.heartbeatPend = false; this.heartbeatMissed = 0; }
|
|
177
|
+
sendRaw(msg) {
|
|
178
|
+
if (this.socket?.readyState === WebSocket.OPEN) {
|
|
179
|
+
const joinRef = msg.topic && msg.topic !== "phoenix" ? (msg.joinRef ?? this.topicJoinRefs.get(msg.topic) ?? null) : (msg.joinRef ?? null);
|
|
180
|
+
const frame = [joinRef, msg.ref ?? null, msg.topic, msg.event, msg.payload];
|
|
181
|
+
this.socket.send(JSON.stringify(frame));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
push(topic, event, payload) { const ref = this.getRef(); this.sendRaw({ topic, event, payload, ref, joinRef: this.topicJoinRefs.get(topic) }); }
|
|
185
|
+
pushWithReply(topic, event, payload) {
|
|
186
|
+
const ref = this.getRef();
|
|
187
|
+
const msg = { topic, event, payload, ref, joinRef: this.topicJoinRefs.get(topic) };
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
this.pendingPushes.set(ref, { resolve, reject });
|
|
190
|
+
this.sendRaw(msg);
|
|
191
|
+
setTimeout(() => { if (this.pendingPushes.has(ref)) {
|
|
192
|
+
this.pendingPushes.delete(ref);
|
|
193
|
+
reject(new Error(`Request timed out: ${event} on ${topic}`));
|
|
194
|
+
} }, 5000);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
handleFrame([joinRef, ref, topic, event, payload]) {
|
|
198
|
+
if (event === "phx_reply" && topic === "phoenix") {
|
|
199
|
+
this.heartbeatPend = false;
|
|
200
|
+
this.heartbeatMissed = 0;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (event === "phx_reply" && ref) {
|
|
204
|
+
const pending = this.pendingPushes.get(ref);
|
|
205
|
+
if (pending) {
|
|
206
|
+
const status = payload?.status;
|
|
207
|
+
const response = (payload?.response ?? {});
|
|
208
|
+
if (status === "ok")
|
|
209
|
+
pending.resolve(response);
|
|
210
|
+
else
|
|
211
|
+
pending.reject(new Error(String(response.reason ?? "Request failed")));
|
|
212
|
+
this.pendingPushes.delete(ref);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (event === "phx_reply" && payload?.status === "ok") {
|
|
217
|
+
const pending = this.pendingJoins.get(topic);
|
|
218
|
+
if (pending) {
|
|
219
|
+
const response = (payload?.response ?? {});
|
|
220
|
+
pending.resolve(response);
|
|
221
|
+
this.pendingJoins.delete(topic);
|
|
222
|
+
this.joinedChannels.add(topic);
|
|
223
|
+
if (joinRef)
|
|
224
|
+
this.topicJoinRefs.set(topic, joinRef);
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (event === "phx_reply" && payload?.status === "error") {
|
|
229
|
+
const pending = this.pendingJoins.get(topic);
|
|
230
|
+
if (pending) {
|
|
231
|
+
const response = (payload?.response ?? {});
|
|
232
|
+
pending.reject(new Error(String(response.reason ?? "Join failed")));
|
|
233
|
+
this.pendingJoins.delete(topic);
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const handlers = this.frameHandlers.get(topic);
|
|
238
|
+
if (handlers)
|
|
239
|
+
handlers.forEach((cb) => cb([joinRef, ref, topic, event, payload]));
|
|
240
|
+
}
|
|
241
|
+
onFrame(topic, handler) {
|
|
242
|
+
if (!this.frameHandlers.has(topic))
|
|
243
|
+
this.frameHandlers.set(topic, new Set());
|
|
244
|
+
this.frameHandlers.get(topic).add(handler);
|
|
245
|
+
return () => { this.frameHandlers.get(topic)?.delete(handler); };
|
|
246
|
+
}
|
|
247
|
+
async joinChannel(topic, payload = {}) {
|
|
248
|
+
if (this.joinedChannels.has(topic))
|
|
249
|
+
return Promise.resolve({});
|
|
250
|
+
if (this.pendingJoins.has(topic))
|
|
251
|
+
return new Promise((resolve, reject) => { this.pendingJoins.set(topic, { resolve, reject }); });
|
|
252
|
+
const ref = this.getRef();
|
|
253
|
+
const msg = { topic, event: "phx_join", payload, ref };
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
this.pendingJoins.set(topic, { resolve, reject });
|
|
256
|
+
this.sendRaw(msg);
|
|
257
|
+
setTimeout(() => { if (this.pendingJoins.has(topic)) {
|
|
258
|
+
this.pendingJoins.delete(topic);
|
|
259
|
+
reject(new Error(`Failed to join channel ${topic} - timeout`));
|
|
260
|
+
} }, 5000);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
leaveChannel(topic) { this.joinedChannels.delete(topic); this.pendingRejoin.delete(topic); this.topicJoinRefs.delete(topic); }
|
|
264
|
+
async rejoinPending() { const topics = Array.from(this.pendingRejoin); this.pendingRejoin.clear(); await Promise.allSettled(topics.map((t) => this.joinChannel(t))); }
|
|
265
|
+
isConnected() { return this.socket?.readyState === WebSocket.OPEN; }
|
|
266
|
+
hasJoined(topic) { return this.joinedChannels.has(topic); }
|
|
267
|
+
}
|
|
268
|
+
PhoenixSocket.MAX_MISSED = 3;
|
|
269
|
+
export const phoenixSocket = new PhoenixSocket();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useEventsConnection(): void;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { eventsService } from "./events.service";
|
|
3
|
+
import { useSyncExternalStore } from "react";
|
|
4
|
+
import { getActiveBusiness, subscribeBusiness } from "../adapters/business";
|
|
5
|
+
import { getAuthToken } from "../adapters/auth";
|
|
6
|
+
import { getConnectivityState, subscribeConnectivity } from "../adapters/connectivity";
|
|
7
|
+
function useBusinessId() {
|
|
8
|
+
return useSyncExternalStore(subscribeBusiness, () => getActiveBusiness()?.id ?? null, () => null);
|
|
9
|
+
}
|
|
10
|
+
function useIsAuthenticated() {
|
|
11
|
+
// poll auth token presence; host should setAuthProvider — if no provider, assume authenticated when token exists
|
|
12
|
+
return !!getAuthToken();
|
|
13
|
+
}
|
|
14
|
+
function useIsEffectivelyOnline() {
|
|
15
|
+
const s = useSyncExternalStore(subscribeConnectivity, getConnectivityState, getConnectivityState);
|
|
16
|
+
return s.isOnline && s.isCloudReachable !== "unreachable";
|
|
17
|
+
}
|
|
18
|
+
export function useEventsConnection() {
|
|
19
|
+
const activeBusinessId = useBusinessId();
|
|
20
|
+
// derive userId from auth token? Host must provide via setAuthProvider that encodes user, but we need userId for topic.
|
|
21
|
+
// For now, read from global if host set it via setAuthUserId adapter. Fallback: token payload decode.
|
|
22
|
+
// Minimal: require host to call eventsService.connect with userId directly via wrapper — this hook is convenience that reads business only.
|
|
23
|
+
// To keep zero-config, we attempt to read from window localStorage token decode — fallback no-op.
|
|
24
|
+
const userId = deriveUserId();
|
|
25
|
+
// Offline-first: never open the Phoenix socket while the cloud is known
|
|
26
|
+
// unreachable (backend down). Connecting anyway just spins the 1s→10s
|
|
27
|
+
// reconnect loop and spams WS errors. Reconnect fires via effect dep.
|
|
28
|
+
const online = useIsEffectivelyOnline();
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!userId || !online)
|
|
31
|
+
return;
|
|
32
|
+
const disconnect = eventsService.connect(userId, activeBusinessId);
|
|
33
|
+
return () => disconnect();
|
|
34
|
+
}, [userId, activeBusinessId, online]);
|
|
35
|
+
}
|
|
36
|
+
function deriveUserId() {
|
|
37
|
+
try {
|
|
38
|
+
const token = getAuthToken();
|
|
39
|
+
if (!token)
|
|
40
|
+
return null;
|
|
41
|
+
const parts = token.split(".");
|
|
42
|
+
if (parts.length < 2)
|
|
43
|
+
return null;
|
|
44
|
+
const payload = JSON.parse(atob(parts[1]));
|
|
45
|
+
return payload.sub || payload.user_id || payload.id || null;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/reference/api.d.ts
CHANGED
|
@@ -25,3 +25,5 @@ export declare function fetchReferenceVersions(businessId: string): Promise<Refe
|
|
|
25
25
|
*/
|
|
26
26
|
export declare function fetchReferenceData(businessId: string, keys: ReferenceDataKey[]): Promise<Record<ReferenceDataKey, unknown>>;
|
|
27
27
|
export declare function fetchReferenceChanges<T>(businessId: string, key: ReferenceDataKey, fromVersion: string): Promise<ReferenceChangeSet<T>>;
|
|
28
|
+
export { setReferenceData, mergeReferenceData } from "./operations";
|
|
29
|
+
export { setOperatingHoursLocal, setBusinessProfileLocal } from "./operations";
|
package/dist/reference/api.js
CHANGED
|
@@ -40,3 +40,5 @@ export async function fetchReferenceChanges(businessId, key, fromVersion) {
|
|
|
40
40
|
const response = await api.get(`/businesses/${businessId}/reference-changes`, { params: { key, from_version: fromVersion } });
|
|
41
41
|
return response.data.data;
|
|
42
42
|
}
|
|
43
|
+
export { setReferenceData, mergeReferenceData } from "./operations";
|
|
44
|
+
export { setOperatingHoursLocal, setBusinessProfileLocal } from "./operations";
|
|
@@ -8,16 +8,21 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Host apps wire `businessService.fetchMyBusinessMembership` via adapters/services.
|
|
10
10
|
*/
|
|
11
|
+
import { useQuery } from "@tanstack/react-query";
|
|
11
12
|
/**
|
|
12
13
|
* Standard hook for both apps — businessId comes from the host's active business store.
|
|
13
|
-
*
|
|
14
|
+
* Offline-first: when offline, only the SQLite cache (`capabilities`) is used so
|
|
15
|
+
* nav filtering (menu/pos/orders/deliveries hidden) does not break when backend is down.
|
|
16
|
+
* When online, network refreshes the cache.
|
|
14
17
|
*/
|
|
15
|
-
export declare function useBusinessCapabilities(businessId?: string | null):
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
export declare function useBusinessCapabilities(businessId?: string | null): ReturnType<typeof useQuery> & {
|
|
19
|
+
data: {
|
|
20
|
+
role: string;
|
|
21
|
+
source: string;
|
|
22
|
+
capabilities: string[];
|
|
23
|
+
status: string;
|
|
24
|
+
} | undefined;
|
|
25
|
+
};
|
|
21
26
|
export declare function useHasCapabilityFor(businessId: string | undefined, capability: string): boolean;
|
|
22
27
|
export declare const CAPABILITY_BY_COMMAND: Record<string, string>;
|
|
23
28
|
export declare function hasCapabilitySync(businessId: string, commandType: string): Promise<boolean>;
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
* Host apps wire `businessService.fetchMyBusinessMembership` via adapters/services.
|
|
10
10
|
*/
|
|
11
11
|
import { useEffect, useState } from "react";
|
|
12
|
-
import { useQuery
|
|
12
|
+
import { useQuery } from "@tanstack/react-query";
|
|
13
13
|
import { openSyncDatabase } from "../db/connection";
|
|
14
14
|
import { getReferenceData, setReferenceData } from "./operations";
|
|
15
15
|
import { businessService } from "../adapters/services";
|
|
16
|
+
import { useIsEffectivelyOnline } from "../hooks/use-infinite-list";
|
|
16
17
|
const MEMBERSHIP_KEYS = {
|
|
17
18
|
all: ["my-membership"],
|
|
18
19
|
byBusiness: (id) => [...MEMBERSHIP_KEYS.all, id],
|
|
@@ -42,13 +43,14 @@ function useCapabilitiesPlaceholder(businessId) {
|
|
|
42
43
|
}
|
|
43
44
|
/**
|
|
44
45
|
* Standard hook for both apps — businessId comes from the host's active business store.
|
|
45
|
-
*
|
|
46
|
+
* Offline-first: when offline, only the SQLite cache (`capabilities`) is used so
|
|
47
|
+
* nav filtering (menu/pos/orders/deliveries hidden) does not break when backend is down.
|
|
48
|
+
* When online, network refreshes the cache.
|
|
46
49
|
*/
|
|
47
50
|
export function useBusinessCapabilities(businessId) {
|
|
48
|
-
const
|
|
49
|
-
void queryClient;
|
|
51
|
+
const isOnline = useIsEffectivelyOnline();
|
|
50
52
|
const cached = useCapabilitiesPlaceholder(businessId ?? null);
|
|
51
|
-
|
|
53
|
+
const query = useQuery({
|
|
52
54
|
queryKey: MEMBERSHIP_KEYS.byBusiness(businessId ?? ""),
|
|
53
55
|
queryFn: async () => {
|
|
54
56
|
const res = (await businessService.fetchMyBusinessMembership(businessId));
|
|
@@ -64,16 +66,26 @@ export function useBusinessCapabilities(businessId) {
|
|
|
64
66
|
catch { }
|
|
65
67
|
return res;
|
|
66
68
|
},
|
|
67
|
-
enabled: !!businessId,
|
|
68
|
-
staleTime:
|
|
69
|
+
enabled: !!businessId && isOnline,
|
|
70
|
+
staleTime: 30 * 1000,
|
|
71
|
+
retry: false,
|
|
72
|
+
networkMode: "offlineFirst",
|
|
69
73
|
placeholderData: cached ?? undefined,
|
|
70
|
-
select: (data) => ({
|
|
71
|
-
role: data.data.role,
|
|
72
|
-
source: data.data.source,
|
|
73
|
-
capabilities: data.data.capabilities,
|
|
74
|
-
status: data.data.status,
|
|
75
|
-
}),
|
|
76
74
|
});
|
|
75
|
+
// When offline, use the SQLite-cached membership directly so nav stays visible.
|
|
76
|
+
const raw = query.data ?? cached ?? null;
|
|
77
|
+
const selected = raw
|
|
78
|
+
? {
|
|
79
|
+
role: raw.data.role,
|
|
80
|
+
source: raw.data.source,
|
|
81
|
+
capabilities: raw.data.capabilities,
|
|
82
|
+
status: raw.data.status,
|
|
83
|
+
}
|
|
84
|
+
: undefined;
|
|
85
|
+
return {
|
|
86
|
+
...query,
|
|
87
|
+
data: selected,
|
|
88
|
+
};
|
|
77
89
|
}
|
|
78
90
|
export function useHasCapabilityFor(businessId, capability) {
|
|
79
91
|
const { data } = useBusinessCapabilities(businessId);
|
|
@@ -93,6 +105,9 @@ export const CAPABILITY_BY_COMMAND = {
|
|
|
93
105
|
"menu:update_item": "menu_manage",
|
|
94
106
|
"menu:delete_item": "menu_manage",
|
|
95
107
|
"menu:create_category": "menu_manage",
|
|
108
|
+
"menu:update_category": "menu_manage",
|
|
109
|
+
"menu:delete_category": "menu_manage",
|
|
110
|
+
"payment:confirm_cash": "orders_accept",
|
|
96
111
|
};
|
|
97
112
|
export async function hasCapabilitySync(businessId, commandType) {
|
|
98
113
|
const required = CAPABILITY_BY_COMMAND[commandType];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference-data React hooks — single source for both apps.
|
|
3
|
+
* Sync hooks use core connectivity/business adapters; cache reads are pure
|
|
4
|
+
* SQLite (never network). Image-query invalidation uses the shared
|
|
5
|
+
* ["cachedImage"] prefix both apps use.
|
|
6
|
+
*/
|
|
7
|
+
import type { ReferenceDataKey, ReferenceSyncOptions, ReferenceSyncResult } from "./types";
|
|
8
|
+
export declare const REFERENCE_KEYS: {
|
|
9
|
+
all: readonly ["reference"];
|
|
10
|
+
sync: readonly ["reference", "sync"];
|
|
11
|
+
data: (key: ReferenceDataKey) => readonly ["reference", "data", ReferenceDataKey];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Trigger reference data synchronization when the active business changes.
|
|
15
|
+
* Does not return data; fires a background sync. Set `enabled` to false to
|
|
16
|
+
* disable automatic sync.
|
|
17
|
+
*/
|
|
18
|
+
export declare function useReferenceDataSync(businessId: string | undefined, options?: Omit<ReferenceSyncOptions, "businessId"> & {
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
}): void;
|
|
21
|
+
/** Manually trigger reference data sync. */
|
|
22
|
+
export declare function useSyncReferenceData(): (options: ReferenceSyncOptions) => Promise<ReferenceSyncResult>;
|
|
23
|
+
/**
|
|
24
|
+
* App-level reference sync: downloads every reference-backed dataset into
|
|
25
|
+
* the SQLite cache so the business operates fully offline. Call once from
|
|
26
|
+
* the app root/layouts. Strict reachable gate — `unknown` (cold start)
|
|
27
|
+
* must not fire sync, otherwise airplane launch hangs on versions fetch.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useBusinessReferenceDataSync(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Read a reference data record from the local SQLite cache for the active
|
|
32
|
+
* business. Never hits the network.
|
|
33
|
+
*/
|
|
34
|
+
export declare function useReferenceData<T>(key: ReferenceDataKey): import("@tanstack/react-query").UseQueryResult<import("./types").ReferenceDataRecord<T> | null, Error>;
|